Mastering Grails: Rewiring Grails with custom URIs and codecs

Replacing primary keys in URIs with something more descriptive

In this installment of Mastering Grails, Scott Davis shows you how to customize the standard Uniform Resource Identifier (URI) that Grails generates for Web pages. Moving from primary keys to descriptive titles in URIs gives users a more memorable and more meaningful path to the resources that they seek.

Share:

Scott Davis, Founder, ThirstyHead.com

Scott DavisScott Davis is an internationally recognized author, speaker, and software developer. He is the founder of ThirstyHead.com, a Groovy and Grails training company. His books include Groovy Recipes: Greasing the Wheels of Java, GIS for Web Developers: Adding Where to Your Application, The Google Maps API, and JBoss At Work. He writes two ongoing article series for IBM developerWorks: Mastering Grails and Practically Groovy.



10 March 2009

Also available in Chinese Russian Japanese

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 Resources). 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.

About this series

Grails is a modern Web development framework that mixes familiar Java technologies like Spring and Hibernate with contemporary practices like convention over configuration. Written in Groovy, Grails give you seamless integration with your legacy Java code while adding the flexibility and dynamism of a scripting language. After you learn Grails, you'll never look at Web development the same way again.

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.

The debate surrounding opaque URIs

Everyone agrees that a URI must uniquely identify a resource, but there's a vigorous, ongoing debate about whether it should provide additional metadata by being human-readable (see Resources). Some contend that it's dangerous to overload a URI's purpose by making it both unique and descriptive. They argue that descriptive URIs are too long or too brittle and can unnecessarily couple the resource's identifier to the underlying technology.

All of these are valid concerns, bit I respectfully disagree with the axiom of URI opacity. I feel that human-readable URIs are simply more user-friendly, and that the benefits outweigh any potential drawbacks. Clear URIs are easier to remember and easier to debug if you run into problems, and they make your Web site more self-describing and discoverable if they follow readily apparent conventions.

Grails begins the fight for transparency by exposing object names and controller methods in URIs. In this article, I continue the fight to its logical conclusion by replacing primary keys with friendlier text identifiers. But just to prove that I can see merits in both sides of the issue, I heartily endorse using Web sites like tinyurl.com (see Resources) when you need concise URIs instead of descriptive ones.

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

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 Resources). 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

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

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 Resources).

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 Resources). 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

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.

Resources

Learn

Get products and technologies

  • Grails: Download the latest Grails release.
  • Blogito: You can download the completed Blogito application.

Discuss

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Java technology on developerWorks


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