Mastering Grails: Give your Grails applications a facelift

Change your application's look and feel with CSS, templates, tag libraries, and more

In this installment of Mastering Grails, Scott Davis demonstrates how to make drastic changes to the look and feel of a Grails application using Cascading Style Sheets (CSS), templates, tag libraries (TagLibs), and more.

Welcome to the second year of Mastering Grails. As I promised in the last article of 2008, with the new year comes a new application. Goodbye Trip Planner, hello blog publishing system.

I've named the application Blogito. It's either Spanish for "little blog" or an homage to Descartes' Cogito ergo sum ("I think, therefore I am"). You can download the finished application from blogito.org. Over the next couple of articles, you will build up the core functionality step by step.

In this article, the focus is on dramatically changing the look and feel of a Grails application. Last year's Trip Planner had a Frankenstein look and feel that only a developer could love. (To be fair, I was more interested in core functionality than aesthetics.) This time around, with a few CSS tweaks here and a couple of partial templates there, you'll end up with a Web application that looks nothing like an out-of-the-box Grails application. Along the way, you'll also get a brief refresher on Grails features like scaffolding, autotimestamping, modifying the default templates, creating a custom TagLib, and adjusting key configuration files such as Bootstrap.groovy and URLMapper.groovy.

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.

But before you get started, you need to install Grails 1.1, still in beta as of this writing.

Installing Grails 1.1

Grails runs best on Java 1.5 or 1.6. Type java -version at a command prompt to confirm that you are up to date.

Once you have Java 1.5 or 1.6 in place, the steps for installing Grails are simple as always:

  1. Download grails.zip from the Grails site.
  2. Unzip grails.zip.
  3. Create a GRAILS_HOME environment variable.
  4. Add GRAILS_HOME/bin to the PATH.

If you have an application written in a previous version of Grails, you can type grails upgrade to migrate it to the current version. But what if you want to work with multiple versions of Grails?

If you are on a UNIX®-esque OS (UNIX, Linux®, or OS X), you can easily flip among Grails versions by pointing the $GRAILS_HOME environment variable to a symlink. On my system, GRAILS_HOME points to /opt/grails. Once that is in place, a quick ln -s moves me from release to release, as shown in Listing 1:

Listing 1. Creating a symlink for $GRAILS_HOME on UNIX, Linux, or Mac OS X
$ ln -s /opt/grails-1.1-beta1 grails

$ ls -l | grep "grails"
lrwxr-xr-x   1 sdavis  admin        17 Dec  5 11:12 grails -> grails-1.1-beta1/
drwxr-xr-x  14 sdavis  admin       476 Nov 10  2006 grails-0.3.1
drwxr-xr-x  16 sdavis  admin       544 Feb  9  2007 grails-0.4.1
drwxr-xr-x  17 sdavis  admin       578 Apr  6  2007 grails-0.4.2
drwxr-xr-x  17 sdavis  admin       578 Jun 15  2007 grails-0.5
drwxr-xr-x  19 sdavis  admin       646 Jul 30  2007 grails-0.5.6
drwxr-xr-x  18 sdavis  admin       612 Sep 18  2007 grails-0.6
drwxr-xr-x  19 sdavis  admin       646 Feb 19  2008 grails-1.0
drwxr-xr-x  18 sdavis  admin       612 Apr  5  2008 grails-1.0.2
drwxr-xr-x  18 sdavis  admin       612 Oct  9 21:46 grails-1.0.3
drwxr-xr-x  18 sdavis  admin       612 Nov 24 20:43 grails-1.0.4
drwxr-xr-x  18 sdavis  admin       612 Dec  5 11:13 grails-1.1-beta1

On a Windows® system, your best bet is to change the %GRAILS_HOME% variable directly. Don't forget to relaunch any existing command prompts after you make the change.

Type grails -version to confirm that you are using the latest release and that your GRAILS_HOME variable is being set correctly. Your output should look like Listing 2:

Listing 2. Output from grails -version
$ grails -version
Welcome to Grails 1.1-beta2 - http://grails.org/
Licensed under Apache Standard License 2.0
Grails home is set to: /opt/grails

Now that Grails 1.1 is installed, you are ready to create a new application.


Creating the application

Type grails create-app blogito to scaffold out the initial directory structure. Change to the new blogito directory and type grails create-domain-class Entry to create the class that will represent the blog entries. Look in grails-app/domain for Entry.groovy and add the code in Listing 3:

Listing 3. Creating the Entry class
class Entry {
  static constraints = {
    title()
    summary(maxSize:1000)
    dateCreated()
    lastUpdated()
  }
  
  String title
  String summary
  Date dateCreated
  Date lastUpdated
}

Each Entry has a title and summary field. Setting the maxSize constraint to 1,000 characters causes the dynamically scaffolded HTML forms to provide a text area for the summary field instead of a simple text field.

Next time: Adding content to a blog entry

In a later article, you'll add a body field to hold the actual contents of the blog entry. I'm ignoring the body field for now because in order to implement it fully, you need to understand how Grails handles user authentication and file uploads. Blogito allows end users to upload a variety of data for the payload — HTML, images, even MP3s for podcasting.

Remember that dateCreated and lastUpdated are magic field names in Grails. These timestamp fields are perfect for a blog application — they'll allow you to keep the latest Entry at the top of the list.

With the domain class in place, the next step is to create a controller. Type grails create-controller Entry. Add the code in Listing 4 to grails-app/controllers/EntryController.groovy:

Listing 4. Creating the EntryController
class EntryController {
    def scaffold = Entry
}

The deceptively simple def scaffold = Entry line instructs Grails to scaffold out the rest of the support for the Entry class. As you'll see in just a moment, you'll have an entry table with a column for each field in the Entry class (plus an ID field for the primary key and a version field for optimistic locking). You'll also have a full set of Groovy Server Pages (GSPs) that provide the mundane but all-important Create/Retrieve/Update/Delete (CRUD) capabilities.

Type grails run-app and visit http://localhost:8080/blogito in a Web browser. Click on EntryController, then New Entry. The good news is that all of the Entry fields show up in the create form (see Figure 1). That's also the bad news — those timestamp fields shouldn't be available for the user to mess around with. You need to adjust the default templates to fix this issue.

Figure 1. Editable timestamp fields in the Create Entry form
Editable timestamp fields in the Create Entry form

Adjusting the default templates

You could type grails generate-views Entry and manually delete the dateCreated and lastUpdated fields from the GSP files, but that cures the symptom instead of the underlying disease. Chances are you won't ever want those fields to show up in the create and edit forms. Your best bet is to change the templates behind def scaffold.

Type grails install-templates. Look in src/templates/scaffolding for create.gsp and edit.gsp. Add dateCreated and lastUpdated to excludedProps in each file, as shown in Listing 5:

Listing 5. Excluding the timestamp fields in the list.gsp and show.gsp templates
excludedProps = ['version',
                 'id',
                 'dateCreated',
                 'lastUpdated',
                 Events.ONLOAD_EVENT,
                 Events.BEFORE_DELETE_EVENT,
                 Events.BEFORE_INSERT_EVENT,
                 Events.BEFORE_UPDATE_EVENT]

Restart Grails and verify that the timestamp fields no longer appear (see Figure 2):

Figure 2. A form that excludes the timestamp fields
A form that excludes the timestamp fields

Changing the sort order

As you add new entries, you'll quickly see that tables are sorted by ID by default. Blogs typically sort their entries in reverse chronological order — newest first. In previous versions of Grails, changing the default sort order meant hand-editing the list closure in EntryController.groovy. Adding the two new sort lines below the existing max line isn't especially onerous (see Listing 6). The problem is this code is no longer being dynamically scaffolded behind the scenes. (You can look in src/templates/scaffolding/Controller.groovy or type grails generate-controller Entry to see the default underlying implementation.)

Listing 6. Sorting in Grails 1.0.x
def list = {
    if(!params.max) params.max = 10
    if(!params.sort) params.sort = "lastUpdated"
    if(!params.order) params.order = "desc"
    [ entryList: Entry.list( params ) ]
}

Grails 1.1 adds a simple but incredibly useful feature to the static mapping block — sort. Add the mapping block in Listing 7 to Entry.groovy. By handling sorting in the domain class, you can continue to def scaffold your controllers.

Listing 7. Adding sort to the static mapping block
class Entry {
  static constraints = {
    title()
    summary(maxSize:1000)
    dateCreated()
    lastUpdated()
  }
  
  static mapping = {
    sort "lastUpdated":"desc"
  }  
    
  String title
  String summary
  Date dateCreated
  Date lastUpdated
}

Restart Grails and verify that edited entries do indeed float to the top of the list, as shown in Figure 3:

Figure 3. Verifying the new sort order
Verifying the new sort order

Creating dummy records in development mode

Have you noticed that you lose your entries each time you restart Grails? Remember this is a feature, not a bug. The entry table is created every time you start Grails and dropped every time you shut down. Open grails-app/conf/DataSource.groovy to verify this. The db-create value for development mode is clearly set to create-drop.

You could change the value to update, but that isn't ideal either. Early in the development process, the schema is usually pretty fluid — you are constantly adding and removing fields, modifying the constraints, and so on. Until things settle down, I find it best to leave db-create set to create-drop.

To ease the tediousness of constantly retyping sample data in development mode, add a bit of logic to grails-app/conf/BootStrap.groovy. The code in Listing 8 inserts new records each time Grails starts up:

Listing 8. Adding dummy records in development mode
import grails.util.GrailsUtil

class BootStrap {

  def init = { servletContext ->
    switch(GrailsUtil.environment){
      case "development":
        new Entry(
          title:"Grails 1.1 beta is out", summary:"Check out the new features").save()
        new Entry(
          title:"Just Released - Groovy 1.6 beta 2", summary:"It is looking good.").save()
      break

      case "production":
      break
    }

  }
  def destroy = {
  }
}

Restart Grails once again. This time, you should be greeted with existing records in the entry table, as shown in Figure 4:

Figure 4. Dummy records that appear on boot
Dummy records that appear on boot

Beautifying the list

The default HTML table in the list view is fine for starters, but it clearly isn't a long-term solution for Blogito. Typically blog pages display the date, title, and summary fields vertically instead of horizontally, one entry at a time.

To make this change, type grails generate-views Entry. The GSP files that were previously being dynamically scaffolded should now appear in grails-app/views/entry. Open list.gsp in a text editor. Change the title in the header from Entry List to Blogito. Delete the <h1> and <g:if> blocks, and replace the existing <div class="list"> with the code in Listing 9:

Listing 9. Changing the list.gsp view
<div class="list">
 <g:each in="${entryInstanceList}" status="i" var="entryInstance">
  <div class="entry">
   <span class="entry-date">${entryInstance.lastUpdated}</span>
   <h2><g:link action="show" id="${entryInstance.id}">${entryInstance.title}</g:link></h2>
   <p>${entryInstance.summary}</p>                
  </div>  
 </g:each>
</div>

Notice that the code is dramatically simplified. The <fieldValue> tags can be removed — they help bind domain class fields to HTML form fields, but they don't add any real value here. Each Entry is wrapped in a named <div>, and the lastUpdated field is wrapped in a named <span>. The class attributes give you a hook for the CSS formatting you'll do in just a moment. The title and summary fields are wrapped in plain old HTML heading and paragraph tags.

CSS 101: <div> vs. <span>

Some programmers' eyes glass over when you begin talking about CSS and other technologies that are usually more associated with graphic design than with software engineering. I'll be the first to admit that when CSS gets complicated, it can be very complicated. But the counterargument is true as well: When CSS is simple, it's very simple.

HTML tags fall into two broad categories: block and inline. Block tags like <h1>, <p>, and <div> are used to surround a large, coarse-grained chunk of content. The browser usually throws an implicit new line at the end of each block element. <h1> and <p> have a predefined look and feel. You generally surround a block of information in a <div> so that you can name it and apply a custom style to it.

Inline elements like <a>, <strong>, and <span> are generally used to surround a word or two as opposed to an entire paragraph. Inline elements don't get an implicit new line added at the end. And just like block elements, inline elements like <strong> and <em> have default formatting associated with them, whereas <span> forces to you apply the formatting via CSS.

The way you name your <span> and <div> elements can also be confusing to the uninitiated. The class attribute is meant to be reused across multiple elements on the page. You create CSS classes such as entry and entry-date so that all elements with the same class will appear the same. These show up in the style sheet with a leading dot: .entry and .entry-date, for example.

You might also come across elements with an id attribute. ids must be unique within the HTML document. Later in this article, you'll create a <div id="header">. This means that there can only be one header element per page. An id shows up in the style sheet with a leading hash, as in #header.

For a quick refresher on CSS basics, see Resources.

Refresh the list view in your browser (see Figure 5). At this point, it's hard to call this an improvement. But with a few new CSS instructions, things should be markedly better looking.

Figure 5. The new list without CSS
The new list without CSS

Add the CSS in Listing 10 to the bottom of web-app/css/main.css:

Listing 10. CSS customizations for the list.gsp view
/* Blogito customizations */
.entry {
  padding-bottom: 2em;
}

.entry-date {
  color: #999;
}

Refresh your Web browser again and things should look a bit more stylish (see Figure 6). You're not done with the CSS yet, but you are off to a good start.

Figure 6. The new list with CSS
The new list with CSS

Creating a Date TagLib

CSS 102: em vs. px

As you scan main.css, you'll probably notice that many of the font sizes are expressed in pixels. There's nothing technically wrong with using a fixed size (it's actually quite common), but it can hinder usability. Users with visual impairments and übergeeks with insanely large monitors (or microscopically tiny resolutions) are all cursing your fixed font sizes under their breath. What looks good on the webmaster's monitor rarely looks as good on the wide variety of displays out in the wild — from cinema-sized screens down to iPhones and everything in between.

Pixel measurements in CSS are not universally bad — they are perfectly appropriate for fixed-size elements like images. But generally speaking, relative units of measure (like the em) are better suited for font sizes. 1em is equal to 100 percent of the default font size set by the browser or the surrounding parent element. 2em is twice the font size, and so on.

See Resources for more information.

It's time to take a stab at making the lastUpdated date look a little more user-friendly. The best place to put reusable snippets of code is in a custom TagLib. Type grails create-tag-lib Date. Add the code in Listing 11 to grails-app/taglib/DateTagLib.groovy:

Listing 11. Code for DateTagLib
import java.text.SimpleDateFormat

class DateTagLib {
  def longDate = {attrs, body ->
    //parse the incoming date
    def b = attrs.body ?: body()
    def d = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").parse(b)
        
    //if no format attribute is supplied, use this
    def pattern = attrs["format"] ?: "EEEE, MMM d, yyyy"
    out << new SimpleDateFormat(pattern).format(d)
  }
}

Now wrap the lastUpdated field in grails-app/views/entry/list.gsp in your newly created <g:longDate> tag, as shown in Listing 12:

Listing 12. Using the <g:longDate> tag in list.gsp
<div class="entry">
  <span class="entry-date"><g:longDate>${entryInstance.lastUpdated}</g:longDate></span>
  <h2>${entryInstance.title}</h2>                  
  <p>${entryInstance.summary}</p>                
</div>

Restart Grails and refresh your Web browser. You should see the reformatted dates appear, as in Figure 7:

Figure 7. Newly formatted dates using the custom <g:longDate> tag
Newly formatted dates using the custom <g:longDate> tag

Creating a partial template

That layout is looking pretty good. I'd like to reuse it for show.gsp as well. Create _entry.gsp in grails-app/views/entry and add the code in Listing 13. (Of course, you can simply cut and paste it from list.gsp.)

Listing 13. The code for _entry.gsp
<div class="entry">
 <span class="entry-date"><g:longDate>${entryInstance.lastUpdated}</g:longDate></span>
 <h2><g:link action="show" id="${entryInstance.id}">${entryInstance.title}</g:link></h2>
 <p>${entryInstance.summary}</p>
</div>

To use your newly created partial template, adjust list.gsp as shown in Listing 14:

Listing 14. Using the _entry.gsp partial template in list.gsp
<div class="list">
  <g:each in="${entryInstanceList}" status="i" var="entryInstance">
    <g:render template="entry" bean="${entryInstance}" var="entryInstance" />
  </g:each>
</div>

Now you can reuse the partial template in show.gsp as well, as in Listing 15:

Listing 15. Using the _entry.gsp partial template in show.gsp
<div class="body">
  <g:render template="entry" bean="${entryInstance}" var="entryInstance" />
  <div class="buttons">
    <!-- snip -->
  </div>
</div>

Refresh the list view in your browser. It should look exactly the way it did before. Now click on an entry's title to verify that it works for the show view as well.


Customizing the header

Things are beginning to pull together nicely. Now it's time to replace the Grails branding with your own.

I don't see the Grails logo referenced anywhere in list.gsp or show.gsp. Remember that Grails uses SiteMesh to pull the various parts of the finished page together. Look in grails-app/views/layouts/main.gsp and you'll see where the grails_logo.jpg file gets included.

Create another partial template named _header.gsp in grails-app/views/layouts. Add the code in Listing 16. Notice that Blogito is a hyperlink back to the main page.

Listing 16. Code for the _header.gsp partial template
<div id="header">
  <p><g:link class="header-main" controller="entry">Blogito</g:link></p>
  <p class="header-sub">A tiny little blog</p>
</div>

Now edit main.gsp as shown in Listing 17 to include the _header.gsp file:

Listing 17. Main.gsp using the new _header.gsp partial template
<body>
  <div id="spinner" class="spinner" style="display:none;">
    <img src="${createLinkTo(dir:'images',file:'spinner.gif')}" alt="Spinner" />
  </div> 
  <g:render template="/layouts/header"/>        
  <g:layoutBody />  
</body>

CSS 103: padding vs. margin

The CSS box model used to give block elements some breathing room can be a bit confusing at first. In a nutshell, padding increases the spacing inside the block, whereas margin increases spacing outside the block.

Listing 18's header uses padding to add space between the text and the edge of the blue box. It uses margin to add space outside the blue box between the header <div> and the nav <div>.

You can set consistent spacing on all four sides of a block with padding: 2em; or margin: 2em;. To set the spacing on a specific side of the block, you can reference it directly using margin-top, margin-right, margin-bottom, or margin-left. When you want to set a different padding for each side in a single line (as in Listing 18) the mnemonic TRBL — Top, Right, Bottom, Left — will help you remember the proper order. You should have no TRBL (trouble) remembering the order of the sides.

See Resources for more on the CSS box model.

Finally, add a bit more to web-app/css/main.css, as shown in Listing 18:

Listing 18. CSS formatting for the _header.gsp partial template
#header {
  background: #67c;
  padding: 2em 1em 2em 1em;
  margin-bottom: 1em;
}

a.header-main:link, a.header-main:visited {
  color: #fff;
  font-size: 3em;
  font-weight: bold;
}

.header-sub {
  color: #fff;
  font-size: 1.25em;
  font-style: italic;
}

Refresh your browser to see the changes (see Figure 8). Click on an entry's title, and then click on Blogito in the header to navigate back to the main page.

Figure 8. Showing off your new header
Showing off your new header

Hiding the navigation bar unless you are logged in

You need to deal with one more tell-tale sign that this is a Grails application: the navigation bar. Although you won't implement authentication until the next article, you can turn off the navigation bar for unauthenticated users right now by wrapping the <div> in a simple <g:if> test. This test is looking for a user variable stored in session scope.

Modify both list.gsp and show.gsp as shown in Listing 19:

Listing 19. Hiding the navigation bar unless you are logged in
<g:if test="${session.user}">
  <div class="nav">
      <span class="menuButton">
         <a class="home" href="${createLinkTo(dir:'')}">Home</a>
      </span>
      <span class="menuButton">
         <g:link class="create" action="create">New Entry</g:link>
      </span>
  </div>
</g:if>

In show.gsp, add the same test around the buttons <div>. (The last thing you want is unauthenticated users editing or deleting your blog entries, right?)

Finally, make one more aesthetic tweak to list.gsp. Move the paginateButtons <div> outside of the body <div>, as shown in Listing 20. This allows the bar to stretch across the entire screen, adding a nice visual anchor to the bottom of the screen.

Listing 20. Moving the paginateButtons <div> outside the body <div> for aesthetics
<html>
 <head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
  <meta name="layout" content="main" />
  <title>Blogito</title>
 </head>
 <body>
  <g:if test="${session.user}">
   <div class="nav">
    <span class="menuButton">
      <a class="home" href="${createLinkTo(dir:'')}">Home</a>
    </span>
    <span class="menuButton">
      <g:link class="create" action="create">New Entry</g:link>
    </span>
   </div>
  </g:if>
   <div class="body">
    <div class="list">
     <g:each in="${entryInstanceList}" status="i" var="entryInstance">
      <g:render template="entry" bean="${entryInstance}" var="entryInstance" />
     </g:each>
    </div>
   </div>    
   <div class="paginateButtons">
    <g:paginate total="${Entry.count()}" />
   </div>
 </body>
</html>

Add one more bit of CSS, shown in Listing 21, to ensure that the paginateButtons <div> shows up below the body <div> instead of next to it:

Listing 21. CSS to ensure that the paginateButtons <div> appears at the bottom of the screen
.paginateButtons{
  clear: left;
}

Refresh your browser for the last time. Your screen should look like Figure 9:

Figure 9. Hiding the navigation bar
Hiding the navigation bar

Setting the home page

Now that everything is in place, you should make EntryController the default home page. You accomplish this by adding a mapping that redirects / (the trailing slash on the URL http://localhost:9090/blogito/) to the EntryController. Edit grails-app/conf/UrlMappings.groovy to match Listing 22:

Listing 22. Making EntryController the default home page
class UrlMappings {
    static mappings = {
      "/$controller/$action?/$id?"{
            constraints {
                   // apply constraints here
              }
        }
        "/"(controller:"entry")
        "500"(view:'/error')
   }
}

Conclusion

This article's goal was to show you how to give your Grails application a facelift. With a few lines of CSS, you can change colors, fonts, and the spacing around block elements. Through partial templates and TagLibs, you can create some reusable snippets of code. In the end, you have all the benefits of the Grails framework and an application that has its very own look and feel.

In the next installment, you'll continue to flesh out the Blogito application. You'll add a User domain class so that multiple people can add blog entries. You'll also explore Grails codecs and play around a bit more with custom URL mappings. Don't forget that you can download the completed application at http://blogito.org. Until then, have fun mastering Grails.

Resources

Learn

  • Mastering Grails: Read more in this series to gain a further understanding of Grails and all you can do with it.
  • Grails: Visit the Grails Web site.
  • Grails Framework Reference Documentation: The Grails bible.
  • "Design Web pages with class" (Molly Holzschlag, developerWorks, September 1999): Find out two ways to use classes with stylesheets to make quick work of designing (or redesigning) your HTML documents.
  • Care With Font Size: Check out this tip in the W3C Quality Assurance series.
  • Box Model: Learn more about padding and margin in CSS.
  • Groovy Recipes (Scott Davis, Pragmatic Programmers, 2007): 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.
  • Groovy: Learn more about Groovy at the project Web site.
  • AboutGroovy.com: Keep up with the latest Groovy news and article links.
  • Technology bookstore: Browse for books on these and other technical topics.
  • developerWorks Java technology zone: Find hundreds of articles about every aspect of Java programming.

Get products and technologies

  • Grails: Download the latest Grails release.

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, Web development, Open source
ArticleID=365451
ArticleTitle=Mastering Grails: Give your Grails applications a facelift
publish-date=01202009