CHAPTER 6
Mapping URLs

Grails provides working URL mappings right out of the box. The default URL mapping configuration is yet one more place that the Grails framework leverages the powerful idea of convention over configuration to lessen the burden put on the application developer. Sometimes, though, you will want to deviate from the convention and define your own custom mappings. For example, you may want to create more descriptive and human-readable URLs. Grails gives you the ability to easily define these custom URL mappings.

Defining application-specific URL mappings is something that comes up all the time while building web applications. The technique for configuring URL mappings in Grails is really powerful while remaining very simple to work with. Like a lot of configuration options in a Grails application, configuring custom URL mappings involves writing a little bit of Groovy code, and that's it. In particular, no XML configuration files are involved.

Understanding the Default URL Mapping

The default URL mapping configuration in a Grails app is simple. The first part of the URL corresponds to the name of a controller, and the second, optional part of the URL corresponds to the name of an action defined in that controller. For example, the /store/index URL will map to the index action in the StoreController. Specifying the action name is optional, so if the action name is left out of the URL, then the default action for the specified controller will be executed. Default controller actions are described in detail in the "Setting the Default Action" section of Chapter 4. Finally, the last piece of the URL is another optional element that represents the value of a request parameter named id. For example, the /album/show/42 URL will map to the show action in the AlbumController with a request parameter named id that has a value of 42.

The definition of the default mapping is in grails-app/conf/UrlMappings.groovy. Listing 6-1 shows what UrlMappings.groovy looks like by default.

Listing 6-1. Default grails-app/conf/UrlMappings.groovy

class UrlMappings {
    static mappings = {
        "/$controller/$action?/$id?"{
            constraints {
              // apply constraints here
            }
}
          "500"(view:'/error')
      }
}

The key to this mapping is the string "/$controller/$action?/$id?". Notice that the $action and $id elements are both followed by a question mark. The question mark indicates an optional piece of the URL. The $controller element has no question mark, so it is a required piece of the URL. A mapping can define any number of optional elements. If a mapping does contain any optional elements, they must all appear at the end of the pattern.


Note The constraints block in the default mapping is empty. The constraints block is optional and will be discussed in the "Applying Constraints to URL Mappings" section later in this chapter. The mapping that begins with "500" will be discussed later in the "Mapping HTTP Response Codes" section.


Including Static Text in a URL Mapping

In the default mapping, each of the elements in the URL is a variable. Variable elements are prefixed with a $ sign. A URL mapping can contain static elements as well. A static element in a URL mapping is simply text that must be part of the URL in order for a particular mapping to apply. See Listing 6-2 for an example of a mapping that contains static text.

Listing 6-2. Including Static Text in a Mapping

class UrlMappings {
    static mappings = {
        "/showAlbum/$controller/$action?/$id?" {
            constraints {
                // apply constraints here
            }
        }

       // ...
    }
}

This mapping will match URLs such as /showAlbum/album/show/42 and /showAlbum/ album/list but will not match URLs such as /album/show/42 since that one does not begin with /showAlbum.

Removing the Controller and Action Names from the URL

The controller and action names do not need to be part of the URL. These special elements can be eliminated from the URL pattern and specified as properties of the mapping. As shown previously, the default mapping supports a URL such as /album/show/42, which will map to the show action in the AlbumController. An application can choose to support a URL such as /showAlbum/42 to access that same controller action. The code in Listing 6-3 includes a mapping to support this.


Listing 6-3. Specifying the Controller and Action As Properties of the Mapping

class UrlMappings {
    static mappings = {
        "/showAlbum/$id" {
            controller = 'album'
            action = 'show'
        }

        // ...
    }
}

The mapping engine in Grails provides support for an alternate syntax to express this same mapping. Which technique you choose is a matter of personal preference. Listing 6-4 shows the alternate syntax.


Listing 6-4. Specifying the Controller and Action As Parameters to the Mapping

class UrlMappings {
    static mappings = {
        "/showAlbum/$id"(controller:'album', action:'show')

        // ...
    }
}

Embedding Parameters in a Mapping

Of course, Grails supports request parameters using the standard HTTP request parameter notation. A URL such as /showArtist?artistName=Rush would work if you had a mapping like the mapping shown in Listing 6-5.

Listing 6-5. A Mapping for the /showArtist URL

class UrlMappings {
    static mappings = {
        "/showArtist"(controller:'artist', action:'show')

        // ...
    }
}

Accessing /showArtist?artistName=Rush would map to the show action in the ArtistController and a request parameter named artistName would be populated with the value Rush. Notice that the artistName parameter is not represented anywhere in the mapping. This is because our mapping applies to the /showArtist URL, and therefore any arbitrary parameters can be passed to that URL without affecting the mapping.

Although this approach works, it has its drawbacks. One drawback is the URL is just ugly, and it would continue to get uglier as more request parameters were introduced.

Grails' URL mapping engine provides a much slicker solution to support custom URLs that have request parameters embedded in the URL. Instead of /showArtist?artistName=Rush, let's support a URL such as /showArtist/Rush. The mapping in Listing 6-6 works perfectly for this.


Listing 6-6. Embedding a Request Parameter in the URL

class UrlMappings {
    static mappings = {
        "/showArtist/$artistName"(controller:'artist', action:'show')

        // ...
    }
}

With this mapping, URLs such as /showArtist/Tool and /showArtist/Cream will be mapped to the show action in the ArtistController with a request parameter named artistName, and the value of that parameter will be whatever is in the last part of the URL; in the previous examples, these were the Tool and Cream values. The action in the AlbumController would have access to the request parameter and could use the parameter however is appropriate. See Listing 6-7.


Listing 6-7. Accessing a Request Parameter in the Controller Action

class ArtistController {
    def show = {
        def artist = Artist.findByName(params.artistName)
        // do whatever is appropriate with the artist...
    }
}

A little snag that must be dealt with here is that the artist names may include characters that are not valid in a URL. One technique you might use is to URL-encode the parameters. A technique like this would support accessing a band named Led Zeppelin with a URL such as /showArtist/Led%20Zeppelin. Notice that the space in the name has been replaced with %20. Yuck! Let's make an application decision here and say that you'll encode artist names by replacing spaces with underscores. This will lead you to a friendlier-looking URL: /showArtist/ Led_Zeppelin. The URL mapping doesn't really care about the value of the parameter, so it does not need to be changed to support this. However, the controller action will need to be updated since the underscores in the query parameter must be replaced with spaces. Listing 6-8 represents an updated version of the code in Listing 6-7 to deal with this.


Listing 6-8. Decoding the Request Parameter to Replace Underscores with Spaces

class ArtistController {
    def show = {
        def artist = Artist.findByName(params.artistName.replaceAll('_', ' '))
        // do whatever is appropriate with the artist...
    }
}

Another approach to encoding and decoding an artist name is to write a custom codec. Grails dynamic codecs are covered in the "Using Dynamic Codecs" section of Chapter 14.


Note This encoding/decoding problem exists even if the request parameter is not embedded in the URL. For example, something like /showArtist?artistName=Led%20Zeppelin or /showArtist?artistName=Led_Zeppelin would be necessary to deal with the space in the parameter value.


Specifying Additional Parameters

In addition to embedding parameters in the URL, arbitrary request parameters may be specified as properties of a particular mapping that never show up in the URL. Listing 6-9 includes an example.

Listing 6-9. Specifying Additional Request Parameters

class UrlMappings {
    static mappings = {
        "/showArtist/$artistName"(controller:'artist', action:'show') {
            format = 'simple'
        }
"/showArtistDetail/$artistName"(controller:'artist', action:'show') {
            format = 'detailed'
        }

        // ...
    }
}

With this mapping in place, a request to the URL /showArtist/Pink_Floyd would map to the show action in the ArtistController, and the request would include parameters named artistName and format with the values Pink_Floyd and simple, respectively. A request to the URL /showArtistDetail/Pink_Floyd would map to the same action and controller, but the format request parameter would have a value of detailed.

Mapping to a View

Sometimes you might want a certain URL pattern to map directly to a view. This is useful when the view does not require any data to be passed in and no controller action is required. In a case like this, you can define a URL mapping that is associated with a view rather than a controller action. The syntax is the same as mapping to an action except you must specify a value for the view property instead of the action property. Listing 6-10 demonstrates how to do this.

Listing 6-10. Mapping to a View

class UrlMappings {
    static mappings = {
        "/"(view:'/welcome')

        // ...
    }
}

This mapping will handle all requests to the root of the application (/) by rendering the GSP at grails-app/views/welcome.gsp. The mapping engine also allows a mapping to specify a view that belongs to a particular controller. For example, Listing 6-11 demonstrates how to map the /find URL to grails-app/views/search/query.gsp.

Listing 6-11. Mapping to a View for a Particular Controller

class UrlMappings {
    static mappings = {
        "/find"(view:'query', controller:'search')

        // ...
    }
}

Remember that no controller action is being executed for this mapping. The controller is being specified only so the framework can locate the appropriate GSP.

Applying Constraints to URL Mappings

The URL mapping engine provides a really powerful mechanism for applying constraints to variables embedded in a URL mapping. The constraints are similar those applied to domain objects. See the "Validating Domain Classes" section in Chapter 3 for information about domain constraints. Applying constraints to variables in a URL mapping can greatly simplify the job of weeding out certain kinds of invalid data that would otherwise have to be dealt with in an imperative manner in a controller or service.

Consider a blogging application written in Grails. A typical format for a URL in a blogging system might be something like /grailsblogs/2009/01/15/new_grails_release. To support a URL like that, you might define a mapping like the one defined in Listing 6-12.

Listing 6-12. A Typical Blog-Type URL Mapping

class UrlMappings {
    static mappings = {
        "/grailsblogs/$year/$month/$day/$entry_name?" {
            controller = 'blog'
            action = 'display'
            constraints {
                // apply constraints here
            }
        }

        // ...
    }
}

With a mapping like that in place, a URL like /grailsblogs/2009/01/15/new_grails_release would map to the display action in the BlogController with request parameters named year, month, day, and entry_name and the values 2009, 01, 15, and new_grails_release, respectively.

A problem with this mapping is that not only will it match a URL such as /grailsblogs/ 2009/01/15/new_grails_release, but it will also match a URL such as /grailsblogs/grails/ rocks/big/time. In this case, the controller action would receive the value grails for the year, rocks for the month, and so on. Dealing with scenarios like this would complicate the logic in the controller. A better way to manage them is to apply constraints to the mapping that would let the framework know that grails is not a valid match for the year parameter in the mapping, for example. The constraints specified in Listing 6-13 use regular expressions to limit the year, month, and day parameters to match only those values that include the right number of digits and only digits.

Listing 6-13. Applying Constraints to Mapping Parameters

class UrlMappings {
    static mappings = {
        "/grailsblogs/$year/$month/$day/$entry_name?" {
            controller = 'blog'
            action = 'display'
            constraints {
                year matches: /[0-9]{4}/
                month matches: /[0-9]{2}/
                day matches: /[0-9]{2}/
            }
        }

        // ...
    }
}

As is the case with domain class constraints, mapping parameters may have as many constraints applied to them as necessary. All the constraints must pass in order for the mapping to apply.


Note There is a small syntactical difference between the way constraints are specified in a URL mapping and how they are specified in a domain class. In a domain class, a constraints property is defined and assigned a value that is a closure. In a URL mapping, you are calling a method named constraints and passing a closure as an argument. This is why no equals sign is needed between constraints and the closure in a URL mapping but is needed between constraints and the closure in a domain class.


Including Wildcards in a Mapping

You have seen how a mapping may contain static text as well as any number of variable parameters (optional and required), and you've seen how constraints may be applied to variable parameters. One more aid to flexibility that you can use in a mapping definition is a wildcard. Wildcards represent placeholders in a mapping pattern that may be matched by anything but do not represent information that will be passed as request parameters. Wildcards in a mapping definition are represented by an asterisk (*). Listing 6-14 includes a mapping with a wildcard in it.

Listing 6-14. Wildcards in a Mapping

class UrlMappings {
    static mappings = {
        "/images/*.jpg"(controller:'image')
// ...
    }
}

This mapping will handle any request for a file under the /images/ directory that ends with the .jpg extension. For example, this mapping will handle /images/header.jpg and /images/ footer.jpg, but this mapping will not match requests for .jpg files that may exist in some subdirectory under the /images/ directory. For example, a request for something like /images/ photos/president.jpg would not match. A double wildcard can be used to match any number of subdirectories. Listing 6-15 shows a double wildcard mapping.

Listing 6-15. Double Wildcards in a Mapping

class UrlMappings {
    static mappings = {
        "/images/**.jpg"(controller:'image')

        // ...
    }
}

This mapping will match requests for things such as /images/header.jpg and /images/ footer.jpg as well as things such as /images/photos/president.jpg.

For some situations, it may be desirable for the value that matched the wildcard to be passed to the controller as a request parameter. This is achieved by prepending a variable to the wildcard in the mapping. See Listing 6-16.

Listing 6-16. Double Wildcards with a Variable in a Mapping

class UrlMappings {
    static mappings = {
        "/images/$pathToFile**.jpg"(controller:'image')

        // ...
    }
}

In this case, the pathToFile request parameter would represent the part of the URL that matched the wildcard. For example, a request for /images/photos/president.jpg would result in the pathToFile request parameter having a value of photos/president.

Mapping to HTTP Request Methods

A URL mapping can be configured to map to different actions based on the HTTP request method.5 This can be useful when building a system that supports RESTful APIs. For example, if a GET request is made to the URL /artist/The_Beatles, then the controller may respond by generating a page that displays details about the Beatles. If a DELETE request is made to that same URL, the controller may respond by attempting to delete the Beatles and all of the band's associated data (albums and so on). An application could deal with all these requests in the same controller action by interrogating the request and reacting differently based on the HTTP request method. Listing 6-17 shows what this might look like in the ArtistController.

Listing 6-17. Inspecting the HTTP Request Method in a Controller Action

class ArtistController {
  def actionName = {
    if(request.method == "GET") {
      // handle the GET
    } else if(request.method == "PUT") {
      // handle the PUT
    } else if(request.method == "POST") {
      // handle the POST
    } else if(request.method == "DELETE") {
      // handle the DELETE
    }
    // ...
}

This is tedious code and would likely be repeated in many places in your application. A better idea is to configure a URL mapping that matches this URL and maps the request to different controller actions based on the HTTP request method. See Listing 6-18 for an example.

Listing 6-18. Mapping to HTTP Request Methods

class UrlMappings {
    static mappings = {
        "/artist/$artistName" {
            controller = 'artist'
            action = [GET: 'show',
                      PUT: 'update',
                      POST: 'save',
                      DELETE: 'delete']
        }

        // ...
    }
}

Note that the value assigned to the action property is not the name of an action but is a Map. The keys in the map correspond to the names of HTTP request methods, and the values associated with the keys represent the name of the action that should be invoked for that particular request method.

Mapping HTTP Response Codes

URL mappings may be defined for specific HTTP response codes. The default mapping includes a mapping for the 500 response code (Internal Error).6 This mapping renders the /error view for any internal error. This view is located at grails-app/views/error.gsp. This GSP renders stack information that may be useful during development and debugging. Listing 6-19 represents the default error.gsp page.

Listing 6-19. The Default grails-app/views/error.gsp Page

<body>
  <h1>Grails Runtime Exception</h1>
  <h2>Error Details</h2>
  <div class="message">
    <strong>Message:</strong> ${exception.message?.encodeAsHTML()} <br />
    <strong>Caused by:</strong> ${exception.cause?.message?.encodeAsHTML()} <br />
    <strong>Class:</strong> ${exception.className} <br />
    <strong>At Line:</strong> [${exception.lineNumber}] <br />
     <strong>Code Snippet:</strong><br />
     <div class="snippet">
       <g:each var="cs" in="${exception.codeSnippet}">
         ${cs?.encodeAsHTML()}<br />
       </g:each>
     </div>
  </div>
  <h2>Stack Trace</h2>
  <div class="stack">
    <pre>
      <g:each in="${exception.stackTraceLines}">
        ${it.encodeAsHTML()}<br/>
      </g:each>
    </pre>
  </div>
</body>

You can add your own mappings for specific response codes. For example, if you wanted to map every request for something that cannot be found to the default action in the StoreController, you could do so with the mapping shown in Listing 6-20.

Listing 6-20. Custom Mapping for All 404 Response Codes

class UrlMappings {
    static mappings = {
      "404"(controller:'store')

      // ...
    }
}

Taking Advantage of Reverse URL Mapping

You have seen how to support URLs such as /showArtist/Pink_Floyd instead of URLs such as /artist/show/42. The support you have seen so far relates to handling a request to a URL. The other end of that interaction is equally important. That is, you need a slick mechanism for generating links that takes advantage of custom URL mappings. Fortunately, that mechanism is built into Grails and is as easy to work with as the mapping mechanisms you have already seen.

The <g:link> GSP tag that is bundled with Grails is useful for generating links to certain controllers and actions. See Listing 6-21 for a common use of the link tag.

This tag will generate a link like <a href="/artist/show/42">Pink Floyd</a>. That link to /artist/show/42 is ugly. You would definitely prefer /showArtist/Pink_Floyd. The good news is that it is easy to get the link tag to generate a link like that. You just tell the link tag what controller and action you want to link to and supply all the necessary parameters that the custom mapping calls for. For example, see the custom mapping in Listing 6-22.

Listing 6-22. A Mapping for the /showArtist/ URL

class UrlMappings {
    static mappings = {
        "/showArtist/$artistName"(controller:'artist', action:'show')

      // ...
    }
}

The link tag will generate a link that takes advantage of this mapping whenever a request is made for a link to the show action in the ArtistController and the artistName parameter is supplied. In a GSP, that would look something like the code in Listing 6-23.

Defining Multiple URL Mappings Classes

When an application defines a lot of custom URL mappings, the UrlMappings class may get long enough to warrant breaking the mappings up into several mappings classes. Having several small, focused mappings classes will be easier to write and maintain than one monolithic class. To introduce new mappings classes, simply define classes under grails-app/conf/ with a name that ends with UrlMappings. The structure of those classes should be exactly the same as the default UrlMappings class. Listing 6-24 shows a custom mappings class that would contain Artist-related mappings.

Listing 6-24. A URL Mappings Class for Artist Mappings

class ArtistUrlMappings {
   static mappings = {
     "/showArtist/$artistName" (controller:'artist', action:'display')
   }
}

Testing URL Mappings

Like most aspects of your application, you are going to want to write automated tests for custom URL mappings to assert that the application does in fact respond to requests in the way you intended. Grails provides a really slick mechanism for writing those tests. The simplest way to test URL mappings is to create an integration test that extends from grails.test. GrailsUrlMappingsTestCase. The GrailsUrlMappingsTestCase class extends GroovyTestCase and provides a number of methods that can be used to test custom mappings.

Listing 6-25 shows a simple mapping to support URLs like /showArtist/Jeff_Beck. A request to a URL like that should map to the display action in the ArtistController.

Listing 6-25. A Custom URL Mapping

class UrlMappings {

  static mappings = {
    "/showArtist/$artistName" (controller:'artist', action:'display')

    // ...
  }
}

The assertForwardUrlMapping method in GrailsUrlMappingsTestCase can be used to assert that a request to a URL like /showArtist/Jeff_Beck is sent to the appropriate controller action. The code in Listing 6-26 demonstrates what this test might look like.

Listing 6-26. Unit Testing a URL Mapping

class ArtistUrlMappingsTests extends grails.test.GrailsUrlMappingsTestCase {

   void testShowArtist() {
     assertForwardUrlMapping('/showArtist/Jeff_Beck',
                            controller: 'artist', action: 'display')
   }
}

The mapping defined in Listing 6-25 includes an embedded variable, artistName. The GrailsUrlMappingsTestCase class provides a simple mechanism for asserting that mapping variables like this one are being assigned the correct value. The way to do this is to pass a closure as the last argument to the assertForwardUrlMapping method and in the closure assign values to properties with names that are consistent with the embedded variable names. See Listing 6-27 for an example. This test will assert not only that the request maps to the display action in the ArtistController but also that the artistName request parameter is being populated with the correct value.

Listing 6-27. Testing URL Mapping Variables

class ArtistUrlMappingsTests extends grails.test.GrailsUrlMappingsTestCase {

   void testShowArtist() {
     assertForwardUrlMapping('/showArtist/Jeff_Beck',
                               controller: 'artist', action: 'display') {
       artistName = 'Jeff_Beck'
     }
   }
}

Listing 6-28 demonstrates a similar approach to testing whether reverse URL mapping is behaving as expected. Note that the assert method is called assertReverseUrlMapping this time.

Listing 6-28. Testing Reverse URL Mapping

class ArtistUrlMappingsTests extends grails.test.GrailsUrlMappingsTestCase {

   void testShowArtist() {
     assertReverseUrlMapping('/showArtist/Jeff_Beck',
                             controller: 'artist', action: 'display') {
        artistName = 'Jeff_Beck'
     }
   }
}

Often it is the case that you want to test both forward and reverse URL mapping. One way to do this is to use the assertForwardUrlMapping method in addition to using the assertReverseUrlMapping method. Although that will work, it is more work than you need to do. If you use the assertUrlMapping method, GrailsUrlMappingsTestCase will assert that both forward and reverse URL mapping are working, and if either of them fail, the test will fail. See Listing 6-29 for an example.

Listing 6-29. Testing Both Forward and Reverse URL Mapping

class ArtistUrlMappingsTests extends grails.test.GrailsUrlMappingsTestCase {

   void testShowArtist() {
     assertUrlMapping('/showArtist/Jeff_Beck',
                      controller: 'artist', action: 'display') {
       artistName = 'Jeff_Beck'
      }
   }
}

The GrailsUrlMappingsTestCase class will load all the mappings defined in an application by default. If you want to take control over which mappings are loaded while the test is running, you can do so by defining a static property in your mapping test called mappings and assigning it a value that is either a class reference or a list of class references. If the value of the mappings property is a class reference, that class reference should represent the mapping class to be loaded. If the value of the mappings property is a list of class references, then all those mapping classes will be loaded. Listing 6-30 demonstrates how to take advantage of the mappings property.

Listing 6-30. Loading Specific URL Mapping Classes in a Unit Test

class ArtistUrlMappingsTests extends grails.test.GrailsUrlMappingsTestCase {

   static mappings = [UrlMappings, ArtistUrlMappings]

   void testShowArtist() {
     assertUrlMapping('/showArtist/Jeff_Beck',
                      [controller: 'artist', action: 'display']) {
       artistName = 'Jeff_Beck'
     }
   }
}

Summary

The URL mapping engine provided by Grails is very flexible. Nearly any URL pattern that you might want to map to a particular controller action can easily be configured simply by writing a small amount of Groovy code in UrlMappings.groovy. The framework provides a lot of mechanisms that enable you to spend less time configuring the framework and more time solving business problems in your application. The URL mapping engine is one more example of this. Custom URL mappings are simple to write and simple to test.



5. See http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html for definitions of all the HTTP request methods.

6. See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html for definitions of all the HTTP response codes.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset