Errata and addendum in The Definitive Guide to Grails

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 $
Unknown macro: {bookmark?.id}

returns the string "null". This is one of those times. Later in the code when we actually
declare the <div id="suggestions$

" it does NOT return "null", rather it returns the empty string. Thus, for new bookmarks the two strings don't match ( "suggestionsnull" versus "suggestions") and you will never see the cool "Suggestions" area populate.


 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.








Enter labels to add to this page:
Please wait 
Looking for a label? Just start typing.