Contents


Mastering Grails

Rewiring Grails with custom URIs and codecs

Replacing primary keys in URIs with something more descriptive

Comments

Content series:

This content is part # of # in the series: Mastering Grails

Stay tuned for additional content in this series.

This content is part of the series:Mastering Grails

Stay tuned for additional content in this series.

In "Give your Grails applications a facelift," you saw how to make cosmetic changes to a Grails application — the Blogito blog site — using Cascading Style Sheets (CSS). This time around, I'll show you how to affect the lifeblood of any Web application: the URIs used for navigation. This is especially important in a weblog like Blogito. The permalinks back to individual entries are passed around the Internet like calling cards; the more descriptive they are, the more effective they are.

To generate more-descriptive URIs, you'll customize controller code to support the personalized URIs. You'll tweak the UrlMappings.groovy file to create new pathways. And finally, you'll create a custom codec to generate custom URIs more easily.

Understanding URIs

The U in URI officially stands for Uniform, but it could just as easily stand for Unique (see Related topics). If the URI http://www.ibm.com/developerworks didn't unambiguously identify the Web site you're at right now, it would be of little use. It also has the benefit of being a memorable Identifier for this Resource. You could get here by typing http://129.42.56.216, but few people are likely willing to commit the dotted decimal IP address of this Web site to memory.

So at the very least, a URI must be unique. Ideally, it should be memorable as well. (See the The debate surrounding opaque URIs sidebar for an opposing view on memorable URIs.) Grails definitely satisfies the first requirement. It uses a combination of the controller name, the closure name, and the database record's primary key to make each URI unique. For example, if you want to show people the first Entry in the database, have them type http://localhost:9090/blogito/entry/show/1 into their browsers.

Although including the primary key in the URI is a reasonable default, it offends my delicate sense of aesthetics in two ways. First, it's a bit of implementation bleed-through. This incidental database artifact has pierced the facade of the Web site. Google, Amazon, and eBay all use databases behind the scenes, but you'd be hard pressed to find evidence of it in their URIs. The second reason for eliminating the primary key from the URI is more semantic. Readers of Jane Smith's blog are more likely to identify her as jsmith than as 12. Similarly, listing blog entries by title instead of primary key satisfies the requirement for memorable URIs.

Creating the User class

Blogito already supports entries, but it doesn't support users yet. To get started, you should create a new User class.

To begin, type grails create-domain-class User at the command prompt. Next, add the code in Listing 1 to grails-app/domain/User.groovy:

Listing 1. The User class
class User {
  static constraints = {
    login(unique:true)
    password(password:true)
    name()
  }
  
  static hasMany = [entries:Entry]
  
  String login
  String password
  String name
  
  String toString(){
    name
  }
}

The login and password fields should be self-explanatory; they handle authentication. The name field is for display purposes. For example, "Jane Smith" will appear for the jsmith login. And as you can see, a one-to-many relationship exists between User and Entry.

Add the static belongsTo field to grails-app/domain/Entry.groovy as shown in Listing 2 to complete the one-to-many relationship:

Listing 2. Adding the one-to-many relationship to the Entry class
class Entry {
  static belongsTo = [author:User]
  //snip
}

Notice that you can easily rename the field while defining the relationship. The User class now has a field named entries. The Entry class now has a field named author.

Normally at this point, you'd create a corresponding UserController to provide a full UI for managing Users. I'm not interested in fighting that battle quite yet. I just want to have a couple of stubbed-out Users as placeholders. In the next Mastering Grails article, you'll flesh out the user authentication and authorization story more fully. In the spirit of "just enough to get by," add a couple of new users by using grails-app/conf/BootStrap.groovy, as shown in Listing 3:

Listing 3. Stubbing out Users in BootStrap.groovy
import grails.util.GrailsUtil

class BootStrap {

  def init = { servletContext ->
    switch(GrailsUtil.environment){
      case "development":
        def jdoe = new User(login:"jdoe", password:"password", name:"John Doe")
        def e1 = new Entry(title:"Grails 1.1 beta is out", 
           summary:"Check out the new features")
        def e2 = new Entry(title:"Just Released - Groovy 1.6 beta 2", 
           summary:"It is looking good.")
        jdoe.addToEntries(e1)
        jdoe.addToEntries(e2)
        jdoe.save()
        
        def jsmith = new User(login:"jsmith", password:"wordpass", name:"Jane Smith")
        def e3 = new Entry(title:"Codecs in Grails", summary:"See Mastering Grails")
        def e4 = new Entry(title:"Testing with Groovy", summary:"See Practically Groovy")
        jsmith.addToEntries(e3)
        jsmith.addToEntries(e4)
        jsmith.save()              
      break

      case "production":
      break
    }

  }
  def destroy = {
  }
}

Notice how you assign entries to a User. You don't need to worry about messing around with the primary key or foreign key. The Grails Object Relational Mapping (GORM) API allows you to think in terms of objects, not relational-database theory.

Next, make a slight tweak to the grails-app/views/entry/_entry.gsp partial template that you created in the last article. Display the author next to the Entry.lastUpdated field, as shown in Listing 4:

Listing 4. Adding author to _entry.gsp
<div class="entry">
  <span class="entry-date">
    <g:longDate>${entryInstance.lastUpdated}</g:longDate> : ${entryInstance.author}
  </span>
  <h2><g:link action="show" id="${entryInstance.id}">${entryInstance.title}
    </g:link></h2>                  
  <p>${entryInstance.summary}</p>
</div>

${entryInstance.author} calls the toString() method on the User class. Alternately, you could use ${entryInstance.author.name} to display the field of your choice explicitly. You can traverse a nested hierarchy of classes as deeply as you'd like using this syntax.

It's time to see your changes in action. Type grails run-app and visit http://localhost:9090/blogito/ in a Web browser. Your screen should look like Figure 1:

Figure 1. Entries showing the newly added author
Entries showing the newly added author
Entries showing the newly added author

Now that Blogito supports multiple users, the next step is to allow the reader to view the entries by author.

Displaying entries by author

The ultimate goal is to support URIs like http://localhost:9090/blogito/entry/list/jdoe. Notice that User.login appears in the URI instead of the primary key. Along the way, you'll also need to make a slight adjustment to pagination.

The scaffolded behavior of EntryController.list doesn't allow you to filter by User. Listing 5 shows you the default implementation of the list closure:

Listing 5. The default list implementation
def list = {
    if(!params.max) params.max = 10
    [ entryInstanceList: Entry.list( params ) ]
}

You'll need to expand this to support an optional user name at the end of the path. Edit grails-app/controllers/EntryController.groovy and add a new list closure as shown in Listing 6:

Listing 6. Limiting the list by author
class EntryController {
  def scaffold = Entry
  
  def list = {
      if(!params.max) params.max = 10
      flash.id = params.id
      if(!params.id) params.id = "No User Supplied"

      def entryList
      def entryCount
      def author = User.findByLogin(params.id)
      if(author){
        def query = { eq('author', author) }
        entryList = Entry.createCriteria().list(params, query)        
        entryCount = Entry.createCriteria().count(query)
      }else{
        entryList = Entry.list( params )
        entryCount = Entry.count()
      }
      
      [ entryInstanceList:entryList, entryCount:entryCount  ]
  }  
}

The first thing you should notice is that params.max and params.id are populated with sensible defaults if they aren't supplied by the end user. Don't worry about flash.id for right now — I'll discuss it later when talking about pagination issues.

The params.id value is usually an integer — the primary key, to be exact. You're used to seeing URIs like /entry/show/1 and entry/edit/2. I could have set up a mapping in grails-app/conf/UrlMappings.groovy to return a more descriptive name like params.name or params.login, but the current mapping already grabs the path element after the action name and stores it in params.id. I'm just taking advantage of existing behavior. Look in URLMapper.groovy, shown in Listing 7, to see the default mapping that returns params.id:

Listing 7. The default mapping in UrlMappings.groovy
class UrlMappings {
    static mappings = {
      "/$controller/$action?/$id?"{
	      constraints {}
	  }
    //snip
	}
}

Because this isn't the primary key of the User, you can't use User.get(params.id) as you normally would. Instead, you must use User.findByLogin(params.id).

If a matching User is found, you create a query block. This is the Hibernate Criteria Builder in action (see Related topics). In this case, you are limiting the list to entries that match a specific author. Again, notice that GORM allows you to think in terms of objects instead of primary or foreign keys.

If no author matches params.id, the full list of entries is returned: entryList = Entry.list( params ).

Notice that you calculate the entryCount value explicitly. Scaffolded GroovyServer Pages (GSP) code normally calls Entry.count() in the <g:paginate> tag. Because you are potentially passing back a filtered list, you need to handle this in a variable in the controller instead.

Storing the params.id value in flash.id allows the application to pass the query criteria back to the <g:paginate> tag. Adjust the <g:paginate> in grails-app/views/entry/list.gsp to take advantage of the new entryCount variable and the parameters stored in flash scope, as shown in Listing 8:

Listing 8. Adjusting the list.gsp page for custom pagination
<div class="paginateButtons">
  <g:paginate total="${entryCount}" params="${flash}"/>
</div>

Restart Grails and visit http://localhost:9090/blogito/entry/list/jsmith in a Web browser. Your screen should look like Figure 2:

Figure 2. Listing entries by author
Listing entries by author
Listing entries by author

To ensure that pagination still works, type http://localhost:9090/blogito/entry/list/jsmith?max=1. Click on the Previous and Next buttons to ensure that only Jane's blog entries appear, as shown in Figure 3:

Figure 3. Testing custom pagination
Testing custom pagination
Testing custom pagination

Now that basic filtering by author is in place, you can take things a step further by creating an even friendlier custom URI.

Creating a custom URI

The UrlMappings.groovy file gives you extraordinary flexibility with creating new URIs. Although http://localhost:9090/blogito/entry/list/jsmith is certainly functional, suppose you get a late-breaking user request to support URIs like http://localhost:9090/blogito/blog/jsmith too. No problem! Add a new mapping to UrlMappings.groovy as shown in Listing 9:

Listing 9. Adding a custom mapping to UrlMappings.groovy
class UrlMappings {
    static mappings = {
      "/$controller/$action?/$id?"{
	      constraints {
			 // apply constraints here
		  }
	  }
	  "/"(controller:"entry")
	  "/blog/$id"(controller:"entry", action="list")
	  "500"(view:'/error')
	}
}

Now URIs that begin with /blog will be redirected to the entry controller and the list action. Although $user or $login might be more descriptive, keeping $id consistent with Grails conventions means that both "/$controller/$action?/$id?" and "/blog/$id"(controller:"entry", action="list") can point to the same endpoint.

Type http://localhost:9090/blogito/blog/jsmith in a Web browser to verify that the mapping works.

Now that you have Users taken care of, it's time to focus on creating friendlier URIs for Entries as well.

Creating a custom codec

When you use User.login instead of User.id, the URI is easy because it doesn't contain spaces. Admittedly, no validation rule currently enforces this "no space" rule, but you could easily add one to ensure compliance (see Related topics).

But what about replacing Entry.id with Entry.title in the URI? Titles almost certainly will have spaces in them. One solution is to add another field to the Entry class and have the end user retype the title without spaces. This isn't ideal because it forces users to do more work and forces you to write another validation rule to make sure that they did it correctly. A better solution is to have Grails automatically convert spaces to underscores depending on where the Entry.title is used. To accomplish this, you need to create a custom codec (short for coder-decoder).

Create grails-app/utils/UnderscoreCodec and add the code in Listing 10:

Listing 10. A custom codec
class UnderscoreCodec {
  static encode = {target->
    target.replaceAll(" ", "_")
  }
  
  static decode = {target->
    target.replaceAll("_", " ")
  }
}

Grails offers a few built-in codecs out of the box: HtmlCodec, UrlCodec, Base64Codec, and JavaScriptCodec (see Related topics). The HtmlCodec is the source of the encodeAsHtml() and decodeHtml() methods you see in the generated GSP files.

You can just as easily add your own codec to the mix. Grails uses any class in the grails-app/utils directory with a Codec suffix to add encodeAs() and decode() methods to Strings. In this case, all Strings in Blogito now magically have two new methods: encodeAsUnderscore() and decodeUnderscore().

Verify this by creating UnderscoreCodecTests.groovy, shown in Listing 11, in test/integration:

Listing 11. Testing a custom codec
class UnderscoreCodecTests extends GroovyTestCase {
  void testEncode() {
    String test = "this is a test"
    assertEquals "this_is_a_test", test.encodeAsUnderscore()
  }
  
  void testDecode() {
    String test = "this_is_a_test"
    assertEquals "this is a test", test.decodeUnderscore()
  }
}

Type grails test-app at the command prompt to run the tests. You should see results similar to those in Listing 12:

Listing 12. Output showing successful a successful test run
$ grails test-app
-------------------------------------------------------
Running 2 Integration Tests...
Running test UnderscoreCodecTests...
                    testEncode...SUCCESS
                    testDecode...SUCCESS
Integration Tests Completed in 157ms
-------------------------------------------------------

Codecs in action

Now that the UnderscoreCodec is in place, you have everything you need to support URIs that include both the user and entry title — for example, http://localhost:9090/blogito/blog/jsmith/this_is_my_latest_entry.

To begin, tweak the /blog mapping in UrlMappings.groovy to support an optional $title, as shown in Listing 13. Recall that a trailing question mark makes things optional in Groovy.

Listing 13. Allowing an optional title in the URI mapping
class UrlMappings {
    static mappings = {
      "/$controller/$action?/$id?"{
	      constraints {
			 // apply constraints here
		  }
	  }
	  "/"(controller:"entry")
	  "/blog/$id/$title?"(controller:"entry", action="list")
	  "/entry/$action?/$id?/$title?"(controller:"entry")
	  "500"(view:'/error')
	}
}

Next, adjust the EntryController.list to account for the new params.title value, as shown in Listing 14:

Listing 14. Handling the params.title in the controller
class EntryController {
  def scaffold = Entry
  
  def list = {
      if(!params.max) params.max = 10
      flash.id = params.id
      if(!params.id) params.id = "No User Supplied"
      flash.title = params.title
      if(!params.title) params.title = ""

      def author = User.findByLogin(params.id)
      def entryList
      def entryCount
      if(author){
        def query = { 
          and{
            eq('author', author) 
            like("title", params.title.decodeUnderscore() + '%')
          }
        }  
        entryList = Entry.createCriteria().list(params, query)        
        entryCount = Entry.createCriteria().count(query)
      }else{
        entryList = Entry.list( params )
        entryCount = Entry.count()
      }
      
      [ entryInstanceList:entryList, entryCount:entryCount  ]
  }  
}

I've used like in the query to make the URI more flexible. For example, the user can type /blog/jsmith/mastering_grails to return all titles that begin with mastering_grails. If you'd prefer to be more strict, you can use the eq method in the query instead to require an exact match.

Type http://localhost:9090/blogito/blog/jsmith/Codecs_in_Grails in your Web browser to see your new codec in action. Your screen should look like Figure 4:

Figure 4. Viewing a blog entry by user name and title
Viewing a blog entry by user name and title
Viewing a blog entry by user name and title

Conclusion

URIs are the lifeblood of a Web application. Grails' sensible defaults are a great way to get started, but you should also feel comfortable customizing the URIs to suit your Web site's requirements in the best way. Thanks to your hard work, Blogito now has Users and Entries. But more important, you can now use something other than the primary key in the URI to view them. You saw how to create friendly URIs by adjusting controller code, add mappings to UrlMappings.groovy, and create a custom codec.

Next time, you'll create a login form so that Blogito Users can be authenticated. Once they are logged in, they'll be able to upload a file for the body of a blog entry — HTML, an image, or even an MP3 file. Until then, have fun mastering Grails.


Downloadable resources


Related topics

  • Mastering Grails: Read more in this series to gain a further understanding of Grails and all you can do with it.
  • Uniform Resource Identifier: See Wikipedia's entry on URI.
  • TinyURL.com: You can use this site to convert long, cumbersome URLs into short ones that don't expire.
  • Hibernate Criteria Builder: This Grails builder helps you create criteria-based queries.
  • Domain Class Validation: Check out the Grails validation reference.
  • Dynamic Encoding Methods: Find out more about creating custom codecs in Grails.
  • Groovy Recipes (Scott Davis, Pragmatic Programmers, 2008): Learn more about Groovy and Grails in Scott Davis' latest book.
  • Practically Groovy: This developerWorks series is dedicated to exploring the practical uses of Groovy and teaching you when and how to apply them successfully.
  • Blogito: You can download the completed Blogito application.

Comments

Sign in or register to add and subscribe to comments.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Java development
ArticleID=374875
ArticleTitle=Mastering Grails: Rewiring Grails with custom URIs and codecs
publish-date=03102009