The Definitive Guide to Grails is a great book but like each book it contains a few errata. It is possible to submit errata on the book's page but these are not publicly available and therefore it is not possible to know if the errata you've found are new or have already been submitted many times. This page should offer a better opportunity to submit errata, addendum and small simplifications.
Page 15
- The book says: Grails created a test class called HelloTests.groovy for the HelloController, but it created a class HelloControllerTest.groovy
Page 21
- Assertions are not introduced in Java 5, but are also part of Java 1.4
page 28
- variable should be person and not fred
- 17.<66 should be 17..<66
page 29
for(i in 0..<text[0..4]) {
println text[i]
}
should be
for(i in 0..<4) {
println text[i]
}
page 49
Since version 1.2.12 log4j has also a TRACE level
page 64
The version column is missing from figure 4-2.
page 65
static optionals = ['notes']
Support for 'optionals' property will be completely removed in 0.6, i.e. this won't work in 1.0. Use:
static constraints = { notes (nullable:true) //.. }
pages 66-67
Given that add* is deprecated, the bottom of page 66 should read: "Not only that, GORM also automatically provides an implementation of an addToTags method that makes it easy to work with the association."
The example at the top of page 67 should read in part:
//now add some tags b.addToTags( new Tag(name: 'grails' ) ) b.addToTags( new Tag(name: 'web framework') )
page 82
password(matches: /[\w\d]+/, length:6..12)
\d is useless as \w is already [a-zA-Z_0-9]
page 83
In table 4-2
maxLength login(maxLength:5) Sets the maximum length of a string or array property
NOTE: maxLength has been deprecated (0.4) then removed (0.5), use maxSize, e.g. login(maxSize:5)
page 86
return ['passwordEqualsLastName', lastName]
should be
return ['EqualsLastName', obj.lastName]
The following script can be helpful to execute code above to test for the last name error in the grails console:
def user = new User(login:'barry', password:'barry', firstName:'barry1', lastName:'barry', email:'barry@fake.com') if(user.save()){ println "User $user created" } else { users.errors.allErrors.each { println ctx.getBean('messageSource').getMessage(it, Locale.getDefault()) } }
Outstanding issue: unable to get arguments to resolve using the above script. If you examine the contents of the FieldErrror object the argument that is being passed from the constraint is not added to the FieldError arguments.
In the listing 4-31 and 4-32 the "?. operator is used". This operator is presented on page 102 for those who are not familiar with it.
page 103
notes(maxLength:1000) maxLength has been deprecated (0.4) then removed (0.5), use maxSize, e.g. notes(maxSize:1000)
page 115
new Bookmark(title:"Canoo",url:"http://canoo.com").save()
should be
new Bookmark(title:"Canoo",url:"http://www.canoo.com").save()
shouldn't it actually be
new Bookmark(title:"Canoo",url:new URL("http://www.canoo.com")).save()
as it is in the rest of the book
page 125,127
mock1.demand.render { Map params ->
should be
ctrlMock.demand.render { Map params ->
page 130
Main feature of property webtest_showhtmlparseroutput is to control if html parsing messages should be saved in the WebTest report or not
page 144
It appears that as of Grails 1.0 the log4j configuration is now located at grails-app/conf/Config.groovy. Also, how logging is defined is quite a bit different than the book indicates and you are encouraged to reference the Grails 1.0+ User Reference Guide under section 3.1.
page 162
Input fields should have an id otherwise the <label for="...">...</label> are useless.
See GRAILS-540
page 165
<div class="errors">
should be
<div class="message">${flash.message}</div> <div class="errors">
This allows the password mis-match error to actually appear.
<g:renderErrors bean="${flash.user}"/>
should be
<g:hasErrors bean="${flash.user}"> <g:renderErrors bean="${flash.user}"/> </g:hasErrors>
<input type="confirm" name="confirm" />
should be
<input type="password" name="confirm" />
page 166
if( user.save() ) {
redirect( controller: 'bookmark', action: 'list' )
}
should be
if( ! user.hasErrors() && user.save() ) {
session.user = user
redirect( controller: 'bookmark', action: 'list' )
}
Otherwise, without setting the session.user to the newly created user, we simply go back to the login page because of the security intercept. Also, the check for hasErrors() seems to be more in line with how Groovy 1.0 does things.
page 168
<form action="upload" enctype="multipart/form-data">
should be
<form action="upload" method="post" enctype="multipart/form-data">
page 174
<p>${bookmark.title}</p>
is correct in JSP too since version 2.0.
Note that this is not exactly the same than
<p><c:out value="${bookmark.title}"/></p>
as the <c:out.../> escapes xml special characters what is not done by ${bookmark.title} (neither in GSP nor in JSP 2.0). This is important to avoid Javascript Cross Site Scripting (XSS).
page 177
<g:each in="${bookmarks.tags?}">
should be
<g:each in="${bookmarks.tags}">
because
- the ? is useless as null.each {} is a valid Groovy expression... that does nothing
- bookmarks.tags? is not a valid Groovy expression, therefore this example only works due to current implementation detail of the <g:each ...> tag.
page 182
In the Linking Tags paragraph
... you are always linking to the write (sic) place in a consistent manner?
should be
... you are always linking to the right place in a consistent manner?
page 197
<g:form action="search">
should be
<g:form controller="bookmark" action="search">
because the search exists on all pages, even the tag controller pages, therefore the controller needs to be specified, else you get an error attempting to use the search form from the controller rendered pages
also
<g:submit value="search"/>
should be
<g:submitButton name="search" value="Search"/>
page 198
12 I like("name", params.q)
should be
12 ilike("name", "%${params.q}%")
also
6 if(params.q && !params.q?.indexOf('%')) {
should be
6 if(params.q && !params.q?.contains('%')) {
If the intention was to only perform the search if the user did not place a % in the search string. The way it is written now the user is forced to start all search strings with a %, though there is no indication that this must be done, and although the code appends a '%' character infront of the input string anyways. The bookmark controller in the current version of the example app (based on chapter 11) avoids this check altogether.
page 200
The new GSP should be placed in grails-app/views/bookmark/_bookmark.gsp
That's bookmark singular.
page 207
Listing 8-52 reads:
class BookmarkTagLib {
def repeat = { attrs, body ->
attrs.times?.toInteger().times { n ->
body(n)
}
}
}
But it should read:
class BookmarkTagLib {
def repeat = { attrs, body ->
attrs.times?.toInteger().times { n ->
out << body(n)
}
}
}
page 209
To make custom editInPlace tag working, Scriptaculous need to be added in the main layout grails-app/views/layouts/main.gsp:
<g:javascript library="scriptaculous"/>
Also, listing 8-55 reads:
9 body() 10 out << "</span>" 11 out << "<script type='text/javascript'>" 12 out << "new Ajax.InPlaceEditor('${id}', '" 13 createLink(attrs)
But on Grails 0.5, that won't output the results of body() or createLink(attrs). It should read:
9 out << body() 10 out << "</span>" 11 out << "<script type='text/javascript'>" 12 out << "new Ajax.InPlaceEditor('${id}', '" 13 out << createLink(attrs)
page 210
Listing 8-56 contains the following line:
url="[action:'updateNotes', id:id:bookmark.id]"
That should be:
url="[action:'updateNotes', id:bookmark.id]"
Listing 8-57 reads:
def updateNotes = {
update.call()
render( Bookmark.get(params.id)?.notes )
}
This depends on the update closure, which does more than just updating the record - it also redirects output to the show action. As a result, you'll end up with Show Bookmark page nested where the notes should be. An alternative is the following:
def updateNotes = {
def bookmark = Bookmark.get( params.id )
if(bookmark) {
bookmark.properties = params
if(bookmark.save())
render( Bookmark.get(params.id)?.notes )
else
render( "Error saving bookmark" )
}
}
page 211
Listing 8-58 reads:
class BookmarkController {
...
void testEditInPlace() throws Exception {
...
}
}
That should be:
class BookmarkTests extends GroovyTestCase { ... void testEditInPlace() throws Exception { ... } }
//
page 221
To continue on with the bookmark example, you will need to make other domain changes. Indeed, you will need to revisit many GSP pages, etc. In addition to adding the new domain class of TagReference, the author has also removed from the working Bookmark class two fields: rating and type. The current Bookmark domain class should look like the following:
class Bookmark {
static belongsTo = User
static hasMany = [tags:TagReference] // a bookmark has 1 to many tag references...
User user
URL url
String title
String notes
Date dateCreated
static constraints = {
url(url:true)
title(blank:false)
notes( nullable:true, maxLength: 1000 )
}
String toString() {
return "$title - $url"
}
}
Likewise, although not mentioned, the Tag domain element needs to change as well, since its clearly no longer belongs to the Bookmark domain.
class Tag {
String name
String toString() { name }
}
Notice the removal of the belongsTo declarative.
page 223
Due to a probably bug in Grails – the code to use the remoteField, and specifically to generate the update= clause needs to be changed.
<g:remoteField action="suggestTag" update="suggestions${bookmark?.id}" name="url" value="${bookmark?.url}" />
Should be:
<g:remoteField action="suggestTag" update="suggestions${bookamrk?.id ? bookmark.id : '' }" name="url" value="${bookmark?.url}" />
Why you ask? Currently (Grails 1.0.1 in any event) the phrase $
returns the string "null". This is one of those times. Later in the code when we actually
declare the <div id="suggestions$
page 226
As of Grails 1.0, the code to add the tag and create a TagRefernece is incorrect.
... b.addTagReference( ...
should be:
... b.addToTags( ...
page 228
I believe the following code for suggestTag is much better than the original for various reasons. First, it works. The original code had a boundary condition that caused a 404 error to appear when the URL was empty. At least, the way I interpreted the code from the book, which had a missing } somewhere (from the trim() I think).
Secondly, the code should be a little bit more efficient by not trying to find suggestions when there is no url.
Also note the use of toURL and not toUrl, which doesn't exist.
Finally, an important note – its key to ALWAYS render something from this routine, else the dreaded 404 due to its trying to default the action to finding a gsp page of the same name as the method being invoked, in this case it was looking for "suggestTag.gsp".
def suggestTag = {
def tags
def bookmark = params.id ? Bookmark.get(params.id) : new Bookmark()
if ( ! bookmark.url ) {
if ( params.value?.trim() ) {
if ( ! params.value?.startsWith("http://") ) {
bookmark.url = "http://${params.value}".toURL()
}
}
}
// If we have a url -- try to get the bookmarks
if ( bookmark.url ) {
tags = getSuggestions( bookmark )
}
// Must always render SOMETHING -- else the default action is to find a gsp page
render( template: 'suggest', model: [tags: tags, bookmark: bookmark] )
}
page 232
... <div id="editButtons"> <g:submit name="save" value="Save" /> <g:submitToRemote url="[action: 'show', id: bookmark.id]" update="bookmark${bookmark.id}" name="cancel" value="Cancel" /> </div> ...
should be:
... <div id="editButtons"> <g:submitButton name="save" value="Save" /> <g:submitToRemote url="[action: 'show', id: bookmark.id]" update="bookmark${bookmark.id}" name="cancel" value="Cancel" /> </div> ...
Note the use of <g:submitButton> instead of <g:submit> which doesn't appear to exist anymore (if it ever did) as of 1.0.
Also note do not use tabs (\t) to pretty up your gsp within at least a <g:render>, it will cause it not to find / parse the tag properly.
i.e. <g:render template="blah" ... /> using a tab between the <g:render and the "template" will not work. Must be a space (or one assumes multiple spaces).
page 236
The best solution for a real-world situation wouldn't be to perform caching but to avoid involving the server: the url is already available on the client side (in the bookmark link) and therefore the preview should be realised on the client side only.
page 245
The location to get the HTTP client jar files has changed within Apache. It appears the new home is http://hc.apache.org/.
page 250
A tip section would be great to explain the trick for string conversion in:
bookmarks << new Bookmark(title:"${p.@description}", url:new URL("${p.@href}"))
page 254
The use of the bookmark template to render the results from del.icio.us isn't optimum since, in its current version, it produces Edit, Delete, and Preview actions, none of which are valid for remote links. Either the bookmark template should be modified to optionally not render those actions, or a new smaller and shorter template be created.
page 266
Listing 10-20 includes:
Add Tag: <g:textField name="tagName" /> <g:submitButton value="Add" />
Should be:
Add Tag: <g:textField name="tagName" /> <g:submitButton value="Add" name="addButton" />
page 267
The create-job script and associated cool quartz stuff was moved to a plug-in as of Grails 1.0 and needs to be installed in the project via the
grails install-plugin quartz
command.
page 276
assert sw.toString().indexOf('<a href="http://grails.org/Download">Grails Download Page</a>')
should be
assert sw.toString().contains('<a href="http://grails.org/Download">Grails Download Page</a>')
because String.indexOf(...) returns -1 when nothing is found and -1 is not false according to the Groovy Truth.
page 278
contains(...) instead of indexOf(...) like on page 276
page 288
boolean equals(obj) { if (this == obj) return true
should be
boolean equals(obj) { if (this.is(obj)) return true
otherwise a StackOverflowError will occur as == is the Groovy equivalent of equals() in Java.