Grails Kick Start

Modified: 06/13/2008, Created: 03/06/2008

A guide to get up and running an intermediate? Grails web app. The following concepts will be touched upon:

  • Create a web app using Grails
  • Install a plugin (Ivy)
  • Create Domain Class
  • Create CRUD web pages for the Domain Class
  • Create initial data for "development" environment (using BootStrap.groovy)
  • How to change the port number that the web app runs upon
  • How to get a war file of your Gails app
  • Providing a "Get" REST Web Service URL
    • Provide REST Web Service results data in XML, JSON or HTML
  • Adding a URL Mapping
  • Modify the default Layout of web pages using Sitemesh
  • Adding YUI CSS
  • Adding YUI Widget(s)
    • Add a Horizontal JavaScript Powered Menu using YUI
  • Adding Custom CSS
  • Creating basic login and logout functionality
    • Use a filter to ensure that secure pages (actions) are only available to logged in users
  • Create Page Controllers
  • Creating GSP pages and use GSP Tags
  • Logging using the default log property
  • Running Tests
  • Adding a Service
  • Injecting a Controller with a Service
  • Adding a file upload form
  • Ajax
    • Add Ajax (YUI) to the upload form
  • Creating a Template
  • Upgrade the Project to a New Version of Grails
  • Export the database schema to a file for inspection
  • Speed up the Grails Web App with pooled

Also see the official Grails Quick Start .

Introduction

The spirit of Grails is to do convention over configuration . Behind the Covers, Grails is using Java, Groovy, Spring, Hibernate, JUnit, Ant, Log4J and Sitemesh. The term CRUD will mean create, read, update, delete. Model View Controller is an accepted web framework pattern. This presentation will show writing Models, Controllers and Views. Closures are like methods that are objects. Closures are provided via the Groovy language. Controllers have actions that are implemented as closures. This presentation will not focus on GORM (Grails Object Relational Mapping) or creating Domain relationships.

Here is the presentation source code in zip format projecttracker-20080617.zip .

Install Grails (on Windows OS)

This section will take on a Windows OS slant. I recommend installing the Grails binary vs. using the Grails Windows Installer. I don't recommend using the Grails Windows Installer because of the issues raised in this blog article, Some notes about the Windows installer for Grails

  • Prerequisites: Java JDK is installed properly.
  • Download a binary version of Grails from http://www.grails.org/
  • Extract the file someplace like C:\dev\tools
  • Set GRAILS_HOME to the extracted Grails folder
  • Add GRAILS_HOME\bin to your PATH
    • Sample: Set up a batch file (or run the following commands in a DOS prompt)
      REM// set up Grails
      
      set DEV_TOOLS_DIR=C:\dev\tools
      
      set GRAILS_HOME=%DEV_TOOLS_DIR%\grails-1.0.2
      set PATH=%GRAILS_HOME%\bin;%PATH%
      

To Create and Run a Grails Web App

  • Change directory into where you want to create the new Grails application.
  • To create the initial version of our new web application, run the following commands:
    grails help
    grails create-app projecttracker
    cd projecttracker
    

    This create-app command created the web application's directory layout and many files.

    Note that the create-app command set the application version to "0.1" in the file application.properties. The application name is also stored in the same application.properties file.

    #Do not edit app.grails.* properties, they may change automatically. DO NOT put application configuration in here, it is not the right place!
    #Thu Apr 17 17:56:36 CDT 2008
    app.version=0.1
    app.servlet.version=2.4
    app.grails.version=1.0.2
    app.name=projecttracker
    
  • Once the web application is created, you can optionally import the project into the Eclipse IDE.
    • Select menu File > Import > Existing project into Workspace
    • Select "Select root directory: "; "Browse..." to the Grails project root directory; Click the "OK" button
    • Click the "Finish" button

The Grails Standard Project Directory Layout

The Grails developers have decided to not reuse Maven's Standard Directory Layout. See the blog article Why Grails doesn't use Maven for reasons. Lets get familiar with Grails Standard Project Directory Layout. Here are the highlights.

grails-app - This is where most of the Grails specific source code will go

grails-app/conf - This is where configuration souce code will go

grails-app/controllers - This is where controller source code will go

grails-app/domain - This is where your persisted data domain source code will go

grails-app/views - This is where view and view template source code will go

grails-app/services - This is where the Spring POGO service source code will go

src/groovy - This is where extra Groovy source code will go

src/java - This is where extra Java source code will go

templates - This is only used if you decide to override the templates that Grails will use to generate code and configuration

plugins - This is used if you install plugins

lib - This is where extra jar library files go

test/integration - This is where integration test source code will go

test/unit - This is where unit test source code will go

web-app - This is the root of the web site files go

web-app/css - This is where the web site css files go

web-app/js - This is where the web site JavaScript files go

web-app/WEB-INF - This is where controlled Java web files go

web-app/WEB-INF/classes - This where the compiled source code files are placed

Plugins

  • As a learning exercise, install the Ivy Plugin
    • For more information on the Ivy plugin, see Ivy Integration - Using Ivy for Dependency Resolution at http://grails.org/Ivy+Integration
    • Frist, list all plugins available to your Grails project.
      grails list-plugins
      

      and

      grails plugin-info ivy
      
    • To install Ivy, run the following:
      grails install-plugin ivy
      
    • The previous command created the following directories:
      plugins/ivy-0.1
      

      in the project directory and

      C:\Documents and Settings\yourusername\.grails\1.0.2\plugins\ivy
      
    • It also created the files
      ivy.xml
      ivyconf.xml
      

      and possible placed the jar file ivy-1.4.1.jar in GRAILS_HOME/lib

Creating Domain Model Classes

  • To create a domain class to be persisted in a database, run the following commands:
    grails create-domain-class project
    

    The above command created the files:

    grails-app/domain/Project.groovy
    test/integration/ProjectTests.groovy
    
  • Edit file grails-app/domain/Project.groovy

    from

    class Project {
    
    }
    

    to be

class Project
{

static constraints = {

displayOrder(nullable: true)

name(blank: true, nullable: true)
status(blank: false, inList: ['notstarted', 'inprogress', 'onhold', 'abandoned', 'completed'] )
referenceNbr(blank: true, nullable: true)
description(blank: true, nullable: true)

percentDevComplete(nullable: true)
briefStatusDescription(blank: true, nullable: true)
workersAssigned(blank: true, nullable: true)

started(nullable: true)
completed(nullable: true)
onHold(nullable: true)

startDate(nullable: true)
estimatedDueDate(nullable: true)
completedDate(nullable: true)

lastModifiedTs(nullable: true)
lastModifiedBy(nullable: true)
}


/*
lower number is considered more important
i.e. 1, then 10, then 22, then 100, etc.
*/
Integer displayOrder

String name
// one word status
String status
String referenceNbr
String description

BigDecimal percentDevComplete = new BigDecimal("0.00")

// sentence status
String briefStatusDescription

// who is working on the project as a string
String workersAssigned

Boolean started
Boolean completed
Boolean onHold

Date startDate
Date estimatedDueDate
Date completedDate

Date lastModifiedTs = new Date()
String lastModifiedBy
}

The "constraints" are used for:

  • Influencing the database column attributes that are created in the database
  • Ordering the listing of fields in Create Read Update Delete (CRUD) web pages
  • Validation for Update and Create web forms
  • Validation when GORM (Grails Object Relational Mapping) does a save
  • To start up the Grails Web App, run the following command:
    grails dev run-app
    

    Grails generates the WEB-INF/web.xml file at load time. The web.xml file probably has contents similar to this code listing .

  • Point a web browser to http://localhost:8080/projecttracker

    You will see a page similar to the following

    Fig. 1: Grails Web App

    So far, we do not have any controllers. If we did they would be listed on the Web Page

  • To shutdown the web app, type Ctrl-C in the Grails app terminal window

Controllers and Scaffolding

  • Grail's Convention for URL Mapping:
    • http://localhost:8080/webappname/CONTROLLER/ACTION/ID

      if an Id is involved

    • http://localhost:8080/webappname/CONTROLLER/ACTION

      if an Id is not involved

  • If we wanted to generate a basic CRUD controller and views, we could run the following commands:
    grails generate-all project
    

    However, don't do that; because, I want to show you a different way.

    With one line of code, we can use Grails dynamic scaffolding which gives you a basic Create/Retrieve/Update/Delete (CRUD) Web interface.

  • Let's create a controller with a package name and dynamic scaffolding. To do this run the following commands:
    grails create-controller dbmaint.ProjectDb
    

    This created the following files:

    grails-app/controllers/dbmaint/ProjectDbController.groovy
    test/integration/dbmaint/ProjectDbControllerTests.groovy
    

    and the directory

    grails-app/views/projectDb
    
  • We will now add the dynamic scaffolding. Modify the ProjectDbController.groovy from
    package dbmaint
    
    class ProjectDbController {
    
        def index = { }
    }
    

    to

    package dbmaint
    
    class ProjectDbController
    {
    def scaffold = Project
    }
    
  • To start up the Grails Web App, run the following command:
    grails dev run-app
    
  • Point a web browser to http://localhost:8080/projecttracker

    You will see a page similar to the following

    Fig. 2: Grails Web App

    Now, we see the controller that we created listed.

  • Click on the controller name and you will see this web page.Fig. 3: Grails Web App
  • You can optionally add projects via the web page formsFig. 4: Grails Web App Fig. 5: Grails Web App Fig. 6: Grails Web App
  • As you create data using the web pages, the data will be saved in an in-memory HSQLDB database. The data will go away when the Grails web app is stopped.
  • To shutdown the web app, type Ctrl-C in the Grails app terminal window
  • In order to support User login and logout, create a User2 domain class to be persisted in a database, run the following commands: (I use the phrase "User2" to avoid the name "User" because MS SQLServer has a problem with tables named "user". You will also come across a similar problem if you use the domain class name "Order". Read the blog article Silly GORM tricks, part III: SQL keywords as attributes for more information. )
    grails create-domain-class user2
    

    The above command created the files:

    grails-app/domain/User2.groovy
    test/integration/User2Tests.groovy
    
  • Edit file grails-app/domain/User2.groovy

    from

    class User2 {
    
    }
    

    to be

    //---------------------------------------------------------
    //---------------------------------------------------------
    class User2
    {
    
    static constraints = {
    username(maxLength:60, blank:false, unique:true, nullable:false)
    email(email:true, maxLength:60, blank:true, nullable:true)
    // blank password is ok!
    password(maxLength:60, blank:true, nullable:false)
    enabled(nullable:true)
    }
    
    String username
    String password
    String email
    Boolean enabled = true
    
    //---------------------------------------------------------
    String toString()
    {
    String strTemp = "${this.class.name}: username: ${username}";
    
    return strTemp;
    }
    
    }
    
  • Let's create a controller with a package name and dynamic scaffolding to handle CRUD for the domain class User2. To do this run the following commands:
    grails create-controller dbmaint.UserDb
    

    This created the following files:

    grails-app/controllers/dbmaint/UserDbController.groovy
    test/integration/dbmaint/UserDbControllerTests.groovy
    

    and the directory

    grails-app/views/userDb
    
  • Modify the UserDbController.groovy from
    package dbmaint
    
    class UserDbController {
    
        def index = { }
    }
    

    to

    package dbmaint
    
    class UserDbController
    {
    def scaffold = User2
    }
    

Database Settings

The database Settings are located in the file grails-app/conf/DataSource.groovy

The default database is HSQLDB .

For the development environment, the database is an in-memory HSQLDB database.

If desired, you can change what database is used and you can have different settings per environment.

Load Sample Data at Startup

  • It can be tedious to always start with no data in your web app when running in the dev environment.

    To load sample data in the database at startup you can add save commands in the file grails-app/conf/BootStrap.groovy

    Note: use BootStrap.groovy in conjunction with the dbCreate property in DataSource.groovy.

    Modify the grails-app/conf/BootStrap.groovy file from

    class BootStrap {
    
         def init = { servletContext ->
         }
         def destroy = {
         }
    } 
    

    to

    import grails.util.GrailsUtil
    
    //---------------------------------------------------------
    //---------------------------------------------------------
    class BootStrap
    {
    
    def init = { servletContext ->
    
    
    //println "**** BootStrap, basedir: ${basedir}"
    println "**** BootStrap, GrailsUtil.environment: ${GrailsUtil.environment}"
    
    
    switch (GrailsUtil.environment)
            {
            case "development":
                    println "**** detected development"
               configureForDevelopment()
                    break
            case "test":
                    println "**** detected test"
               //configureForTest()
               configureForDevelopment()
                    break
            case "production":
                    println "**** detected production"
               configureForProduction()
                    break 
            }
    
    
    }
    
    def destroy = {
    }
    
    //---------------------------------------------------------
    /**
    Tasks to do when Grails is running in dev environment.
    
    */
    void configureForDevelopment()
    {
    println "configureForDevelopment() called"
    
    def dataItem = null
    
    println "*** BootStrap, at add projects"
    
    dataItem = new Project(
            displayOrder: 10,
            
            name: "Project Tracker",
            status: 'inprogress',
            referenceNbr: 100,
            description: "A web app to track development projects",
            
            percentDevComplete: 64.2,
            briefStatusDescription: "going good",
            workersAssigned: "ME",
            
            started: true,
            completed: false,
            onHold: false,
            
            startDate: new Date(),
            estimatedDueDate: null,
            completedDate: null,
            )
    
    println "attempt to save project #1"
    if( !dataItem.save() )
            {
            dataItem.errors.each
                    {
                    println "error: " + it
                    }
            }
    
    dataItem = new Project(
            displayOrder: 20,
            
            name: "To Do List Manager",
            status: 'notstarted',
            referenceNbr: 200,
            description: "A web app to manage to do lists",
            
            percentDevComplete: 0.0,
            briefStatusDescription: "waiting for a round \"TUIT\"",
            workersAssigned: null,
            
            started: false,
            completed: false,
            onHold: false,
            
            startDate: null,
            estimatedDueDate: null,
            completedDate: null,
            )
    
    println "attempt to save project #2"
    if( !dataItem.save() )
            {
            dataItem.errors.each
                    {
                    println "error: " + it
                    }
            }
    
    dataItem = new Project(
            displayOrder: 30,
            
            name: "Person Time Worked Tracker",
            status: 'notstarted',
            referenceNbr: 300,
            description: "A web app to track time worked on tasks",
            
            percentDevComplete: 110.0,
            briefStatusDescription: "DONE",
            workersAssigned: "ME, Stan, Brenda",
            
            started: true,
            completed: true,
            onHold: false,
            
            startDate: new Date(),
            estimatedDueDate: new Date(),
            completedDate: new Date(),
            )
    
    println "attempt to save project #3"
    if( !dataItem.save() )
            {
            dataItem.errors.each
                    {
                    println "error: " + it
                    }
            }
    
    
    
    // Users
    // User2s
    User2 user2 = new User2()
    user2.setUsername("steve")
    user2.setPassword("steve")
    user2.save()
    
    user2 = new User2()
    user2.setUsername("jay")
    user2.setPassword("jay")
    user2.save()
    
    user2 = new User2()
    user2.setUsername("sally")
    user2.setPassword("sally")
    user2.save()
    
    println "*** BootStrap, Done"
    }
    
    //---------------------------------------------------------
    void configureForTest()
    {
    println "configureForTest() called"
    }
    
    //---------------------------------------------------------
    void configureForProduction()
    {
    println "configureForProduction() called"
    }
    
    }
    

To change the port number of the Grails web server

When you issue the command grails run-app , Grails is starting a Jetty web server . Grail's Jetty web server runs on port 8080 by default. This is also the same default port number as Tomcat.

There are at least two ways to change the port number of the Grails (Jetty) web server.

Method 1

Edit the file GRAILS_HOME/scripts/Init.groovy and change the phrase 8080 to another port number like 9090 .

Method 2

When running the Grails web app, add a command line parameter of -Dserver.port=9090 before the run-app phrase.

Example:

grails -Dserver.port=9090 run-app

To get a war file of your Gails app

To get a war file of your Gails app configured to use production environment, run the command

grails war

When the command completes, a war file named projecttracker-0.1.war will be located in the root of your project.

To get a war file of your Gails app configured to use the production environment with a custom file name, run the command

grails prod war projecttracker.war

or

grails war projecttracker.war

The war file named projecttracker.war will be located in the root of your project.

To get a war file of your Gails app configured to use development environment and using a custom file name, run the command

grails dev war projecttracker.war

The war file will be located in the root of your project.

About Controllers

Controllers have Actions that are implemented as Closures .

The Closure Action s generally have the following returning statements:

  • redirect to another contoller action
  • render (to render text, html, xml, json, etc)
  • a model that will be passed to a view with same name as the closure
  • org.springframework.web.servlet.ModelAndView

Providing "Get" REST Web Service URLs

  • To create a REST controller, run the command:
    grails create-controller restV1
    
  • This created the following files:
    grails-app/controllers/RestV1Controller.groovy
    test/integration/RestV1ControllerTests.groovy
    

    and the directory

    grails-app/views/restV1
    
  • Edit grails-app/controllers/RestV1Controller.groovy from
    class RestV1Controller {
    
        def index = { }
    }
    

    to

    import grails.converters.*
    
    class RestV1Controller
    {
    
    //---------------------------------------------------------
    /*
    returns a REST GET view
    
    URL can contain the following params:
    
    id specify the id for display; if not present, 
            a list of all items will be returned
    output type of return results data format
       XML is default format
       User can specify json result format with output=json 
       User can specify html result format with output=html 
    
    */
    def project = {
    
    def results = null
    
    // get data needed
    if (params.id && Project.exists(params.id))
            {
            results = Project.get( params.id )
            }
    else
            {
            results = Project.list()
            }
    
    switch (params.output)
            {
            case ~/(?i)html/:
                    if (params.id == null)
                            {
                            // Call another action in another controller class
                            redirect(controller:'projectDb', action:'list', params:params)
                            }
                    else
                            {
                            // Call another action in another controller class
                            redirect(controller:'projectDb', action:'show', params:params)
                            }
                    break
            case ~/(?i)json/:
                    render results as JSON
                    break
            default:        
                    render results as XML
                    break
            }
    }
    
    }
    
  • Now, start up the Grails web app and point a web browser to any of the following URLs:
    http://localhost:9090/projecttracker/restV1
    http://localhost:9090/projecttracker/restV1/project
    http://localhost:9090/projecttracker/restV1/project?id=1
    http://localhost:9090/projecttracker/restV1/project?id=2
    http://localhost:9090/projecttracker/restV1/project?id=3
    http://localhost:9090/projecttracker/restV1/project/1
    http://localhost:9090/projecttracker/restV1/project/2
    http://localhost:9090/projecttracker/restV1/project/3
    http://localhost:9090/projecttracker/restV1/project/1?output=json
    http://localhost:9090/projecttracker/restV1/project/2?output=json
    http://localhost:9090/projecttracker/restV1/project/3?output=json
    http://localhost:9090/projecttracker/restV1/project/1?output=html
    http://localhost:9090/projecttracker/restV1/project/2?output=html
    http://localhost:9090/projecttracker/restV1/project/3?output=html
    
    Fig. 7: Grails Web App

    Here is an example of JSON output (newlines added for format):

    [{"id":1,
    "class":"Project",
    "briefStatusDescription":"going good",
    "completed":false,
    "completedDate":null,
    "description":"A web app to track development projects",
    "displayOrder":10,
    "estimatedDueDate":null,
    "lastModifiedBy":null,
    "lastModifiedTs":new Date(1210435279075),
    "name":"Project Tracker",
    "onHold":false,
    "percentDevComplete":64.2,
    "referenceNbr":"100",
    "startDate":new Date(1210435279075),
    "started":true,
    "status":"inprogress",
    "workersAssigned":"ME"},
    
    {"id":2,
    "class":"Project",
    "briefStatusDescription":"waiting for a round \"TUIT\"",
    "completed":false,
    "completedDate":null,
    "description":"A web app to manage to do lists",
    "displayOrder":20,
    "estimatedDueDate":null,
    "lastModifiedBy":null,
    "lastModifiedTs":new Date(1210435280151),
    "name":"To Do List Manager",
    "onHold":false,
    "percentDevComplete":0,
    "referenceNbr":"200",
    "startDate":null,
    "started":false,
    "status":"notstarted",
    "workersAssigned":null},
    
    {"id":3,"class":"Project",
    "briefStatusDescription":"DONE",
    "completed":true,
    "completedDate":new Date(1210435280151),
    "description":"A web app to track time worked on tasks",
    "displayOrder":30,
    "estimatedDueDate":new Date(1210435280151),
    "lastModifiedBy":null,
    "lastModifiedTs":new Date(1210435280151),
    "name":"Person Time Worked Tracker",
    "onHold":false,
    "percentDevComplete":110,
    "referenceNbr":"300",
    "startDate":new Date(1210435280151),
    "started":true,"status":"notstarted",
    "workersAssigned":"ME, Stan, Brenda"}]
    

Adding a URL Mapping

You can read about custom URL Mapping in the Grails Reference Guide, URL Mappings .

To add a special mapping for the REST web service, modify the file grails-app/conf/UrlMappings.groovy from

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

to

class UrlMappings {

static mappings = {

	"/restwebservice/v1/$action?/$id?"(controller: "restV1")

	// primary default Grails URL mapping
	"/$controller/$action?/$id?" {
		constraints {
			// apply constraints here
			}
		}

	"500"(view:'/error')
	}
}
  • Now, you can start up the Grails web app and point a web browser to any of the following URLs:
    http://localhost:9090/projecttracker/restwebservice/v1
    http://localhost:9090/projecttracker/restwebservice/v1/project
    http://localhost:9090/projecttracker/restwebservice/v1/project?ouput=xml
    http://localhost:9090/projecttracker/restwebservice/v1/project?ouput=json
    http://localhost:9090/projecttracker/restwebservice/v1/project?ouput=html
    http://localhost:9090/projecttracker/restwebservice/v1/project?id=1
    http://localhost:9090/projecttracker/restwebservice/v1/project?id=2
    http://localhost:9090/projecttracker/restwebservice/v1/project?id=3
    http://localhost:9090/projecttracker/restwebservice/v1/project/1
    http://localhost:9090/projecttracker/restwebservice/v1/project/2
    http://localhost:9090/projecttracker/restwebservice/v1/project/3
    http://localhost:9090/projecttracker/restwebservice/v1/project/1?output=json
    http://localhost:9090/projecttracker/restwebservice/v1/project/2?output=json
    http://localhost:9090/projecttracker/restwebservice/v1/project/3?output=json
    http://localhost:9090/projecttracker/restwebservice/v1/project/1?output=html
    http://localhost:9090/projecttracker/restwebservice/v1/project/2?output=html
    http://localhost:9090/projecttracker/restwebservice/v1/project/3?output=html
    

Adding YUI CSS and YUI Menu

  • Let's say for aesthetic reasons, we want to add YUI CSS and YUI Menu to every web page (but not the REST URLs). We can do this using a Sitemesh layout file.

    Read more about Sitemesh in the Grails Reference Guide, Layouts with Sitemesh .

    Layouts are located in the grails-app/views/layouts directory. The primary and default layout file is grails-app/views/layouts/main.gsp .

    We can change the look and feel of all web pages by modifying this file.

  • Edit the file grails-app/views/layouts/main.gsp from
    <html>
        <head>
            <title><g:layoutTitle default="Grails" /></title>
            <link rel="stylesheet" href="${createLinkTo(dir:'css',file:'main.css')}" />
            <link rel="shortcut icon" href="${createLinkTo(dir:'images',file:'favicon.ico')}" type="image/x-icon" />
            <g:layoutHead />
            <g:javascript library="application" />                          
        </head>
        <body>
            <div id="spinner" class="spinner" style="display:none;">
                <img src="${createLinkTo(dir:'images',file:'spinner.gif')}" alt="Spinner" />
            </div>  
            <div class="logo"><img src="${createLinkTo(dir:'images',file:'grails_logo.jpg')}" alt="Grails" /></div> 
            <g:layoutBody />                
        </body>     
    </html>
    

    to

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
    "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="http://www.w3.org/MarkUp/SCHEMA/xhtml11.xsd"
     xml:lang="en" >
<head>
<title><g:layoutTitle default="Demo App" /> - Project Tracker Demo App</title>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />

<!-- start YUI CSS -->
<link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.5.1/build/reset-fonts-grids/reset-fonts-grids.css">
<link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.5.1/build/base/base-min.css">
<!-- end YUI CSS --> 

<!-- start other YUI skin CSS --> 
<link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.5.1/build/assets/skins/sam/skin.css"> 
<!-- end other YUI skin CSS --> 


<script type="text/javascript" src="http://code.jquery.com/jquery-latest.pack.js"></script>
<!--
<g:javascript src="jquery-1.2.3.min.js" />
-->
<script type="text/javascript">
var $j = jQuery.noConflict();
</script>

<!-- start the YUI Loader script: -->
<script type="text/javascript" src="http://yui.yahooapis.com/2.5.1/build/yuiloader-dom-event/yuiloader-dom-event.js"></script>
<!-- end the YUI Loader script: -->

<g:javascript>
var MYCOMPANY = YAHOO.namespace("mycompany777com");  
</g:javascript>

<link rel="stylesheet" href="${createLinkTo(dir:'css',file:'main.css')}" />
<link rel="stylesheet" href="${createLinkTo(dir:'css',file:'additional.css')}" />
<link rel="shortcut icon" href="${createLinkTo(dir:'images',file:'favicon.ico')}" type="image/x-icon" />


<g:layoutHead />

<g:javascript library="application" />				

<!-- page-specific scripts -->
<g:javascript src="attach-yui.js" />
<g:javascript src="progress-bar-yui.js" />

</head>

<body class=" yui-skin-sam">

<div id="spinner" class="spinner" style="display:none;">
	<img src="${createLinkTo(dir:'images',file:'spinner.gif')}" alt="Spinner" />
</div>



<div id="doc3">

<div id="hd">
<!-- header -->
<div class="header">

<!--
<div class="logo"><img src="${createLinkTo(dir:'images',file:'grails_logo.jpg')}" alt="Grails" /></div>	
-->
<div class="banner">
<div class="banner-text">
<a href="${createLinkTo(dir:'',file:'index.gsp')}">Project Tracker</a>
</div>
</div>

<!-- application menu begins here -->
<g:if test="${session?.user != null}">
<div id="mymenu" class="yuimenubar yuimenubarnav">
	<div class="bd">
		<ul class="first-of-type">
			<li class="yuimenubaritem first-of-type">
			<a class="yuimenubaritemlabel" href="<g:createLink controller="project" action="list" />">Project</a>
				<div id="projectmenu" class="yuimenu">
					<div class="bd">
						<ul>
							<li class="yuimenuitem"><a class="yuimenuitemlabel" href="<g:createLink controller="project" action="list" />">Project List</a></li>
							<li class="yuimenuitem"><a class="yuimenuitemlabel" href="<g:createLink controller="project" action="listusetemplate" />">Project List (Using Template)</a></li>
						</ul>
					</div>
				</div>



			</li>

			<li class="yuimenubaritem">
			<a class="yuimenubaritemlabel" href="<g:createLink controller="dbMaint" action="index" />">Database Maintenance</a>
			</li>

			<li class="yuimenubaritem"><a class="yuimenubaritemlabel" href="#">Profile</a>
				<div id="profilemenu" class="yuimenu">
					<div class="bd">
						<ul>
							<li class="yuimenuitem"><a class="yuimenuitemlabel" href="<g:createLink controller="userProfile" action="changepasswordform" />">Change Password...</a></li>
						</ul>
					</div>
				</div>
			</li>

			<li class="yuimenubaritem">
			<a class="yuimenubaritemlabel" href="<g:createLink controller="project" action="uploadform" />">File Upload</a>
			</li>
			<li class="yuimenubaritem">
			<a class="yuimenubaritemlabel" href="<g:createLink controller="project" action="uploadformajax" />">File Upload (Ajax)</a>
			</li>

			<li class="yuimenubaritem">
			<a class="yuimenubaritemlabel" href="<g:createLink controller="help" action="index" />">Help</a>
			</li>

			<li class="yuimenubaritem">
			<a class="yuimenubaritemlabel" href="<g:createLink controller="login" action="logout" />">Logout</a>
			</li>

		</ul>            
	</div>
</div>

</g:if>

</div>
</div><!-- end div id="hd" -->




<div id="bd">
<!-- body -->
<div class="article">
<g:layoutBody />		

<p>
&nbsp;
</p>
<p>
&nbsp;
</p>

</div>
</div><!-- end div id="bd" -->




<div id="ft">
<!-- footer -->
<div class="footer">
<p class="text-small">
Project Tracker Application<br />
Copyright &copy; 2008 Me Enterprises
</p>
</div>
</div><!-- end div id="hd" -->

</div><!-- end div id="doc" -->

</body>
</html>
  • Add this file attach-yui.js as web-app/js/attach-yui.js
  • Add this file progress-bar-yui.js as web-app/js/progress-bar-yui.js
  • Add this file additional.css as web-app/css/additional.css
  • Here is what the Grails web app looks like now.Fig. 8: Grails Web App
  • Because I prefer to style the main XHTML tags myself, I will update the file web-app/css/main.css to have the following contents:

    (Feel free to delete the commented out CSS code.)

/*
html * {
    margin: 0;
    /*padding: 0; SELECT NOT DISPLAYED CORRECTLY IN FIREFOX *8/
}
*/

/* GENERAL */

.spinner
{
padding: 5px;
position: absolute;
right: 0;
}

body
{
/*
background: #fff;
color: #333;
font: 11px verdana, arial, helvetica, sans-serif;
*/
}

a:link, a:visited, a:hover
{
/*
color: #666;
font-weight: bold;
text-decoration: none;
*/
} 

h1
{
/*
color: #006dba;
font-weight: normal;
font-size: 16px;
margin: .8em 0 .3em 0;
*/
}

ul
{
/*
padding-left: 15px;	
*/
}

input, select, textarea
{
background-color: #fcfcfc;
border: 1px solid #ccc;
/*
font: 11px verdana, arial, helvetica, sans-serif;
margin: 2px 0;
padding: 2px 4px;
*/
}

select
{
/*
padding: 2px 2px 2px 0;
*/
}

textarea
{
width: 250px;
height: 150px;
vertical-align: top;
}

input:focus, select:focus, textarea:focus
{
border: 1px solid #b2d1ff;
}

.body
{
/*
float: left;
margin: 0 15px 10px 15px;
*/
}

/* NAVIGATION MENU */

.nav {
    background: #fff url(../images/skin/shadow.jpg) bottom repeat-x;
    border: 1px solid #ccc;
    border-style: solid none solid none;	
    margin-top: 5px;
    padding: 7px 12px;
}

.menuButton {
    font-size: 10px;
    padding: 0 5px;
}
.menuButton a {
    color: #333;
    padding: 4px 6px;
}
.menuButton a.home {
    background: url(../images/skin/house.png) center left no-repeat;
    color: #333;
    padding-left: 25px;
}
.menuButton a.list {
    background: url(../images/skin/database_table.png) center left no-repeat;
    color: #333;
    padding-left: 25px;
}
.menuButton a.create {
    background: url(../images/skin/database_add.png) center left no-repeat;
    color: #333;
    padding-left: 25px;
}

/* MESSAGES AND ERRORS */

.message {
    background: #f3f8fc url(../images/skin/information.png) 8px 50% no-repeat;
    border: 1px solid #b2d1ff;
    color: #006dba;
    margin: 10px 0 5px 0;
    padding: 5px 5px 5px 30px
}

div.errors {
    background: #fff3f3;
    border: 1px solid red;
    color: #cc0000;
    margin: 10px 0 5px 0;
    padding: 5px 0 5px 0;
}
div.errors ul {
    list-style: none;
    padding: 0;	
}
div.errors li {
	background: url(../images/skin/exclamation.png) 8px 0% no-repeat;
    line-height: 16px;
    padding-left: 30px;
}

td.errors select {
    border: 1px solid red;
}
td.errors input {
    border: 1px solid red;
}

/* TABLES */

table {
/*
    border: 1px solid #ccc;
    width: 100%
*/
}

tr {
/*
border: 0;
*/
}


td, th { 
/*
    font: 11px verdana, arial, helvetica, sans-serif;
    line-height: 12px;
    padding: 5px 6px;
    text-align: left;
    vertical-align: top;
*/
}
th {
/*
    background: #fff url(../images/skin/shadow.jpg);
    color: #666;
    font-size: 11px;
    font-weight: bold;
    line-height: 17px;
    padding: 2px 6px;
*/
}
th a:link, th a:visited, th a:hover {
/*
    color: #333;
    display: block;
    font-size: 10px;
    text-decoration: none;
    width: 100%;
*/
}
th.asc a, th.desc a {
    background-position: right;
    background-repeat: no-repeat;
}
th.asc a {
    background-image: url(../images/skin/sorted_asc.gif);
}
th.desc a {
    background-image: url(../images/skin/sorted_desc.gif);
}

.odd {
    background: #f7f7f7;
}
.even {
    background: #fff;
}

/* LIST */

.list table {
    border-collapse: collapse;
}
.list th, .list td {
    border-left: 1px solid #ddd;
}
.list th:hover, .list tr:hover {
    background: #b2d1ff;
}

/* PAGINATION */

.paginateButtons {
    background: #fff url(../images/skin/shadow.jpg) bottom repeat-x;
    border: 1px solid #ccc;
    border-top: 0;
    color: #666;
    font-size: 10px;
    overflow: hidden;
    padding: 10px 3px;
}
.paginateButtons a {
    background: #fff;
    border: 1px solid #ccc;
    border-color: #ccc #aaa #aaa #ccc;
    color: #666;
    margin: 0 3px;
    padding: 2px 6px;
}
.paginateButtons span {
    padding: 2px 3px;
}

/* DIALOG */

.dialog table {
    padding: 5px 0;
}

.prop {
    padding: 5px;
}
.prop .name {
    text-align: left;
    width: 15%;
    white-space: nowrap;
}
.prop .value {
    text-align: left;
    width: 85%;
}

/* ACTION BUTTONS */

.buttons {
    background: #fff url(../images/skin/shadow.jpg) bottom repeat-x;
    border: 1px solid #ccc;
    color: #666;
    font-size: 10px;
    margin-top: 5px;
    overflow: hidden;
    padding: 0;
}

.buttons input {
    background: #fff;
    border: 0;
    color: #333;
    cursor: pointer;
    font-size: 10px;
    font-weight: bold;
    margin-left: 3px;
    overflow: visible;
    padding: 2px 6px;
}
.buttons input.delete {
    background: transparent url(../images/skin/database_delete.png) 5px 50% no-repeat;
    padding-left: 28px;
}
.buttons input.edit {
    background: transparent url(../images/skin/database_edit.png) 5px 50% no-repeat;
    padding-left: 28px;
}
.buttons input.save {
    background: transparent url(../images/skin/database_save.png) 5px 50% no-repeat;
    padding-left: 28px;
}
  • Here is what the Grails web app looks like now.Fig. 9: Grails Web App
  • Because I feel that the external CSS files should handle the major styling, modify the default file web-app/index.gsp from
    <html>
        <head>
            <title>Welcome to Grails</title>
                    <meta name="layout" content="main" />
        </head>
        <body>
            <h1 style="margin-left:20px;">Welcome to Grails</h1>
            <p style="margin-left:20px;width:80%">Congratulations, you have successfully started your first Grails application! At the moment
            this is the default page, feel free to modify it to either redirect to a controller or display whatever
            content you may choose. Below is a list of controllers that are currently deployed in this application,
            click on each to execute its default action:</p>
            <div class="dialog" style="margin-left:20px;width:60%;">
                <ul>
                  <g:each var="c" in="${grailsApplication.controllerClasses}">
                        <li class="controller"><g:link controller="${c.logicalPropertyName}">${c.fullName}</g:link></li>
                  </g:each>
                </ul>
            </div>
        </body>
    </html>
    

    to

    <html>
        <head>
            <title>Welcome to Grails</title>
                    <meta name="layout" content="main" />
        </head>
        <body>
            <h1>Welcome to Grails</h1>
    
            <p>Congratulations, you have successfully started your first Grails application! At the moment
            this is the default page, feel free to modify it to either redirect to a controller or display whatever
            content you may choose. Below is a list of controllers that are currently deployed in this application,
            click on each to execute its default action:</p>
    
            <div class="dialog">
                <ul>
                  <g:each var="c" in="${grailsApplication.controllerClasses}">
                        <li class="controller"><g:link controller="${c.logicalPropertyName}">${c.fullName}</g:link></li>
                  </g:each>
                </ul>
            </div>
    
        </body>
    </html>
    
  • Here is what the Grails web app looks like now.Fig. 10: Grails Web App

Add Login / Logout and Some Security

  • We will use a filter to apply an "is user logged in?" check to most of our controller actions. We will be using Grails filter convention to apply our filter. Create the file grails-app/conf/SecurityFilters.groovy with the following contents:
class SecurityFilters {

def filters = {

authenticated(controller: "*", action: "*")
	{
	before = {
		// ignore exception actions
		if (!actionName.equals("login") && !actionName.equals("handleLogin"))
			{
			if (!session.user)
				{
				println "failed security check!"
				redirect(controller: "login", action: "login")
				return false
				}
			}
		}
	}
} // end closure filters

}

More Controllers

  • We will now create a few controllers to support our menu items. Here are the suggested controllers with possible pages/actions:
    • Web Page: View Project Statuses > Main Page
      • Controller: Project
        • Possible Action(s): list
    • Web Page: Database Maintenance > Main Page
      • Controller: DbMaint
        • Possible Action(s): list
    • Web Page: Profile > Change Password
      • Controller: UserProfile
        • Possible Action(s): changepasswordform, changepassword
    • Web Page: Help Page
      • Controller: Help
        • Possible Action(s): index
    • Web Page: After Logout Page
      • Controller: Login
        • Possible Action(s): logout, loginform
  • Let us now create page controllers for the above.

    To create the controllers, run the following commands:

    grails create-controller Project
    grails create-controller DbMaint
    grails create-controller UserProfile
    grails create-controller Help
    grails create-controller Login
    

    This will have created at least two files per create-controller command.

  • Edit grails-app/controllers/DbMaintController.groovy to match the following:
//---------------------------------------------------------
//---------------------------------------------------------
class DbMaintController
{

//---------------------------------------------------------
def index = {

// send to a gsp view page

// render the view with the controller as the model
return new org.springframework.web.servlet.ModelAndView(
	"/dbMaint/dbmaintmain")
}

}
  • Edit grails-app/controllers/HelpController.groovy to match the following:
//---------------------------------------------------------
//---------------------------------------------------------
class HelpController
{

//def index = { }

//---------------------------------------------------------
def index = {

// send to a gsp view page

// render the view with the controller as the model
return new org.springframework.web.servlet.ModelAndView(
	"/help/help")
}

}
  • Edit grails-app/controllers/LoginController.groovy to match the following:
//---------------------------------------------------------
//---------------------------------------------------------
class LoginController
{
//def index = { }

//---------------------------------------------------------
def login = {
if (session.user)
	{
	redirect(uri:"/index.gsp")
	}
}

//---------------------------------------------------------
def handleLogin = {		
	
def u = User2.findByUsername( params.username )
if (u)
	{
	if (u.password == params.password)
		{
		session.user = u
		redirect(uri:"/index.gsp")
		}
	else
		{
		log.debug('incorrect password for ${params.username}')
		flash.message = "Username or password is not correct"
		redirect(action:'login')
		}
	}
else
	{
	log.debug('username ${params.username} does not exist')
	flash.message = "Username or password is not correct"
	redirect(action:'login')
	}
}

//---------------------------------------------------------
def logout = {
if (session.user)
	{
	session.user = null
	redirect(uri:"/login/login.gsp")
	}
}

}
  • Edit grails-app/controllers/ProjectController.groovy to match the following:
//---------------------------------------------------------
//---------------------------------------------------------
class ProjectController
{
List projects = null

// services
def importService

//---------------------------------------------------------
def index = {
redirect(action:'list', params:params)
}

//---------------------------------------------------------
def list = {
//projects = Project.list(params)
projects = Project.list(sort:"displayOrder", order:"asc")

// this is the model for the view
[ dataItemList : projects ]
}

//---------------------------------------------------------
def listusetemplate = {
//projects = Project.list(params)
projects = Project.list(sort:"displayOrder", order:"asc")

return new org.springframework.web.servlet.ModelAndView(
	"/project/list-use-template", [ dataItemList : projects ])
}

//---------------------------------------------------------
def uploadform = {
return new org.springframework.web.servlet.ModelAndView(
	"/project/upload-one-file-form")
}

//---------------------------------------------------------
def upload = {
println "begin upload process"

//println "request type: ${request.class.name}"
String strResult = importService.handleUploadAndImport(request);
flash.message = strResult 

println "end upload process"

// show the same upload form
redirect(controller: "project", action: "uploadform")
}

//---------------------------------------------------------
def uploadformajax = {
return new org.springframework.web.servlet.ModelAndView(
	"/project/upload-one-file-form-ajax")
}

//---------------------------------------------------------
def uploadajax = {
println "begin upload process"

//println "request type: ${request.class.name}"
String strResult = importService.handleUploadAndImport(request);

println "end upload process"
render strResult
}

}
  • Edit grails-app/controllers/UserProfileController.groovy to match the following:
//---------------------------------------------------------
//---------------------------------------------------------
class UserProfileController
{

//---------------------------------------------------------
def changepasswordform =
{
// send to a gsp view page

// render the view with the controller as the model
return new org.springframework.web.servlet.ModelAndView(
	"/userProfile/change-pw-form")
}

//---------------------------------------------------------
def changePassword = {		 
def u = User2.get(session.user.id)

if (params.pwd == params.pwconfirm)
	{
	u.setPassword( params.pwd )
	u.save()
	flash.message = "Password change successful."
	}
else
	{
	flash.message = "The password and the confirmation password don't match."
	}

redirect(action:'changepasswordform')
}

}
  • Create the following GSPs:
    • Create file grails-app/views/dbMaint/dbmaintmain.gsp with the following contents:
<html>

<head>
<title>Database Maintenance</title>
<meta name="layout" content="main" />
</head>

<body>

<h1>Database Maintenance</h1>

<p>
Pick the table to maintain.
</p>

<div class="dialog">
	<ul>
		<g:each var="c" in="${grailsApplication.controllerClasses}">
			<g:if test="${c.fullName.indexOf('dbmaint.') != -1}">
				<li class="controller"><g:link controller="${c.logicalPropertyName}">${c.fullName}</g:link></li>
			</g:if>
		</g:each>
	</ul>
</div>

</body>

</html>
  • Create file grails-app/views/help/help.gsp with the following contents:
<html>

<head>
<meta name="layout" content="main" />
<title>Help</title>
</head>

<body>
<h1>Help</h1>

<g:if test="${flash.message}">
<div class="message">${flash.message}</div>
</g:if>

<p>
TODO: put some helpful descriptions here.
</p>

<p>
TODO: put some helpful descriptions here.
</p>

<p>
TODO: put some helpful descriptions here.
</p>


</body>
</html>
  • Create file grails-app/views/login/login.gsp with the following contents:
<html>

<head>
<title>Login Page</title>
<meta name="layout" content="main" />
</head>

<body>

<h1>Login</h1>

<p>Welcome to Project Tracker. Login below.</p>

<g:if test="${flash.message}">
	<div class="message">${flash.message}</div>
</g:if>

<g:form controller="login" action="handleLogin" method="post">
<table>
<tr>
	<td><label for="username">Username:</label> </td>
	<td><input type="text" name="username" /></td>
</tr>
<tr>
	<td><label for="password">Password:</label> </td>
	<td><input type="password" name="password" /></td>
</tr>
<tr>
	<td>&nbsp;</td>
	<td><input id="login-button" class="button" type="submit" value="Login" /></td>
</tr>
</table>

</g:form>


<g:javascript>

MYCOMPANY.initPageJs = function() {

// apply nice look to the form submit button
var oButton1 = new YAHOO.widget.Button("login-button");

}; // end of MYCOMPANY.initPageJs;

</g:javascript>


</body>
</html>
  • Create file grails-app/views/project/list.gsp with the following contents:
<html>
<head>
<meta name="layout" content="main" />
<title>Project List</title>

<style type="text/css" media="screen">
h2
{
background-color: rgb(178, 209, 255);
margin: 0 0 0 0;
padding: 0.5em 0.5em 0.5em 0.5em;
}

td
{
border-style: none;
}

.even
{
background-color: rgb(249, 248, 232);
background-color: rgb(250, 249, 235);
background-color: rgb(254, 254, 241);
background-color: rgb(249, 250, 243);
}

.odd
{
background-color: rgb(233, 237, 243);
}

.small-text
{
font-size: x-small;
}

.important-text
{
font-size: 2em;
font-weight: bold;
}
</style>

</head>
<body>

<h1>Project List</h1>

<g:if test="${flash.message}">
<div class="message">${flash.message}</div>
</g:if>

<div class="projectlist">
    <table>
        <tbody>
        <g:each in="${dataItemList}" status="i" var="dataItem">

            <tr class="${(i % 2) == 0 ? 'odd' : 'even'}">
                <td colspan="3"><h2><span style="font-size: 0.5em;">Project Name: </span><br />
                ${dataItem.name?.encodeAsHTML()}</h2></td>
            </tr>
            <tr class="${(i % 2) == 0 ? 'odd' : 'even'}">
                <td class="small-text" colspan="3">Ref Nbr: ${dataItem.referenceNbr?.encodeAsHTML()}</td>
            </tr>

            <tr class="${(i % 2) == 0 ? 'odd' : 'even'}">
                <td colspan="3">Description: ${dataItem.description?.encodeAsHTML()}</td>
            </tr>
            <tr class="${(i % 2) == 0 ? 'odd' : 'even'}"
                <td colspan="3">Percent Dev Complete: <span class="important-text">${dataItem.percentDevComplete?.encodeAsHTML()} %</span></td>
            </tr>
            <tr class="${(i % 2) == 0 ? 'odd' : 'even'}">
                <td colspan="3">
                <div style="background-color: rgb(214, 220, 201); width: ${100.0 * 0.2}em; height: 2em;">
                <div style="background-color: rgb(240, 66, 92); width: ${dataItem.percentDevComplete * 0.2}em; height: 2em;">&nbsp;</div>
                </div>
                </td>
            </tr>
            <tr class="${(i % 2) == 0 ? 'odd' : 'even'}">
                <td colspan="3">Status: <span class="important-text">${dataItem.status?.encodeAsHTML()}</span></td>
            </tr>
            <tr class="${(i % 2) == 0 ? 'odd' : 'even'}">
                <td class="small-text" colspan="3">DB Id: ${dataItem.id?.encodeAsHTML()}</td>
            </tr>
            <tr class="${(i % 2) == 0 ? 'odd' : 'even'}">
                <td colspan="3">Brief Status Desc.: ${dataItem.briefStatusDescription?.encodeAsHTML()}</td>
            </tr>
            <tr class="${(i % 2) == 0 ? 'odd' : 'even'}">
                <td class="small-text" colspan="3">Workers Assigned: ${dataItem.workersAssigned?.encodeAsHTML()}</td>
            </tr>
            <tr class="${(i % 2) == 0 ? 'odd' : 'even'}">
                <td class="small-text">Started: ${dataItem.started?.encodeAsHTML()}</td>
                <td class="small-text">Completed: ${dataItem.completed?.encodeAsHTML()}</td>
                <td class="small-text">On Hold: ${dataItem.onHold?.encodeAsHTML()}</td>
            </tr>

            <tr class="${(i % 2) == 0 ? 'odd' : 'even'}">
                <td class="small-text" colspan="3">StartDate: ${dataItem.startDate?.encodeAsHTML()}</td>
            </tr>
            <tr class="${(i % 2) == 0 ? 'odd' : 'even'}">
                <td class="small-text" colspan="3">Estimated Due Date: ${dataItem.estimatedDueDate?.encodeAsHTML()}</td>
            </tr>
            <tr class="${(i % 2) == 0 ? 'odd' : 'even'}">
					<g:if test="${dataItem.completed}">
	                <td colspan="3"><strong>Completed Date: 
	                ${dataItem.completedDate?.encodeAsHTML()}<strong>
	                </td>
					</g:if>

            </tr>

        </g:each>
        </tbody>
    </table>
</div>
<div class="paginateButtons">
    <g:paginate total="${Project.count()}" />
</div>

</body>
</html>
  • Create file grails-app/views/userProfile/change-pw-form.gsp with the following contents:
<html>
<head>
<title>Change Password</title>
<meta name="layout" content="main" />
</head>

<body>
<h1>Change Password</h1>

<g:if test="${flash.message}">
	<div class="message">${flash.message}</div>
</g:if>

<g:form controller="userProfile" action="changePassword" method="post">
<table>
<tr>
	<td><label>New Password:</label> </td>
	<td><input type="password" name="pwd" validate="required" /></td>
</tr>
<tr>
	<td><label>New Password (Confirm):</label> </td>
	<td><input type="password" name="pwconfirm" validate="required" /><td>
</tr>
<tr>
	<td></td>
	<td><input class="button" type="submit" value="Submit" /></td>
<tr>
</table>

</g:form>

</body>
</html>
  • Now, modify web-app/index.gsp to be
<html>

<head>
<title>Welcome to Project Tracker</title>
<meta name="layout" content="main" />
</head>

<body>
<% if (!session.user) response.sendRedirect("login/login.gsp"); %>

<h1>Welcome to Project Tracker</h1>

<p>
You are now logged in as 
<strong><%= session?.user?.username %></strong>
</p>


</body>

</html>
  • Here is what the Grails web app Login looks like now.Fig. 12: Grails Web App
  • Here is what the Project list web page looks like.Fig. 13: Grails Web App
  • To display stats on your project, run the following command:
    grails stats
    
  • To clean up your project (i.e. delete compiled classes and other things), run the following command:
    grails clean
    

About Logging

Grails includes Log4j to enable application logging. The default logging level for the application is "error" level logging.

To change the logging level and the log message pattern just for the development environment, add the following code snipplet into the file grails-app/conf/Config.groovy at the proper location.

environments {
        development {
                // change log level and pattern for development environment
                log4j {
                appender.'stdout.layout'="org.apache.log4j.PatternLayout"
                appender.'stdout.layout.ConversionPattern'='%d{yyyyMMdd HH:mm:ss} %-5p [%t] %C{2} %L %M: %m%n'
                rootLogger="info,stdout"
                logger.org.hibernate="debug"
                }
        }
}

If you want to see the SQL statements from Hibernate, add or modify the following code fragment

                hibernate {
                        loggingSql = true
                }

in grails-app/conf/DataSource.groovy within the "environments, development" code block.

Resulting in something like the following:

// environment specific settings
environments {
        development {
                ...
                hibernate {
                        loggingSql = true
                }
        ...
        }
...
}

or add the line

        logSql = true

within the dataSource code block in the file grails-app/conf/DataSource.groovy, for the result

dataSource {
        ...
        logSql = true
        ...
}

By convention, the following will receive a variable named log that you can use to create log messages:

  • Controllers
  • Services
  • Domain Classes
  • Tag Libraries
  • other artifacts

Running Tests

  • Lets add a simple Test for the ProjectController. Modify the file test/integration/ProjectControllerTests.groovy from
class ProjectControllerTests extends GroovyTestCase {

    void testSomething() {

    }
}

to

//---------------------------------------------------------
//---------------------------------------------------------
class ProjectControllerTests extends GroovyTestCase
{

//---------------------------------------------------------
void setUp()
{
Project.list()*.delete()

def dataItem = null

dataItem = new Project(
	displayOrder: 10,
	
	name: "Project Tracker",
	status: 'inprogress',
	referenceNbr: 100,
	description: "A web app to track development projects",
	
	percentDevComplete: 64.2,
	briefStatusDescription: "going good",
	workersAssigned: "ME",
	
	started: true,
	completed: false,
	onHold: false,
	
	startDate: new Date(),
	estimatedDueDate: null,
	completedDate: null,
	)

println "attempt to save project #1"
if( !dataItem.save() )
	{
	dataItem.errors.each
		{
		println "error: " + it
		}
	}

dataItem = new Project(
	displayOrder: 20,
	
	name: "To Do List Manager",
	status: 'notstarted',
	referenceNbr: 200,
	description: "A web app to manage to do lists",
	
	percentDevComplete: 0.0,
	briefStatusDescription: "waiting for a round \"TUIT\"",
	workersAssigned: null,
	
	started: false,
	completed: false,
	onHold: false,
	
	startDate: null,
	estimatedDueDate: null,
	completedDate: null,
	)

println "attempt to save project #2"
if( !dataItem.save() )
	{
	dataItem.errors.each
		{
		println "error: " + it
		}
	}

dataItem = new Project(
	displayOrder: 30,
	
	name: "Person Time Worked Tracker",
	status: 'completed',
	referenceNbr: 300,
	description: "A web app to track time worked on tasks",
	
	percentDevComplete: 110.0,
	briefStatusDescription: "DONE",
	workersAssigned: "ME, Stan, Brenda",
	
	started: true,
	completed: true,
	onHold: false,
	
	startDate: new Date(),
	estimatedDueDate: new Date(),
	completedDate: new Date(),
	)

println "attempt to save project #3"
if( !dataItem.save() )
	{
	dataItem.errors.each
		{
		println "error: " + it
		}
	}
}

//---------------------------------------------------------
void tearDown()
{
Project.list()*.delete()
}

//---------------------------------------------------------
void testList()
{
ProjectController projectController = new ProjectController()
assertNotNull(projectController)

assertNull(projectController.importService)


def mapModel = projectController.list()
assertNotNull(mapModel)

if (mapModel instanceof Map)
	{
	assertNotNull(mapModel.dataItemList)
	
	def list = mapModel.dataItemList
	assertEquals(list.size(), 3)
	}
}

//---------------------------------------------------------
void testListusetemplate()
{
ProjectController projectController = new ProjectController()
assertNotNull(projectController)

assertNull(projectController.importService)

def mapModel = projectController.listusetemplate()
assertNotNull(mapModel)

if (mapModel instanceof Map)
	{
	assertNotNull(mapModel.dataItemList)
	
	def list = mapModel.dataItemList
	assertEquals(list.size(), 3)
	}
}


}
  • To run the unit and integration tests, run the command
    grails test-app
    
  • You can review the testing results by viewing the page test/reports/html/index.html

Providing File Upload

  • Lets provide some form of file upload. Create a new Service to process the upload.
    grails create-service import
    

    The above command created the files:

    grails-app/services/ImportService.groovy
    test/integration/ImportServiceTests.groovy
    
  • Modify file grails-app/services/ImportService.groovy from
    class ImportService {
    
        boolean transactional = true
    
        def serviceMethod() {
    
        }
    }
    

    to be

import com.mycompany777.projecttracker.Constants


//import org.codehaus.groovy.grails.commons.ApplicationHolder
import org.codehaus.groovy.grails.web.context.ServletContextHolder as SCH

import org.apache.commons.lang.StringUtils
import org.apache.commons.io.IOUtils
import org.apache.commons.io.FileUtils
import org.apache.commons.io.FilenameUtils
import org.apache.commons.io.IOCase
import org.apache.commons.lang.time.DateUtils

//import org.codehaus.groovy.grails.commons.ConfigurationHolder


//---------------------------------------------------------
//---------------------------------------------------------
class ImportService
{
boolean transactional = false

String strEncoding = Constants.STR_UTF_8
String strXhtmlLineBreak = Constants.STR_XHTML_LINE_BREAK
String strXhtmlIndent = Constants.STR_XHTML_INDENT

//---------------------------------------------------------
def serviceMethod()
{
//def servletContext = ApplicationHolder.getApplication().getParentContext().getServletContext()
def servletContext = SCH.servletContext

return "Hello from service"
}

//---------------------------------------------------------
def greeting()
{
//def servletContext = ApplicationHolder.getApplication().getParentContext().getServletContext()
def servletContext = SCH.servletContext

return "Hello from service"
}

//---------------------------------------------------------
/*
1: Project data 
etc...

-1: None of the above

*/
Integer determineFileTypeBasedOnFileName(String filename)
{
if (StringUtils.isBlank(filename))
	{
	return Constants.INT_FILE_TYPE_INVALID
	}

String wildcardMatcher = "*project*.txt"
if (FilenameUtils.wildcardMatch(filename, wildcardMatcher, IOCase.SENSITIVE))
	{
	return Constants.INT_FILE_TYPE_BLAH
	}

return Constants.INT_FILE_TYPE_INVALID
}

//---------------------------------------------------------
Boolean isInputFileNameValid(String filename)
{
if (StringUtils.isBlank(filename))
	{
	return false
	}

String extension = "txt"

if (FilenameUtils.isExtension(filename, extension))
	{
	String wildcardMatcher = "*data*.txt"
	if (FilenameUtils.wildcardMatch(filename, wildcardMatcher, IOCase.INSENSITIVE))
		{
		return true
		}
	}

return false
}

//---------------------------------------------------------
/*



*/
Map processUploadSingleFile(javax.servlet.http.HttpServletRequest request, 
	String strWebFormFileId,
	String strOutputDirectory)
{

// create output directory if it does not exist
File fileDirOutput = new File(strOutputDirectory)
if (!fileDirOutput.exists())
	{
	// this will make all the directories
	log.debug("forceMkdir, fileDirOutput: ${fileDirOutput}")
	FileUtils.forceMkdir(fileDirOutput)
	}

//org.springframework.web.multipart.commons.CommonsMultipartFile
Map mapSingleFileStatus = [:]

// assume it is empty unless find otherwise
mapSingleFileStatus.empty = true

def f = request.getFile(strWebFormFileId)
log.debug("f type is ${f.class.name}")
Integer nFileType1 = Constants.INT_FILE_TYPE_INVALID

log.debug("f.getOriginalFilename(): ${f.getOriginalFilename()}")
mapSingleFileStatus.originalFileName = f.getOriginalFilename()
nFileType1 = determineFileTypeBasedOnFileName(f.getOriginalFilename())
mapSingleFileStatus.fileType = nFileType1

if (isInputFileNameValid(f.getOriginalFilename()))
	{
	if (!f.empty)
		{
		mapSingleFileStatus.empty = false

		String strDataFileNameExcel = strOutputDirectory + "/" + f.getOriginalFilename()

		log.debug("strDataFileNameExcel: ${strDataFileNameExcel}")
		File fileOutput = new File(strDataFileNameExcel)	

		log.debug("transferTo, fileOutput: ${fileOutput}")
		f.transferTo( fileOutput )

		log.debug("file upload was successful")
		}
	else
		{
		mapSingleFileStatus.empty = true
		}
	}
else
	{
	log.debug("file name is not valid")
	}

return mapSingleFileStatus
}


//---------------------------------------------------------
/*



*/
String handleUploadAndImport(javax.servlet.http.HttpServletRequest request)
{
StringBuffer sbuffStatusMessage = new StringBuffer()

sbuffStatusMessage << "<strong>Upload</strong>"
sbuffStatusMessage << strXhtmlLineBreak
sbuffStatusMessage << "${strXhtmlIndent}${new Date()}: Begin Upload"
sbuffStatusMessage << strXhtmlLineBreak


//def servletContext = ApplicationHolder.getApplication().getParentContext().getServletContext()
def servletContext = SCH.servletContext


log.debug("begin upload process")


String strOutputDirectory = servletContext.getRealPath("/WEB-INF" + "/" + Constants.STR_DATA_DIRECTORY)


List listStatusFilesUploaded = []
Map mapSingleFileStatus = null


mapSingleFileStatus = processUploadSingleFile(request, "myFile", strOutputDirectory)
listStatusFilesUploaded << mapSingleFileStatus
if (mapSingleFileStatus.fileType != Constants.INT_FILE_TYPE_INVALID)
	{
	sbuffStatusMessage << "${strXhtmlIndent}${mapSingleFileStatus.originalFileName} uploaded"
	sbuffStatusMessage << strXhtmlLineBreak
	}

log.debug("Done uploading files")
log.debug("listStatusFilesUploaded: ${listStatusFilesUploaded}")



Boolean booleanDoImport = false
log.debug("listStatusFilesUploaded: ")
for (item in listStatusFilesUploaded)
	{
	log.debug("item: ${item}")
	if (item.fileType != Constants.INT_FILE_TYPE_INVALID)
		{
		booleanDoImport = true
		}
	}

sbuffStatusMessage << "${strXhtmlIndent}${new Date()}: Upload Complete"
sbuffStatusMessage << strXhtmlLineBreak



sbuffStatusMessage << "<strong>Import</strong>"
sbuffStatusMessage << strXhtmlLineBreak

String strResult = ""
if (booleanDoImport == true)
	{
	sbuffStatusMessage << "${strXhtmlIndent}${new Date()}: Begin Import"
	sbuffStatusMessage << strXhtmlLineBreak

	for (item in listStatusFilesUploaded)
		{
		log.debug("item: ${item}")

		switch (item.fileType)
			{
			case Constants.INT_FILE_TYPE_BLAH:
				log.debug("calling processDataInFile()")
				processDataInFile(item)
				log.debug("finished calling processDataInFile()")

				sbuffStatusMessage << "${strXhtmlIndent}${new Date()}: Imported Data File"
				sbuffStatusMessage << strXhtmlLineBreak
				break;

			default:
				break;
			}
		}

	sbuffStatusMessage << "${strXhtmlIndent}${new Date()}: Import Complete"
	sbuffStatusMessage << strXhtmlLineBreak
	}
else
	{
	sbuffStatusMessage << "${strXhtmlIndent}<span class=\"error-msg\">No valid files were uploaded.</span>"
	sbuffStatusMessage << strXhtmlLineBreak
	}


return sbuffStatusMessage.toString()
}

//---------------------------------------------------------
String processDataInFile(Map mapFileInfo)
{
//def servletContext = ApplicationHolder.getApplication().getParentContext().getServletContext()
def servletContext = SCH.servletContext

log.debug("processDataInFiles; begin import all")
log.debug("ImportService doImport begin")


log.debug("ImportService doImport complete")
return "processDataInFiles; finished importing all"
}

}
  • Add the service name as a variable to a Controller and the service will be injected into the Controller by Spring.
    • Edit grails-app/controllers/ProjectController.groovy and add the following property
      // services
      def importService
      
      
    • Also add to grails-app/controllers/ProjectController.groovy a closure to display the upload file form and another closure to process the upload file form submit.
      //---------------------------------------------------------
      def uploadform = {
      return new org.springframework.web.servlet.ModelAndView(
              "/project/upload-one-file-form")
      }
      
      //---------------------------------------------------------
      def upload = {
      println "begin upload process"
      
      //println "request type: ${request.class.name}"
      String strResult = importService.handleUploadAndImport(request);
      flash.message = strResult 
      
      println "end upload process"
      
      // show the same upload form
      redirect(controller: "project", action: "uploadform")
      }
      
  • Add these two files

    src/groovy/com/mycompany777/projecttracker/BasicUtil.groovy

    package com.mycompany777.projecttracker
    
    
    import org.apache.commons.io.FileUtils
    
    //---------------------------------------------------------
    //---------------------------------------------------------
    /**
    This requires commons-io-1.4.jar or higher 
    and not commons-io-1.2.jar to work correctly.
    */
    class BasicUtil
    {
    
    //---------------------------------------------------------
    /**
    
    
    */
    static void writeStringToFile(String strFileName, String data)
    {
    File file = new File(strFileName)
    FileUtils.writeStringToFile(file, data) 
    }
    
    }
    

    and

    src/groovy/com/mycompany777/projecttracker/Constants.groovy

    package com.mycompany777.projecttracker
    
    //---------------------------------------------------------
    //---------------------------------------------------------
    /**
    
    
    */
    class Constants
    {
    static final Integer INT_FILE_TYPE_BLAH = 1
    static final Integer INT_FILE_TYPE_INVALID = -1
    
    public static final String STR_DATA_DIRECTORY = "imported-data"
    
    public static final String STR_PATTERN_DATE_FOR_VIEWING = "EEE, dd MMM yyyy HH:mm:ss z" // RFC_822
    
    
    public static final String STR_UTF_8 = "UTF-8"
    public static final String STR_XHTML_LINE_BREAK = "<br />"
    public static final String STR_XHTML_INDENT = "&nbsp; &nbsp; &nbsp; "
    
    }
    
  • In the file grails-app/views/layouts/main.gsp , add another menu item to the menu to display the file upload page (if not already present). Here is a suggestion:
                            <li class="yuimenubaritem">
                            <a class="yuimenubaritemlabel" href="<g:createLink controller="project" action="uploadform" />">File Upload</a>
                            </li>
    
  • Create the following GSP page grails-app/views/project/upload-one-file-form.gsp
<html>

<head>
<meta name="layout" content="main" />
<title>Upload A Data File</title>
</head>

<body>
<h1>Upload A Data File</h1>

<g:if test="${flash.message}">
<div class="message">${flash.message}</div>
</g:if>

<p>
This form allows you to upload some data file.
</p>

<g:form id="form3" controller="project" action="upload" method="post" enctype="multipart/form-data">

<table>
<tr>
<td><label for="myFile">Some Data File:</label> </td>
<td><input type="file" name="myFile" id="myFile" size="70" /></td>
</tr>
<tr>
<td>&nbsp;</td>
<td>
<input type="submit" value="Upload and Import"/>
</td>
</tr>
</table>
</g:form>

</body>
</html>
  • Here is what the upload form looks like.Fig. 14: Grails Web App

Provide an Ajax version of the upload form

  • Although grails comes with built in Ajax support, I prefer to use the YUI JavaScript library and hand-code the Ajax functionality.
  • Edit grails-app/controllers/ProjectController.groovy and add the following two closures
    //---------------------------------------------------------
    def uploadformajax = {
    return new org.springframework.web.servlet.ModelAndView(
            "/project/upload-one-file-form-ajax")
    }
    
    //---------------------------------------------------------
    def uploadajax = {
    println "begin upload process"
    
    //println "request type: ${request.class.name}"
    String strResult = importService.handleUploadAndImport(request);
    
    println "end upload process"
    render strResult
    }
    
  • Modify grails-app/views/project/upload-one-file-form-ajax.gsp , to be the following:
<html>

<head>
<meta name="layout" content="main" />
<title>Upload A Data File</title>
</head>

<body>
<h1>Upload A Data File (Using Ajax)</h1>

<g:if test="${flash.message}">
<div class="message">${flash.message}</div>
</g:if>


<div id="message-import" class="status-msg" style="visibility: hidden;"></div>

<p>
This form allows you to upload some data file.
</p>

<form id="ajaxForm" name="ajaxForm" enctype="multipart/form-data">

<table>
<tr>
	<td><label for="myFile">Some Data File:</label> </td>
	<td><input type="file" name="myFile" id="myFile" size="70" /></td>
</tr>
<tr>
<td>&nbsp;</td>
<td>
<input type="button" id="link-upload" value="Upload and Import">
</td>
</tr>
</table>
</form>



<g:javascript>

MYCOMPANY.initPageJs = function() {

/*
var elResultStatus = document.getElementById('message-import');
elResultStatus.style.visibility = "hidden";
*/

// apply nice look to the form submit button
var oButton1 = new YAHOO.widget.Button("link-upload");



// BEGIN upload form ajax stuff

//---------------------------------------------------------
//---------------------------------------------------------
var objUploadFileImport = {

handleSuccess: function(o) {

YAHOO.progressbar.yui.containter.hideProgressBar();

YAHOO.log("The success handler was called.  tId: " + o.tId + ".", "info", "example");
if (o.responseText !== undefined)
	{
	//alert(o.responseText)
	// fill in the "import directory" location
	var elImportDirectory = document.getElementById("message-import");
	elImportDirectory.innerHTML = o.responseText;
	}
},

//---------------------------------------------------------
makeAjaxRequest_UploadFileImport: function ()
{

var callbackUploadFileImport = {
upload: this.handleSuccess,
cache: false
};

var sUrl = "<g:createLink controller="project" action="uploadajax" />";

// argument formId can be the id or name attribute value of the
// HTML form, or an HTML form object.
var formObject = document.getElementById("ajaxForm");

// the second argument is true to indicate file upload.
YAHOO.util.Connect.setForm(formObject, true);
var objAjaxRequest = YAHOO.util.Connect.asyncRequest('POST', sUrl, callbackUploadFileImport);
YAHOO.log("Initiating request; tId: " + objAjaxRequest.tId + ".", "info", "example");
}

}; // end of objUploadFileImport



var elLinkSubmitForm = new YAHOO.util.Element("link-upload");

//create an event listener:
var fnHandleClick_UploadFileImport = function(e)
	{
	// prevent default behavior
	YAHOO.util.Event.preventDefault(e);
	
	YAHOO.log("do import button clicked", "info", "example");
	
	//alert(elLinkSubmitForm.get('id') + ' clicked');

	// clear any previous call's message
	var elResultStatus = document.getElementById('message-import');
	elResultStatus.innerHTML = "<p>Importing, please wait...</p>";
	elResultStatus.style.visibility = "visible";
	
	YAHOO.progressbar.yui.containter.showProgressBar("Importing, please wait...");

	objUploadFileImport.makeAjaxRequest_UploadFileImport();
	};

// assign an event listener using 'on';
// el.addListener and el.on are identical here:
elLinkSubmitForm.on('click', fnHandleClick_UploadFileImport); 
//YAHOO.util.Event.addListener(elLinkSubmitForm, "click", fnHandleClick_UploadFileImport);


}; // end of MYCOMPANY.initPageJs;

</g:javascript>


</body>
</html>

Using Templates

If desired, one can contain snipplets of html view code in a template file. This is useful if the snipplet of html view code is used in multiple view pages. Read more about templates in the Grails Reference Guide, Views and Templates . For demonstration, we will use a template file for the individual project view code.

  • Create file grails-app/views/project/_projectTemplate.gsp with the following contents:

            <tr class="${(i % 2) == 0 ? 'odd' : 'even'}">
                <td colspan="3"><h2><span style="font-size: 0.5em;">Project Name: </span><br />
                ${dataItem.name?.encodeAsHTML()}</h2></td>
            </tr>
            <tr class="${(i % 2) == 0 ? 'odd' : 'even'}">
                <td class="small-text" colspan="3">Ref Nbr: ${dataItem.referenceNbr?.encodeAsHTML()}</td>
            </tr>

            <tr class="${(i % 2) == 0 ? 'odd' : 'even'}">
                <td colspan="3">Description: ${dataItem.description?.encodeAsHTML()}</td>
            </tr>
            <tr class="${(i % 2) == 0 ? 'odd' : 'even'}"
                <td colspan="3">Percent Dev Complete: <span class="important-text">${dataItem.percentDevComplete?.encodeAsHTML()} %</span></td>
            </tr>
            <tr class="${(i % 2) == 0 ? 'odd' : 'even'}">
                <td colspan="3">
                <div style="background-color: rgb(214, 220, 201); width: ${100.0 * 0.2}em; height: 2em;">
                <div style="background-color: rgb(240, 66, 92); width: ${dataItem.percentDevComplete * 0.2}em; height: 2em;">&nbsp;</div>
                </div>
                </td>
            </tr>
            <tr class="${(i % 2) == 0 ? 'odd' : 'even'}">
                <td colspan="3">Status: <span class="important-text">${dataItem.status?.encodeAsHTML()}</span></td>
            </tr>
            <tr class="${(i % 2) == 0 ? 'odd' : 'even'}">
                <td class="small-text" colspan="3">DB Id: ${dataItem.id?.encodeAsHTML()}</td>
            </tr>
            <tr class="${(i % 2) == 0 ? 'odd' : 'even'}">
                <td colspan="3">Brief Status Desc.: ${dataItem.briefStatusDescription?.encodeAsHTML()}</td>
            </tr>
            <tr class="${(i % 2) == 0 ? 'odd' : 'even'}">
                <td class="small-text" colspan="3">Workers Assigned: ${dataItem.workersAssigned?.encodeAsHTML()}</td>
            </tr>
            <tr class="${(i % 2) == 0 ? 'odd' : 'even'}">
                <td class="small-text">Started: ${dataItem.started?.encodeAsHTML()}</td>
                <td class="small-text">Completed: ${dataItem.completed?.encodeAsHTML()}</td>
                <td class="small-text">On Hold: ${dataItem.onHold?.encodeAsHTML()}</td>
            </tr>

            <tr class="${(i % 2) == 0 ? 'odd' : 'even'}">
                <td class="small-text" colspan="3">StartDate: ${dataItem.startDate?.encodeAsHTML()}</td>
            </tr>
            <tr class="${(i % 2) == 0 ? 'odd' : 'even'}">
                <td class="small-text" colspan="3">Estimated Due Date: ${dataItem.estimatedDueDate?.encodeAsHTML()}</td>
            </tr>
            <tr class="${(i % 2) == 0 ? 'odd' : 'even'}">
                                        <g:if test="${dataItem.completed}">
                        <td colspan="3"><strong>Completed Date: 
                        ${dataItem.completedDate?.encodeAsHTML()}<strong>
                        </td>
                                        </g:if>

            </tr>
  • Create file grails-app/views/project/list-use-template.gsp with the following contents:
<html>
<head>
<meta name="layout" content="main" />
<title>Project List</title>

<style type="text/css" media="screen">
h2
{
background-color: rgb(178, 209, 255);
margin: 0 0 0 0;
padding: 0.5em 0.5em 0.5em 0.5em;
}

td
{
border-style: none;
}

.even
{
background-color: rgb(249, 248, 232);
background-color: rgb(250, 249, 235);
background-color: rgb(254, 254, 241);
background-color: rgb(249, 250, 243);
}

.odd
{
background-color: rgb(233, 237, 243);
}

.small-text
{
font-size: x-small;
}

.important-text
{
font-size: 2em;
font-weight: bold;
}
</style>

</head>
<body>

<h1>Project List (using a template for each project)</h1>

<g:if test="${flash.message}">
<div class="message">${flash.message}</div>
</g:if>

<div class="projectlist">
    <table>
        <tbody>
        <g:each in="${dataItemList}" status="i" var="dataItem">

			<g:render template="projectTemplate" model="[i:i, dataItem:dataItem]" />

        </g:each>
        </tbody>
    </table>
</div>
<div class="paginateButtons">
    <g:paginate total="${Project.count()}" />
</div>

</body>
</html>

We already have the following action in the ProjectController.groovy that will render out our view that uses the template.

//---------------------------------------------------------
def listusetemplate = {
projects = Project.list(sort:"displayOrder", order:"asc")

return new org.springframework.web.servlet.ModelAndView(
        "/project/list-use-template", [ dataItemList : projects ])
}

We also already have a menu item ready to call the correct controller and action.

Debugging Tip

This works in the development environment only.

Add the query string "showSource" on the end of the URL to see the source code for a gsp view.

Example URL: http://localhost:9090/projecttracker/project/list?showSource

Source: Viewing the source of a compiled GSP in Grails

What to check into Source Code Control

Try doing a "grails clean" before you begin to import your Grails project files into source code control.

grails clean

Generally you should check in all files and directories and leave out the following:

stacktrace.log (file)

web-app\WEB-INF\classes (directory)

web-app\WEB-INF\spring (directory)

How to use commons-lang-2.4.jar instead of commons-lang-2.1.jar in your Grails App

  • Delete the file commons-lang-2.1.jar from GRAILS_HOME/lib
  • Add commons-lang-2.4.jar to GRAILS_HOME/lib

Upgrade the Project to a New Version of Grails

If I had been using Grails version 1.0.1 and then Grails version 1.0.2 was released and I wanted to upgrade, I could do the following.

Install Grails version 1.0.2, update GRAILS_HOME, and update my path to include the bin directory of Grails version 1.0.2. In my Grails project I would then issue the command:

grails upgrade

I would answer a few prompts and then I am done.

Export the Database Schema to a File for Inspection

To export the database schema, read the following blog entry, A Gant Script to Call Hibernate's SchemaExport

  • Download the file SchemaExport.groovy from that blog entry and place it in your GRAILS_HOME/scripts directory.
  • From the project directory, run the command:
    grails prod schema-export ddl.sql.txt
    

    The schema will be written to the file ddl.sql.txt

Speed up the Grails Web App with pooled

Your Grails web application is probably running pretty slow. In the file grails-app/conf/DataSource.groovy, set the pooled element of dataSource to true to speed up the Grails web application

dataSource {
        pooled = true
        ...
}

Presentor's Additional Thoughts

  • It is good to learn, know and be familiar with Groovy closures before learning Grails because the actions in your controllers are acomplished with closures.
  • Grails' learning curve consists of learning Grail's conventions and demystifying the "magic" that appears to me happening.
  • To a developer, it can seem that Grails is doing magic. This can seem a little frustration if you can't describe to yourself what is exactly going on.
  • Grails is easier to get started coding if you have a green field for the database. It is possible to do mappings to existing database tables; but, that adds to the getting started learning curve.
  • Grails is a good fit for where some organizations would have instead developed a MS Access form application.
  • For Java programmers that want to start to learn JavaScript (for RIA and Ajax), Groovy is a good language to learn before learning JavaScript because it eases the Java programmer into the world of dynamic languages. Coding in JavaScript aids in developing Rich Internet Application UI's and Ajax.
  • The author prefers to use hand-coded Ajax instead of using Grails' Ajax backed tags.
  • The expected inputs and output of a closure are sometimes hard to find documentation for. This may be because a javadoc-like tool that is closure aware is hard for people to find or use.
  • Once you have success with Groovy, there is reluctance to return to just Java. Groovy is addictive.
  • Once you have success with Grails, there is reluctance to return to just Sprint MVC or Struts. Grails is addictive.