Grails Demo Walkthrough
Developing a Grails Web Application
Tutorial will: Walk through steps of creating a robust Grails web application.Application name:
grails-mongodb-demo
The Goal: To track and record persons addresses.
The Source: https://github.com/keaplogik/grails-mongodb-demo
The application will use the Grails Framework to develop a web application using:
-
MongoDB
-
Spring Security
-
Apache CXF (TODO)
-
Jetty Web Server
-
Maven build integration
Note: Tutorial follows maven but maven has been removed from the git project.
-Index
- Installing the Grails SDK And MongoDB
- Project Setup
- Configuring the Plugins
- Building an Application
- Advanced Application Features
Installing the Grails SDK And MongoDB
In this example, we are using Mac OS X. Some adjustments are required for Windows
Install Grails SDK
- Go to the Grails Download Page and install the latest 2.2.x binary
- Extract the file to any location, for example:
~/sdks
- Add the bin directory to your path
- Linux/Unix:
export GRAILS_HOME=~/sdks/grails-2.2.x
- Then add the
bin
directory to youPATH
variable:export PATH="$PATH:$GRAILS_HOME/bin
- Then add the
- Windows:
set an environment variable under My Computer/Advanced/Environment Variables
- Linux/Unix:
Executing Commands
- Open a console
- The grails commands are used in the manner:
grails [command]
- You can also launch the grails script executor by simply typing
grails
.
- The grails commands are used in the manner:
Once you create a Grails project, you can use this console to quickly create domain classes, controllers, or many other Grails components. This will not be neccesary in this demo, since we will be using the IntelliJ Idea 11 IDE
Install Mongo
- Follow the quickstart guide online. There are specific instructions for each OS.
Project Setup
Project setup requires working within a terminal/command window
1. Creating a Grails Application
$ cd ~/dev/workspaces
$ grails create-app grails-mongodb-demo
2. Integrating with Maven
$ cd grails-mongodb-demo
$ grails create-pom com.mycompany
$ mvn compile
The create-pom command expects a group id as an argument. The name and the version are taken from the application.properties of the application. The Maven plugin will keep the version in the pom.xml in sync with the version in application.properties.
Referenced From Grail Docs
3. Integrating with IntelliJ
**Integrating is simple! $ grails integrate-with --intellij
The last thing to do is open the project in IntelliJ, and thats it! Your ready to get started. A Run configuration should already be available.
Note: The latest version of this demo in git does not use maven. Maven really isn't the best first choice. To configure without maven, do not add a POM.xml to directory, and add the following definitions to the file at 'grails-app/conf/BuildConfig.groovy'
On with the maven tutorial..
Important Note: When grails creates the Maven POM, it adds a configuration to the plugin; grails-maven-plugin:
<fork>true</fork>
. If you want to be able to kill run processes from IntelliJ, comment out this line. back to top
Configuring the Plugins
When working with grails we don't typically have dependency jars. Instead, Grails uses plugins for it's management of artifacts. These plugins aren't just artifacts, but also are an extension of the Grails framework. Plugins are available for download on their website, and more information on their specs can be found in the documentation. You may also install plugins with the grails command:grails install-plugin [plugin-name]
Since we are working with maven, the plugins will be managed by maven. These plugins are available in the Grails remote repository.
This repo must be listed in your list of remote repositories:
<repository>
<id>grails-plugins</id>
<name>grails-plugins</name>
<url>http://repo.grails.org/grails/plugins</url>
</repository>
../my-app/grails-app/conf/BuildConfig.Groovy
To enable the grails command line to read the POMs, you must modify this file to use Maven when resolving dependencies. I would suggest removing the
plugins
property from this script as well.grails.project.dependency.resolution = {
…
//Use maven for dependency resolution
pom true
repositories {
…
mavenLocal()
}
//REMOVE if using maven
plugins {
...
}
}
1. MongoDB Plugin
By default, Grails comes pre-packaged with Hibernate. Since we are working with a NoSql data store, we will need to remove that plugin from our maven dependency list.Go into your pom.xml, and remove the dependency:
<dependency>
<groupId>org.grails.plugins</groupId>
<artifactId>hibernate</artifactId>
<version>2.1.1</version>
<scope>runtime</scope>
<type>zip</type>
</dependency>
<dependency>
<groupId>org.grails.plugins</groupId>
<artifactId>mongodb</artifactId>
<version>1.2.0</version>
<scope>compile</scope>
<type>zip</type>
</dependency>
- Remove the database-migration dependencey from your POM.
- To ensure removal, run command:
-
grails uninstall-plugin database-migration
-
grails uninstall-plugin tomcat
- Refresh by running:
grails refresh-dependencies
- There are some compatibility issues between some versions of the mongoDB plugin and Grails. If you get any errors running this application, first please consult the mongoDB plugin page.
back to top
2. Jetty Plugin
Grails, by default comes with Apache Tomcat as it's web server. To change to Jetty, we need to remove the Tomcat plugin dependency from our pom.xml.To make Jetty 7.6.0 the development time container for Grails, replace Tomcat plugin dependency with:
<dependency>
<groupId>org.grails.plugins</groupId>
<artifactId>jetty</artifactId>
<version>2.0.1</version>
<scope>compile</scope>
<type>zip</type>
</dependency>
back to top
3. Spring Security & Apache CXF Plugins
Add the following Maven dependencies to get Spring Security and Apache CXF support: <dependency>
<groupId>org.grails.plugins</groupId>
<artifactId>spring-security-core</artifactId>
<version>1.2.7.3</version>
<scope>compile</scope>
<type>zip</type>
</dependency>
<dependency>
<groupId>org.grails.plugins</groupId>
<artifactId>webxml</artifactId>
<version>1.4.1</version>
<scope>compile</scope>
<type>zip</type>
</dependency>
<dependency>
<groupId>org.grails.plugins</groupId>
<artifactId>ws-client</artifactId>
<version>1.0</version>
<scope>compile</scope>
<type>zip</type>
</dependency>
- Stop the current run process in IntelliJ, or if in console:
Ctrl-C
- Run:
mvn clean compile
(May not be necessary, but ensures a clean refresh) - Click Run button in IntelliJ. If your not in IntelliJ, on the command line run:
mvn grails:run-app
back to top
Building an Application
When building out the application, we will be using a combination of grails on the command line, and IntelliJ. You can decide what works best for creating skeleton domain and controller classes. But for this tutorial, I'm trying not to be completely bound to IntelliJ IDE.The Domain Model
*
The initial goal of this application, is to create and update person address history *
To get started we are going to first build out some skeleton classes for our model.
- In IntelliJ, press
command-alt-G
or right click in project view -> Grails -> Run Target- If on command line, cd to your application directory
- On the command line type:
create-domain-class org.mycompany.Person
- In console prepend grails to the command.
- On the command line type:
create-domain-class org.mycompany.Address
*Note: The names of database tables are automatically inferred from the name of the domain class.Now we have skeleton classes for our two domain classes under:
myapp/grails-app/domain
Open the Address class, and edit it as follows
class Address {
String streetAddress;
String city;
String state;
String zipCode;
//Date moveInDate;
//Date moveOutDate;
static belongsTo = [person: Person]
static constraints = {
streetAddress(blank: false)
city(blank: false)
state(blank: false, size: 2..2)
zipCode(blank: false, size: 5..5, validator: {val, obj -> val?.isNumber()})
/*moveInDate(nullable: false, max: new Date())
moveOutDate(nullable: true, validator: { val, obj ->
val?.after(obj.moveInDate)
})*/
}
}
- The static field
belongsTo
indicates that the class Person assumes ownership of the relationship. i.e. An Address belongs to only one person (in this case). - The static field
constraints
, handles the validation logic for each field. Most fields cannot be blank, but the move out date can be blank if a person hasn't moved out yet.moveOutDate
also has a custom validator to ensure the date is after the move in date. The dates are disabled to keep the demo simple. Feel free to uncomment them, to see how the scaffold view handles dates.
class Person {
String firstName;
String lastName;
static hasMany = [addresses: Address]
static constraints = {
firstName(blank: false)
lastName(blank: false)
}
}
- The static field
hasMany
Defines a one-to-many association between two classes. i.e. a Person has many Addresses.
hasMany
definition will not be efficient in a production setting. The reason I chose to use this, is because scaffold views do not support lists within domain classes. The mongo GORM plugin does support this. So a better way to create Person addresses would look as follows:class Person {
String firstName;
String lastName;
List<Address> addresses; //We are going to store an array of addresses in the person collection.
static constraints = {
firstName(blank: false)
lastName(blank: false)
}
}
You can view descriptions on all domain class features on the Grails Quick Reference Page
back to top
Controllers with Grails Scaffolding
Grails by default, has a decent UI for testing your new MVC components. Basic CRUD operations can be performed with one line of code in the model's controller. These views (called scaffolding views) are generated by the compiler when callinggrails run-app
To get started we are going to create controllers for our model classes.
- In IntelliJ, press
command-alt-G
or right click in project view -> Grails -> Run Target- If on command line, cd to your application directory
- On the command line type:
create-controller org.mycompany.Person
- In console prepend grails to the command.
- On the command line type:
create-controller org.mycompany.Address
PersonController
, and AddressController
for our model.Open up PersonController class.
- remove the line
def index() { }
and replce it withdef scaffold = Person
class PersonController {
def scaffold = Person
}
class AddressController {
def scaffold = Address
}
Configure GRAILS-Boootstrap
The Configuration class:Bootstrap
is useful for, because you can use it to add data to the application when the application starts. Copy the below Bootstrap class. It will add many People, with many associated addresses to the mongoDB instance, next time you run the application.import org.keaplogik.Person
import org.keaplogik.Address
class BootStrap {
def init = { servletContext ->
if (!Person.count()) {
def johnDoe = new Person( firstName: "John", lastName: "Doe" ).save(failOnError: true)
def joeReed = new Person( firstName: "Joe", lastName: "Reed" ).save(failOnError: true)
def jimSmith = new Person( firstName: "Jim", lastName: "Smith" ).save(failOnError: true)
def patrickHartwin = new Person( firstName: "Patrick", lastName: "Hartwin" ).save(failOnError: true)
def steveGunther = new Person( firstName: "Steve", lastName: "Gunther" ).save(failOnError: true)
def samWhiting = new Person( firstName: "Sam", lastName: "Whiting" ).save(failOnError: true)
def sarahMathews = new Person( firstName: "Sarah", lastName: "Mathews" ).save(failOnError: true)
def lisaPudock = new Person( firstName: "Lisa", lastName: "Pudock" ).save(failOnError: true)
def karaWhiting = new Person( firstName: "Kara", lastName: "Whiting" ).save(failOnError: true)
johnDoe.addToAddresses(
new Address(state: "NY", city: "Windsor", streetAddress: "117 W 2nd St", zipCode: "13865")
).addToAddresses(
new Address(state: "TX", city: "Alberta", streetAddress: "117 W 2nd St", zipCode: "55555")
).addToAddresses(
new Address(state: "NY", city: "Longely", streetAddress: "2 Sandy Creek", zipCode: "34009")
).addToAddresses(
new Address(state: "ME", city: "Ladly", streetAddress: "117 W 2nd St", zipCode: "55533")
).addToAddresses(
new Address(state: "KY", city: "Korba", streetAddress: "3 Apple St", zipCode: "40351")
).save(failOnError: true)
joeReed.addToAddresses(
new Address(state: "KY", city: "Frankfort", streetAddress: "33 Main St", zipCode: "77625")
).addToAddresses(
new Address(state: "PA", city: "Scranton", streetAddress: "71 Kind Ave Apt 3", zipCode: "44567")
).addToAddresses(
new Address(state: "PA", city: "Scranton", streetAddress: "8559 Hard Rock", zipCode: "44567")
).addToAddresses(
new Address(state: "WV", city: "Charleston", streetAddress: "8233 Juniper Rd", zipCode: "33982")
).save(failOnError: true)
jimSmith.addToAddresses(
new Address(state: "PA", city: "Blue Ridge", streetAddress: "780 Country Rd", zipCode: "44564")
).addToAddresses(
new Address(state: "TX", city: "Ft. Worth", streetAddress: "55 Holdem Dr." , zipCode: "77298")
).save(failOnError: true)
patrickHartwin.addToAddresses(
new Address(state: "CA", city: "Sacramento", streetAddress: "1 Beach Rd", zipCode: "98765")
).addToAddresses(
new Address(state: "CA", city: "Sacramento", streetAddress: "53 Sinking Dr." , zipCode: "98765")
).save(failOnError: true)
steveGunther.addToAddresses(
new Address(state: "CA", city: "Sacramento", streetAddress: "1 Beach Rd", zipCode: "98765")
).addToAddresses(
new Address(state: "CA", city: "Sacramento", streetAddress: "53 Sinking Dr." , zipCode: "98765")
).addToAddresses(
new Address(state: "CA", city: "Sacramento", streetAddress: "759 Sinking Dr." , zipCode: "98765")
).save(failOnError: true)
samWhiting.addToAddresses(
new Address(state: "CA", city: "Sacramento", streetAddress: "1 Beach Rd", zipCode: "98765")
).save(failOnError: true)
sarahMathews.addToAddresses(
new Address(state: "VT", city: "Burlington", streetAddress: "81 Lake Dr.", zipCode: "22183")
).addToAddresses(
new Address(state: "VT", city: "Burlington", streetAddress: "40 Shorten Ave Apt 33" , zipCode: "22183")
).addToAddresses(
new Address(state: "NY", city: "Plattsburgh", streetAddress: "1772 Lovely Lane" , zipCode: "22795")
).save(failOnError: true)
lisaPudock.addToAddresses(
new Address(state: "VT", city: "Burlington", streetAddress: "81 Lake Dr.", zipCode: "22183")
).addToAddresses(
new Address(state: "VT", city: "Burlington", streetAddress: "40 Shorten Ave Apt 33" , zipCode: "22183")
).addToAddresses(
new Address(state: "NY", city: "Plattsburgh", streetAddress: "1772 Lovely Lane" , zipCode: "22795")
).save(failOnError: true)
karaWhiting.addToAddresses(
new Address(state: "CA", city: "Sandiego", streetAddress: "9901 Shore Dr.", zipCode: "98741")
).save(failOnError: true)
}
}
def destroy = {
}
}
mvn grails:run-app
Your application will be running on localhost. Play around with the default scaffolding UI and the validation.
back to top
Advanced Application Features
Enhanced UI Design
When building a user interface, it is important to first choose a style library. For this example we will be using bootstrap css and js to quickly enhance the user interface of our application. I also would like to be able to customize the default theme, so we will be adding the lesscss-resources plugin as well.1. Configure Twitter Bootstrap
Head over to the Twitter Bootstrap Github Page and click on download the latest release in the Quick start section of the doc's.- Extract the files
- Copy the bootstrap .js files into your project at
web-app/js/bootstrap
- Copy the bootstrap .less files into your project at
web-app/less/bootstrap
- Copy the /img/~.png files over to your projects
web-app/images
directory
2. Configure Bootstrap with Less compiler
First, add the new dependency lesscss-resources to your POM. <dependency>
<groupId>org.grails.plugins</groupId>
<artifactId>lesscss-resources</artifactId>
<version>1.3.0.3</version>
<scope>runtime</scope>
<type>zip</type>
</dependency>
@import "bootstrap/bootstrap.less";
// Sprite icons path
// -------------------------
@iconSpritePath: "../images/glyphicons-halflings.png";
@iconWhiteSpritePath: "../images/glyphicons-halflings-white.png";
Finally, open the grails
Config
class and add the following lines to the bottom of the page.Note: After looking back, this code belongs in AplicationResources.groovy config file. Here is a link to the AplicationResources.groovy
grails.resources.modules = {
bootstrap {
resource url:'less/custom-bootstrap.less',attrs:[rel: "stylesheet/less", type:'css']
dependsOn 'jquery'
}
}
To use bootstrap in any gsp page, add the tag
<r:require modules="bootstrap"/>
in the head.3. Adding a custom controller
Now, we're going to modify our PersonController. Edit to look like so:class PersonController {
def scaffold = Person
def index = {
def people = Person.list([sort:"lastName", order:"asc"])
return [people: people]
}
}
myapp/grails-app/views/person/index.gsp
Add the following code to the groovy server page.
<@ page contentType="text/html;charset=UTF-8" >
<@ page contentType="text/html;charset=UTF-8" >
<html xmlns="http://www.w3.org/1999/html" xmlns="http://www.w3.org/1999/html">
<head>
<meta name="layout" content="main"/>
<r:require modules="bootstrap"/>
</head>
<body>
<section>
<div class="container">
<div class="row">
<header class="page-header">
<h3>Person <small class="lead">Address List</small></h3>
</header>
<div class="span3">
<g:link class="btn btn-block btn-link" action="create">
Create New Person
</g:link>
<div class="well">
<ul class="nav nav-list">
<li class="nav-header">People</li>
<li class="active">
<a id="view-all" href="#">
<i class="icon-chevron-right pull-right"></i>
<b>View All</b>
</a>
</li>
<g:each in="${ people }" var="person" status="i">
<li>
<a href="#Person-${person.id}">
<i class="icon-chevron-right pull-right"></i>
${ "${ person.firstName } ${ person.lastName }" }
</a>
</li>
</g:each>
</ul>
</div>
</div>
<div class="span9">
<g:each in="${ people }" var="person" status="i">
<div id="Person-${ person.id }" class="well well-small">
<table class="table table-bordered table-striped">
<caption>
${ "${ person.firstName } ${ person.lastName }" }: List of known addresses
</caption>
<thead>
<tr>
<th>State</th>
<th>City</th>
<th>Street</th>
<th>Zip Code</th>
<th>Options</th>
</tr>
</thead>
<tbody>
<g:each in="${ person.addresses }" var="address">
<tr>
<td>${ address.state }</td>
<td>${ address.city }</td>
<td>${ address.streetAddress }</td>
<td>${ address.zipCode }</td>
<td><g:link class="btn btn-small btn-inverse" controller="address"
action="edit" id="${address.id}">
<i class="icon-edit icon-white"></i>
</g:link>
</td>
</tr>
</g:each>
</tbody>
</table>
<div class="btn-group">
<g:link class="btn btn-primary" action="edit" id="${person.id}">
<i class="icon-edit icon-white"></i>Edit
</g:link>
</div>
</div>
</g:each>
</div>
</div>
</div>
</section>
<g:javascript>
$('ul.nav > li > a').click(function(e){
if($(this).attr('id') == "view-all"){
$('div[id*="Person-"]').fadeIn('fast');
}else{
var aRef = $(this);
var tablesToHide = $('div[id*="Person-"]:visible').length > 1
? $('div[id*="Person-"]:visible') : $($('.nav > li[class="active"] > a').attr('href'));
tablesToHide.hide();
$(aRef.attr('href')).fadeIn('fast');
}
$('.nav > li[class="active"]').removeClass('active');
$(this).parent().addClass('active');
});
</g:javascript>
</body>
</html>
PersonController.index()
maps to views/person/index.gsp
.Next, open the view:
views/layouts/main.gsp
. Remove any reference links to css files, except the main.css. Ensure that the tag <r:layoutResources />
is declared at both the bottom of the head and body tags. It should look similar to this.<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title><g:layoutTitle default="Grails Mongo"/></title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="${resource(dir: 'images', file: 'favicon.ico')}" type="image/x-icon">
<link rel="apple-touch-icon" href="${resource(dir: 'images', file: 'apple-touch-icon.png')}">
<link rel="apple-touch-icon" sizes="114x114" href="${resource(dir: 'images', file: 'apple-touch-icon-retina.png')}">
<link rel="stylesheet" href="${resource(dir: 'css', file: 'main.css')}" type="text/css">
<g:layoutHead/>
<r:layoutResources />
</head>
<body>
<g:layoutBody/>
<div id="spinner" class="spinner" style="display:none;"><g:message code="spinner.alt" default="Loading…"/></div>
<g:javascript library="application"/>
<r:layoutResources />
</body>
</html>
/* PAGINATION */
and /* MESSAGES AND ERRORS */
Finally, go into the Configuration Class:
URLMappings
and modify as such:class UrlMappings {
static mappings = {
"/$controller/$action?/$id?"{
constraints {
// apply constraints here
}
}
"/"(controller: "person", action: "index")
"500"(view:'/error')
}
}
Note: The grails runtime supports hot swapping of gsp files except for the template files. It seems to work with URLMappings, but I haven't tested what classes are supported and aren't.
4. Customize Scaffold Templates
We are going to customize the default scaffolding template. This will allow us to keep using scaffolding for our basic CRUD operations.In console run:
grails install-template
Copies the the templates used by Grails during code generation to your project directory at src/templates
- Add the tag
<r:require modules="bootstrap"/>
below<meta name="layout" content="main">
in each of the views: show, list, create, edit - Wrap each of the views with a
<div class="container"></div>
just inside the body - Modify the nav classes. Wrap the Navigation list with a
<div class="navbar"></div>
- Change the div wrapping the list
<div role="navigation">
to<div class="nav">
- Add the class nav to the ul tag like so:
<ul class="nav">
- Change the div wrapping the list
- Modify edit, show, create
- Modify the tag:
<fieldset class="buttons">
to<fieldset class="form-actions">
- In show.gsp replace all class names "property-value" with "uneditable-input"
- Change this span in show.gsp
<span id="${p.name}-label"...
> to a label tag.
- Modify the tag:
- Modify list.gsp
- The table class should look as follows:
<table class="table table-bordered table-striped">
- The table class should look as follows:
5. Improve Scaffold Output
One thing we would like to do is add proper error messages to CRUD forms. For each validation we can define an error message in thegrails-app/i8n/messages.properties
file. This file can map validation errors to their associated messages. Insert these lines at the bottom of this file.person.firstName.blank=first name cannot be left blank
person.lastName.blank=last name cannot be left blank
address.streetAddress.blank=Street Address cannot be left blank
address.city.blank=City cannot be left blank
address.zipCode.blank=Zipcode cannot be left blank
address.zipCode.validator=Zipcode must be a valid number
address.zipCode.size=Zipcode must be a valid 5 digit number
address.state.blank=State cannot be left blank
address.state.size=State must be entered as a valid state code. Ex: NY
So in Person
String toString(){ return "${firstName} ${lastName}"}
String toString(){ return "${streetAddress} ${city}, ${state}, ${zipCode}"}
back to top
Adding Security
If you would like to follow along but haven't walked through each tutorial, downoad this tag on github. This is a snapshot of the demo project before adding security. To clone from git, use these commmand: $ git clone https://github.com/keaplogik/grails-mongodb-demo.git
$ cd grails-mongodb-demo/
$ git checkout v1.0
1. Configuring Spring Security Plugin
To start, ensure you have the spring-security-core plugin installed. If not, add it to your POM if using maven, otherwise add it to the dependency list of theBuildConfig
class. If following along, with the grails-mongodb-demo, you will already have it installed.A useful command
s2-quickstart
is built into the security plugin. It generates the controllers to handle authentication, as well the domain classes you'll need to store user information. As in the springsource blog, we will create domain classes with names: SecUser and SecRole.
Also generated:
-
SecUserSecRole
- links users to roles. LoginController
LogoutController
- Associated login/logout views
$ grails s2-quickstart org.mycompany SecUser SecRole
The application now needs to specify mappings to the login/logout controllers. Add the following to the configuration class UrlMappings
: "/login/$action?"(controller: "login")
"/logout/$action?"(controller: "logout")
You can take a controller-centric approach and annotate the actions; work with static URL rules in Config.groovy; or define runtime rules in the database using request maps. Consult the springsource blog post Simplified Spring Security with Grails for more information.
2. Adding static URL maps
For the grails-mongodb-demo, we will secure the application using static URL rules.The Rules:
- Any authenticated user has access to the Person Index
- Only Admin's can create/update/delete Persons or Addresses
Config.groovy
class:grails.plugins.springsecurity.securityConfigType = 'InterceptUrlMap'
grails.plugins.springsecurity.interceptUrlMap = [
'/person/index': ['ROLE_USER, ROLE_ADMIN, IS_AUTHENTICATED_FULLY'],
'/person/**': ['ROLE_ADMIN'],
'/address/**': ['ROLE_ADMIN'],
'/js/**': ['IS_AUTHENTICATED_ANONYMOUSLY'],
'/css/**': ['IS_AUTHENTICATED_ANONYMOUSLY'],
'/images/**': ['IS_AUTHENTICATED_ANONYMOUSLY'],
'/*': ['IS_AUTHENTICATED_FULLY'],
'/login/**': ['IS_AUTHENTICATED_ANONYMOUSLY'],
'/logout/**': ['IS_AUTHENTICATED_ANONYMOUSLY']
]
grails.plugins.springsecurity.password.algorithm='SHA-512' //pw encryption algorithm
grails.plugins.springsecurity.portMapper.httpPort = "8080" //port map for http
grails.plugins.springsecurity.portMapper.httpsPort = "8443" //port map for https
grails.plugins.springsecurity.rejectIfNoRule = true //force authentication if no rule exists
3. Bootstrap in security data
Our application needs to determine who has access to what URL. Because of this, we need to create a set of rules. They only need to be created once on application start, so lets useBootstrap
to create them.class BootStrap {
def init = {
...
def userRole = SecRole.findByAuthority('ROLE_USER') ?: new SecRole(authority: 'ROLE_USER').save(failOnError: true)
def adminRole = SecRole.findByAuthority('ROLE_ADMIN') ?: new SecRole(authority: 'ROLE_ADMIN').save(failOnError: true)
...
}
}
- Admin User {u/p}: {admin/admin}
- Default User {u/p}: {guest/guest}
Bootstrap.init{...}
, add persistence for the default users : //add an admin and default user
def adminUser = SecUser.findByUsername('admin') ?: new SecUser(
username: 'admin',
password: 'admin',
enabled: true).save(failOnError: true)
def basicUser = SecUser.findByUsername('guest') ?: new SecUser(
username: 'guest',
password: 'guest', //pw encoded by security plugin
enabled: true).save(failOnError: true)
if (!adminUser.authorities.contains(adminRole)) {
SecUserSecRole.create adminUser, adminRole
}
if (!basicUser.authorities.contains(userRole)) {
SecUserSecRole.create basicUser, userRole
}
4. SecurityTagLib - Conditionally display gsp content
The security tags are part of thesec
namespace, and can be used in your gsp's. Heres an example:<sec:ifLoggedIn>
<p>Your Logged in!</p>
</sec:ifLoggedIn>
person/index
page. Specifically the condition will check if a user has the role: ROLE_ADMIN: <sec:ifAllGranted roles="ROLE_ADMIN">hide this stuff</sec:ifAllGranted>
Go ahead and open up the view
grails-mongodb-demo/grails-app/views/person/index.gsp
.Wrap the Create New Person link like so:
<sec:ifAllGranted roles="ROLE_ADMIN">
<g:link class="btn btn-block btn-link" action="create">
Create New Person
</g:link>
</sec:ifAllGranted>
<sec:ifAllGranted roles="ROLE_ADMIN">
<th>Options</th>
</sec:ifAllGranted>
...
<sec:ifAllGranted roles="ROLE_ADMIN">
<td><g:link class="btn btn-small btn-inverse" controller="address"
action="edit" id="${address.id}">
<i class="icon-edit icon-white"></i>
</g:link>
</td>
...
<sec:ifAllGranted roles="ROLE_ADMIN">
<div class="btn-group">
<g:link class="btn btn-primary" action="edit" id="${person.id}">
<i class="icon-edit icon-white"></i>Edit
</g:link>
</div>
</sec:ifAllGranted>
</sec:ifAllGranted>
5. Configure Security Pages with Bootstrap UI
We want to give the user an option to log out from any screen. Let's use a static navbar in our/layouts/main.gsp
:<body>
<div class="navbar navbar-static-top">
<div class="navbar-inner">
<div class="container">
<a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</a>
<g:link class="brand" uri="/">Address Tracker</g:link>
<div class="nav-collapse">
<ul class="nav pull-right">
<sec:ifLoggedIn>
<li>
<g:link controller="logout" action="index">Logout</g:link>
</li>
</sec:ifLoggedIn>
</ul>
</div>
</div>
</div>
</div>
<g:layoutBody/>
...
auth.gsp
page configured with twitter bootstrap is below, but you can modify yours however.<html>
<head>
<meta name='layout' content='main'/>
<r:require modules="bootstrap"/>
<title><g:message code="springSecurity.login.title"/></title>
</head>
<body>
<div class='container'>
<g:if test='${flash.message}'>
<div class='alert alert-error'>${flash.message}</div>
</g:if>
<div class="row span12">
<legend><g:message code="springSecurity.login.header"/></legend>
<div class="span6">
<form action='${postUrl}' method='POST' id='loginForm' class='form-horizontal' autocomplete='off'>
<div class="control-group">
<label class="control-label" for='username'><g:message code="springSecurity.login.username.label"/>:</label>
<div class="controls">
<input type='text' class='text_' name='j_username' id='username'/>
</div>
</div>
<div class="control-group">
<label class="control-label" for='password'><g:message code="springSecurity.login.password.label"/>:</label>
<div class="controls">
<input type='password' class='text_' name='j_password' id='password'/>
</div>
</div>
<div class="control-group" id="remember_me_holder">
<div class="controls">
<label class="checkbox" for='remember_me'><g:message code="springSecurity.login.remember.me.label"/>
<input type='checkbox' name='${rememberMeParameter}' id='remember_me'
<g:if test='${hasCookie}'>checked='checked'</g:if>/>
</label>
<input class="btn btn-primary" type='submit' id="submit" value='${message(code: "springSecurity.login.button")}'/>
</div>
</div>
</form>
</div>
<div class="span4">
<div class="">
<dl class="dl-horizontal">
<dt>Admin User (u/p):</dt>
<dd>admin/admin</dd>
</dl>
<dl class="dl-horizontal">
<dt>Guest User (u/p):</dt>
<dd>guest/guest</dd>
</dl>
</div>
</div>
</div>
</div>
</body>
</html>
<head>
<meta name='layout' content='main' />
<r:require modules="bootstrap"/>
<title><g:message code="springSecurity.denied.title" /></title>
</head>
<body>
<div class='container'>
<div class='alert alert-block alert-error'>
<g:message code="springSecurity.denied.message" />
<br/><br/>
<g:link class="btn btn-inverse" uri="/">Return Home</g:link>
</div>
</div>
</body>
back to top
This is great stuff, Billy. Thank you!
ReplyDeleteI found your post because I was trying to see how people were integrating Grails and Twitter Bootstrap. When I saw your article I expected to see reference to the Twitter Bootstrap Plugin for Grails (http://grails.org/plugin/twitter-bootstrap).
Have you tried that route and found yours superior? I am beginning a new project myself and want to chose the best way to integrate Grails and Bootstrap.
Thank you again!
Yes, I did check out the twitter-bootstrap plugin. The main issue is that it becomes difficult to customize bootstrap if you need. In this example, you can easily customize bootstrap by modifying bootstraps variables (link colors, background color, etc..) by adding overrides in your customize-bootstrap.less file. This is an issue with the bootstrap plugin. https://github.com/groovydev/twitter-bootstrap-grails-plugin/issues/9. Also you can add in additional less files to create your own template. Thing's like font-awesome or chosen for bootstrap. And finally, you are not bound to the bootstrap version that the plugin supports. This is a simple route to take. Another neet thing about the lesscssresource plugin, is that you can modify your less file, and changes take affect immediately on dev environment.
ReplyDeleteThat makes perfect sense. I'm going to go your route. Thank you! :^)
ReplyDeleteQuick comment:
ReplyDelete$ grails integrate-with --intelliJ
should be
$ grails integrate-with --intellij (lowercase "j")
Thanks, I made the adjustment. Also added a not to the "Building the Domain Model" section. With mongo, your better off not using the field "hasMany". Instead use a list of addresses within your Person domain class. Unfortunately, scaffold views do not support this, so for this demo stick with using the relational syntax. It is much more efficient to embed the address list into your Person collection items. I've found that queries are extremely slow when querying on relationships in Mongo.
ReplyDeleteHi Billy, just checking, your tutorial doesn't use any of the javascript files from bootstrap, right? Is there anything specific, like bundling those into a resource (e.g., AppResources.groovy), that we need to do before using the JS files on the front-end. I am just curious to know if there is a 'recommended' approach.
ReplyDeleteOk, after looking back at this, the less resource should probably have been declared in the ApplicationResource.groovy file. If you would like to include the twitter bootstrap javascript files, that would be the best place to add them. I have added an example ApplicationResource file you can use in place of the added bootstrap module in config.groovy: https://gist.github.com/4477107
ReplyDeleteAlso, you may want to separate out the javascript files into different modules. This way you only include the javascript that's necessary for the current page your working on. Say say you are using the popover.js library. In Application resources you would declare modules like so:
bootstrapTooltip {
resource url:'js/bootstrap/bootstrap-tooltip.js'
}
bootstrapPopover {
dependsOn 'bootstrapTooltip'
resource url:'js/bootstrap/bootstrap-popover.js'
}
In order to use the popover js, you have to import the tooltip js as well according the the tw bootstrap specs. That is why we use the dependsOn indicator above. Now inside your view.gsp, add this tag to your head:
That should import the javascript files necessary at the bottom of your generated page.
Hope this helps.
Correction to this line of last comment: *add this tag to your head: " The tag didn't appear in my comment. r:require modules="bootstrapPopover"
ReplyDeleteThanks Billy, this makes complete sense.
ReplyDeleteFor anyone else who is interested and who's also not using the twitter-bootstrap plugin, I'll suggest at least taking a look at the plugin's BootstrapResources.groovy file. It does a real fine job of enumerating and declaring all the sub-modules.
mvn compile command throws an error
ReplyDeletec:\dev\workspace\grails-mongodb-demo> mvn compile
'mvn' is not recognized as an internal or external command,
operable program or batch file.
You need to download and install the Maven compiler before running the application. Here is a link to an example on windows
DeleteThis comment has been removed by the author.
ReplyDeleteI am getting the following error. Please help me out!!
ReplyDeleteError |
2013-04-29 11:01:54,010 [main] ERROR context.GrailsContextLoader - Error initia
lizing Grails: Error creating bean with name 'instanceTagLibraryApi': Injection
of autowired dependencies failed; nested exception is org.springframework.beans.
factory.BeanCreationException: Could not autowire method: public void org.codeha
us.groovy.grails.plugins.web.api.TagLibraryApi.setGspTagLibraryLookup(org.codeha
us.groovy.grails.web.pages.TagLibraryLookup); nested exception is org.springfram
ework.beans.factory.BeanCreationException: Error creating bean with name 'gspTag
LibraryLookup': Invocation of init method failed; nested exception is org.spring
framework.beans.factory.BeanCreationException: Error creating bean with name 'or
g.codehaus.groovy.grails.plugins.web.taglib.ApplicationTagLib': Initialization o
f bean failed; nested exception is org.springframework.beans.factory.BeanCreatio
nException: Error creating bean with name 'grailsUrlMappingsHolder': Cannot reso
lve reference to bean 'urlMappingsTargetSource' while setting bean property 'tar
getSource'; nested exception is org.springframework.beans.factory.BeanCreationEx
ception: Error creating bean with name 'urlMappingsTargetSource': Cannot resolve
reference to bean 'org.grails.internal.URL_MAPPINGS_HOLDER' while setting const
ructor argument; nested exception is org.springframework.beans.factory.BeanCreat
ionException: Error creating bean with name 'org.grails.internal.URL_MAPPINGS_HO
LDER': Invocation of init method failed; nested exception is java.lang.NoSuchMet
hodError: com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap$Builder
.maximumWeightedCapacity(J)Lcom/googlecode/concurrentlinkedhashmap/ConcurrentLin
kedHashMap$Builder;
Were you able to resolve this? I need to update the project to the latest grails version and update mongo. Those may resolve the issue. Do a pull on the repo later today and try again.
DeleteI had some problems running the application after entrying the dummy data (not because of the dummy data). After updating mongoDB dependency problem was resolved:
ReplyDeleteorg.grails.plugins
mongodb
1.2.0
runtime
zip
I updated the source on github to use the latest stable mongo gorm client (1.2.0). Also updated to Grails 2.2.0. I'm curious why you set the build scope to runtime? In the specs it states "compile" http://grails.org/plugin/mongodb
DeleteMaybe I'm mistaken or I misunderstood something but this tutorial states:
Deleteorg.grails.plugins
mongodb
1.0.0.GA
runtime
zip
Ah I see what you are saying. I will make the adjustments in the article.
DeleteWhen running the application after doing all the plugin configuration steps and before "Building an Application" section I notice that I still see tomcat installed under Installed Plugins on the left side of the browser. Should that be saying Jetty? Also, I feel like the welcome page should be different if running jetty should it not?
ReplyDeleteI solved it...had to run this:
Deletegrails uninstall-plugin tomcat
grails refresh-dependencies
i'm using intellij with auto-import turned on and the auto import doesnt seem to be working.
Hi:
ReplyDeleteI am gettigng the following error, right after I configure everyhitng in pom.xml and try to do a maven clean compile from eclipse. (right before I get into "Building an app" section) -
symbol : method ultimateTargetClass(groovy.lang.GroovyObject)
location: class org.springframework.aop.framework.AopProxyUtils
Method method = ReflectionUtils.findMethod(AopProxyUtils.ultimateTargetClass(controller),
Please help
Thanks
I have not set this up in eclipse. Have you tried compiling from command line? Also, what version of grails you have installed may have an effect. The latest version uses 2.2.0. I am not familiar with the error you are seeing.
DeleteNice to see another IntelliJ based Grails developer. Rock on!
ReplyDeleteNice tutorial for the most part.. My only suggestion is loose the Maven portion. After all, this is about learning Grails and you yourself would not use it. After all, this about learning Grails and that's what I want to do.
ReplyDeleteIf you feel it's worth it, make a separate Maven specific tutorial.
Otherwise, I like the mix of things you've got here.
Thanks for your response. I gave it some thought and agree. I will leave the maven integration in the tutorial but comment about how it's been removed and how to do it without maven. I merged the git project with a fork someone had created and removed maven. Thanks again.
Deleteits nice i really like to understand and fallow
ReplyDeleteHi Billy,
ReplyDeleteIm a beginner with Grails ( have exp. in java though)
I need to extract data records from a table and display in tabular view using groovy-grails.
Please please help me out with to 'get started'
Really good tutorial..Thanks a lot.
ReplyDelete