Managing Content through Tagging in Grails: Part 1

Exclusive offer: get 50% off this eBook here
Grails 1.1 Web Application Development

Grails 1.1 Web Application Development — Save 50%

Reclaiming Productivity for faster Java Web Development

$23.99    $12.00
by Jon Dickinson | May 2009 | Java Open Source Web Development

As with messages, files are displayed on the home page in the order they are added to the system. Currently all messages and files are displayed on the home page. Over time, our home page is going to become rather large and unwieldy. We need a user's home page to show only the files and messages that they are interested in. To do this, users need to be able to tag their content.

The new Grails concepts that will be introduced in this two-part article series by Jon Dickinson are:

  • Working with inheritance in the domain classes, and looking at which strategies GORM supports for persistence
  • Using polymorphic queries over a domain inheritance hierarchy
  • Encapsulating view-rendering logic in GSP templates
  • Manipulating collections with the Groovy collect and sort methods

Add basic tagging

Tagging is a loose, community-based way of categorizing content. It allows a group of people to categorize by consensus. Anyone is able to tag a piece of content. The more a tag is used, the more meaning it takes on and the more widely used it becomes. This categorization by consensus has been dubbed as folksonomy (http://en.wikipedia.org/wiki/Folksonomy)

So let's get started by building our tagging support.

Tagging domain model

When implementing tagging in our system, we need to consider the following:

  • We must be able to have many tags in our system
  • We must be able to associate a single tag with many different files and messages
  • We need to make sure that new domain objects can be easily tagged without having to change the tagging logic
  • We want to know when a domain object was tagged

To satisfy these requirements, we need to create the following new domain classes:

  • Tag—to store the name of the tag. There is one instance of this class per unique tag name in the application.
  • Tagger—to store the relationship from domain objects to a tag. This allows us to store the date a tag was added to a domain object.

Let's create these domain classes and then write a test to prove that we can tag a message using this tagging structure.

The Tag class

We are going to separate the tagging classes out from our application domain classes. Create a folder under grails-app/domain called tagging. This is where we will put the domain model to implement tagging.

Our Tag class is extremely simple and holds only a name property:

package tagging
class Tag {
String name
static constrains = {
name( blank: false )
}
}

The Tagger class

The next class that we are going to create is the Tagger class. In relational terms, this object represents a link table between a Tag and any other domain class. It is important that the relationship between tagged domain classes and the Tagger relationship class is unidirectional. By this, we mean the domain classes are allowed to know that they can be tagged, but tags do not know which domain classes can be tagged, otherwise every tagged domain class would need a special relationship class.

Create the Tagger class as a domain class in the tagging package as follows:

package tagging
class Tagger {
Tag tag
static constraints = {
tag( nullable: false )
}
}

The basics of our tagging model are complete! We now need some logic to allow tags to be created. Create a new service class called TagService under grails-app/services/tagging, as shown below:

package tagging
class TagService {
boolean transactional = true
def createTagRelationships(String spaceDelimitedTags) {
return spaceDelimitedTags?.split(' ')?.collect { tagName ->
createTagRelationship( tagName )
}
}
def createTagRelationship(String tagName) {
def tag = Tag.findByName(tagName)?:
new Tag(name: tagName).save()
return new Tagger( tag: tag )
}

This service provides two utility methods to create new relationships by tag name or by a space delimited string of tag names. The important behavior of these two methods is that they do not allow duplicate tags to be created in the application. If a tag name already exists, the tag will be retrieved from the database and used as the tag in the relationship.

Notice that the createTagRelationships method is using the collect method to simplify what would normally take a few more lines of code to achieve. The collect method is dynamically added to any object that can be iterated over. For example, collections, arrays, strings and so on. It takes a closure as its argument and executes this closure for each item in the collection. The return value from each execution of the closure is added to a new collection that the collect method builds up and then returns once it has finished iterating the original collection.

In createTagRelationship, we are using another neat language feature of Groovy called the "Elvis operator". It is named so, as it looks like Elvis' hair style. This is a shorter version of the normal Java ternary operator. If the operand being checked is true then the checked operand will be returned as the default, otherwise the alternative operand will be used. So in our example:    

def tag = Tag.findByName(tagName) ?: new Tag(name: tagName).save()

If a tag can be found from the database then it is used, otherwise a new tag is created.

Tagging a message

The next step is to allow a message to be tagged. Write some integration tests to make sure the relationships are working before using tagging in the application.

In the folder test/integration/app, create the file TaggableIntegrationTests.groovy and add the following code:

package app
import tagging.Tag
class TaggableIntegrationTest extends GroovyTestCase {
User flancelot
protected void setUp() {
flancelot = User.findByUsername('flancelot')
Tag.list().each { it.delete() }
Message.list().each { it.delete() }
}
}

The code above sets up the test data needed to create messages and associate tags to messages. Remember that the flancelot user already exists because it was created by the BootStrap class.

The first test will determine that we can add tags to a message and then retrieve messages by tag. Add the following test method to your test class:

void testCanRetrieveMessagesByTags() {
Message message = new Message(user: flancelot, title: 'tagged',
detail: "I've been tagged.").save(flush: true)
Message secondMessage = new Message(user: flancelot,
title: 'other tagged',
detail: "I've been tagged.").save(flush: true)
message.addTag('urgent')
message.addTag('late')
secondMessage.addTag('urgent')
def taggedMessages = Message.withTag( 'urgent' )
assertEquals(2, taggedMessages.size())
assertEquals(2, Tag.list().size())
def secondMessages = Message.withTag( 'late' )
assertEquals(1, secondMessages.size())
assertEquals(2, Tag.list().size())
}

The test above does the following:

  • Creates two new messages
  • Adds the urgent tag to both messages
  • Adds the late tag to one message
  • Checks if we can retrieve both messages by using the urgent tag
  • Checks if only one message is returned for the late tag

Notice that the highlighted lines of code have not been implemented yet. To allow this test to pass, we need to add the following methods to the Message domain class:

  • addTag—instance method to allow a message to be tagged
  • withTag—class method to retrieve all messages with a particular tag

Add the following method to the Message class (don't forget to import tagging.Tagger):

def addTag(String tagName) {
tags = (tags)?:[]
tags << tagService.createTagRelationship( tagName )
}

This method simply delegates the creation of the tag relationship off to the TagService class, and then stores the relationship in the tags list.

Add the following method to the Message class that retrieves all messages with a given tag name:

def static withTag(String tagName) {
return Message.withCriteria {
tags {
tag {
eq('name', tagName )
}
}
}
}

This method must be static on the Message class, as it is used to load message instances for a given tag. We do not want to have to instantiate a message before we can perform the search.

Before running the test, you will notice both of these new methods assume that there is a property on the Message class called tags. This has not yet been created. We need to create a one-to-many relationship from Message to Tagger that will allow messages to be tagged. We also need to inject the TagService into new instances of the Message class so the work for creating a new tag relationship can be delegated. Add the relationship to the Message class and inject TagService as shown below:

class Message {
def tagService
static hasMany = [tags:Tagger]
...
}

Now we can run our tests by entering the following on the command line:

grails test-app

We should see some output in the command line similar to:

Running test app.TaggableTest...
testCanRetrieveMessagesByTags...SUCCESS

Tagging a file

Now that we have implemented tagging for messages, we need to make tagging available for files.

Currently the logic for creating and fetching tags is in the Message domain class. We need to extract this logic so the File domain class can reuse it. It's time to look at how GORM supports inheritance.

GORM inheritance

The GORM supports inheritance of domain classes by default through the underlying Hibernate framework. Hibernate has a number of strategies for handling inheritance and Grails supports the following two:

  • Table-per-hierarchy—this strategy creates one database table per inheritance hierarchy. This is the default strategy in Grails.
  • Table-per-subclass—this strategy creates a database table for each subclass in an inheritance hierarchy and treats the inheritance (is a) relationship as a foreign key (has a) relationship.

Taking our domain as an example, we have two classes. They are Message and File. We are going to make them both extend a super class Taggable, which will handle all of our tagging logic and state.

Table-per-hierarchy

If we were to choose the table-per-hierarchy strategy, we would end up with one table called Taggable that contained the data for both Message and File. The database structure would look something like:

Grails 1.1 Web Application Development

The interesting side-effect of this approach is that all of the fields to be persisted must be nullable. If a File is created and persisted, it is obviously not possible for the fields from Message to be populated.

Table-per-subclass

By using the table-per-subclass strategy, we would keep two separate tables called Message and File, and both would have the tags relationship inherited from Taggable. So the Message table will look like:

Grails 1.1 Web Application Development

We can see in the diagram above that the Message and File tables have remained separate and a table representing the superclass Taggable has been created, which the subclass tables have foreign key relationships to. In the table-per-subclass strategy, a table must exist to represent the inheritance (is a) relationship.

We are going to follow the table-per-subclass strategy so that we can retain database level data integrity. The default behavior for GORM is to use the table-per-hierarchy strategy. To override this we must use the mapping property:

static mapping = {
tablePerHierarchy false
}
Grails 1.1 Web Application Development Reclaiming Productivity for faster Java Web Development
Published: May 2009
eBook Price: $23.99
Book Price: $39.99
See more
Select your format and quantity:

Taggable superclass

Now that we have discussed how GORM handles domain class inheritance, it is time to implement our Taggable superclass that will allow Message and File to handle tagging. Create a domain class in the tagging package that:

  • Contains a set of Tagger instances
  • Defines the hibernate inheritance mapping strategy to use
  • Implements the addTag method from Message
  • Implements the withTag method from Message

The implementation for the base class looks like:

package tagging
class Taggable {
def tagService
static hasMany = [tags: Tagger]
static mapping = {
tablePerHierarchy false
}
def addTag(String tagName) {
tags = (tags)?:[]
tags << tagService.createTagRelationship( tagName )
}
def static withTag(String tagName) {
return Taggable.withCriteria {
tags {
tag {
eq('name', tagName )
}
}
}
}
}

Now that we have the tag-specific logic implemented in a base class, we need to remove the addTag and withTag methods from the Message class, as well as the tags relationship and the tagService property, and make the File and Message classes extend Taggable. Message, as shown below:

import tagging.Taggable
class Message extends Taggable {
...
}

Make the following changes to the File class:

import tagging.Taggable
class File extends Taggable {
...
}

Run the tests again and we can see that TaggableIntegrationTests still passes. Now add a test to verify that File objects can be tagged:

void testFileCanBeTagged() {
def fileData = new FileData(data: [0])
def aVersion = new FileVersion(name: name, fileData: fileData,
description: 'foo', extension: 'pdf', user: fred )
def firstFile = new File( currentVersion: aVersion )
.save(flush: true)
def secondFile = new File( currentVersion: aVersion )
.save(flush: true)
firstFile.addTag('draft')
secondFile.addTag('draft')
secondFile.addTag('released')
def draftFiles = File.withTag('draft')
assertEquals(2, draftFiles.size())
assertEquals(2, Tag.list().size())
def releasedFiles = File.withTag('released')
assertEquals(1, releasedFiles.size())
assertEquals(2, Tag.list().size())
}

This test verifies that File instances can be tagged in exactly the same way as Message instances.

Polymorphic queries

So far so good, however, there is an additional implication to inheritance with domain classes that we need to investigate, that is, the introduction of polymorphic queries. When we query a domain superclass, we are performing a polymorphic query, which means the query will actually run over the subclasses and return all matching instances from all of the subclasses.

In more practical terms, when we call the withTag method on File or Message, we are actually going to receive all File and Message instances with the specified tag. This is because the withTag implementation exists on the Taggable class, so we are performing the query against the Taggable class:

def static withTag(String tagName) {
return Taggable.withCriteria {
tags {
tag {
eq('name', tagName )
}
}
}
}

Let's write a test to prove this:

void testTaggedObjectsCanBeRetrievedByType() {
def fileData = new FileData( data: [0] )
def aVersion = new FileVersion( name: 'v1', fileData: fileData,
description: 'foo', size: 101, extension: 'pdf',
user: fred )
def firstFile = new File( currentVersion: aVersion )
.save(flush: true)
def message = new Message(user: fred, title: 'tagged',
detail: "I've been tagged.").save(flush: true)
firstFile.addTag('draft')
message.addTag('draft')

assertEquals(1, Message.withTag('draft').size())
assertEquals(1, File.withTag('draft').size())
}

Here we are creating a file and a message, and tagging each of them as draft. If we didn't know about polymorphic queries, we would expect to be able to retrieve one message with the 'draft' tag and one file with the 'draft' tag.

Running the tests now, we will see the following failed test in the output:

Running test app.TaggableTest...
testCanRetrieveMessagesByTags...SUCCESS
testFileCanBeTagged...SUCCESS
testTaggedObjectsCanBeRetrievedByType...FAILURE

Open the tests HTML report (test/reports/html/index.html) to get more detail on the reason for the failure as shown in the following screenshot:

Grails 1.1 Web Application Development

Our expectation was that there should be one item in the returned results; instead two items were returned—the tagged message and the tagged file.

This functionality may be useful to us in the future, but for now, we need to be able to search by a specific type. To solve this problem, we can create another withTag method that also takes a type. We end up with the following methods in Taggable:

def static withTag(String tagName) {
return withTag(tagName, Taggable)
}
def static withTag(String tagName, Class type) {
return type.withCriteria {
tags {
tag {
eq('name', tagName )
}
}
}
}

Now we can optionally specify a type that we wish to query. The default behavior, if no type is specified, is to perform a polymorphic query against Taggable. We can now add a withTag method onto the Message and File classes that will override the default behavior of the Taggable class so that our test passes. Add the following method to Message:

def static withTag(String tagName) {
return Taggable.withTag(tagName, Message)
}

And add the following method to File:

def static withTag(String tagName) {
return Taggable.withTag(tagName, File)
}

Now run the tests again and all should pass.

Exposing tagging to the users

The domain model is up and running so we can move on to allowing users to tag messages and files. In the first instance, we will allow users to tag messages and files when they are created. To do this, we need to make the following changes:

  • The GSPs that render the forms to submit messages and files must allow users to enter tags
  • The Taggable class needs to handle adding many tags in one go
  • The controllers must handle tags entered by the user
  • The FileService class must populate user tags on the File
  • The home page needs to render the tags that were added to each message and file

Add the Tags input field

We are going to allow users to input tags in free text and make them delimited by spaces. So we simply need to add a new text input field to each of the Post Message and Post File screens. The fieldset element in the message create.gsp becomes:

<fieldset>
<dl>
<dt>Title</dt>
<dd><g:textField name="title" value="${message.title}"
size="35"/>
</dd>
<dt>Message detail</dt>
<dd><g:textArea name="detail" value="${message.detail}"/></dd>
<dt>Tags</dt>
<dd><g:textField name="userTags" value="${userTags}"
size="35"/></dd>
</dl>
</fieldset>

While the fieldset for the file create.gsp becomes:

<fieldset>
<dl>
<dt>Title</dt>
<dd><g:textField name="name" value="${file.name}"
size="35"/></dd>
<dt>File</dt>
<dd><input type="file" name="data"/></dd>
<dt>Message detail</dt>
<dd><g:textArea name="description"
value="${file.description}"/></dd>
<dt>Tags</dt>
<dd><g:textField name="userTags" value="${userTags}"
size="35"/></dd>
</dl>
</fieldset>

Add multiple tags to Taggable

We can already store a list of tags against a Taggable class, but currently they must be added one at a time. For our users convenience, we really need to be able to handle adding multiple tags in one go. Let's create the addTags method on Taggable:

def addTags(String spaceDelimitedTags) {
tags = (tags)?:[]
tags.addAll(tagService.createTagRelationships(spaceDelimitedTags))
}

Once again, here we just delegate the logic to the TagService class.

Saving the users tags

The next step is to handle the new user input so that the tags will be persisted. The save action on MessageController is updated as shown below:

def save = {
def message = new Message(params)
message.addTags( params.userTags )
message.user = userService.getAuthenticatedUser()
if( !message.hasErrors() && message.save() ) {
flash.toUser = "Message [${message.title}] has been added."
redirect(action: 'create')
} else {
render(view: 'create',
model: [message: message, userTags: params.userTags])
}
}

We need to convert the user input, a space delimited list of tags, into our structured tagging model. This means we are not able to take advantage of the Grails data binding support and must add the tags manually through the addTags method that we created earlier. When there are validation errors, we must also make sure the tags entered by the user are made available on the page model so the tags are not lost when rendering the error messages.

In FileController, we must also make the users tags are available on the model when rendering validation errors. Change the current line in the save action from:

render(view: 'post', model: [file: file.currentVersion])

to:

render(view: 'post', model: [file: file.currentVersion,
userTags: params.userTags])

The call to addTags takes place in the saveNewVersion method in FileService:

def saveNewVersion( params, multipartFile ) {
def version = createVersionFile( params, multipartFile )
def file = applyNewVersion( params.fileId, version )
file.addTags( params.userTags )
file.save()
return file
}

Displaying tags

The last step for our basic tag handling is to display the tags to the users. To accomplish this we need to be able to get a list of tags as a string and then render the string representation on the home page. Add a read-only property implementation to Taggable:

def getTagsAsString() {
return ((tags)?:[]).join(' ')
}

We also need to override the toString implementation on Tagger:

public String toString() {
return tag.name
}

Open up the index.gsp file under views/home and add the following under the messagetitle div:

<div class="tagcontainer">
<g:message code="tags.display" args="${[message.tagsAsString]}" />
</div>

Then add the following under the filename panel:

<div class="tagcontainer">
<g:message code="tags.display" args="${[file.tagsAsString]}" />
</div>

Create the entry for tags.display in the message bundle file under i18n/messages.properties:

tags.display=tags: {0}

Now if we run the application, we should see that users are able to add tags to their messages and files:

Grails 1.1 Web Application Development

These tags can be displayed on the home page as shown in the following screenshot:

Grails 1.1 Web Application Development

Summary

In this article, we have seen how to construct a domain model to allow files and messages to be tagged. We used inheritance to enable tagging for the Message and File domain classes and saw how GORM supports persistence of inheritance structures to the database. Creating an inheritance structure in our domain classes allowed us to make use of polymorphic queries. In the next part of this article, we will look at how to customize our home page and work with the templates and tags.

Grails 1.1 Web Application Development Reclaiming Productivity for faster Java Web Development
Published: May 2009
eBook Price: $23.99
Book Price: $39.99
See more
Select your format and quantity:

About the Author :


Jon Dickinson

Jon Dickinson is the principal consultant and founder of Accolade Consulting Ltd. (http://www.accolade-consulting.co.uk) and can be contacted at jon@accolade-consulting.co.uk. He specializes in developing web applications on the Java platform to meet the goals of users in the simplest and least obtrusive way possible.

Books From Packt

 

Practical Plone 3: A Beginner's Guide to Building Powerful Websites
Practical Plone 3: A Beginner's Guide to Building Powerful Websites

WordPress Plugin Development: Beginner's Guide
WordPress Plugin Development: Beginner's Guide

Spring 2.5 Aspect Oriented Programming
Spring 2.5 Aspect Oriented Programming

Spring Web Flow 2 Web Development
Spring Web Flow 2 Web Development

Seam 2.x Web Development
Seam 2.x Web Development

Drools JBoss Rules 5.0 Developer's Guide
Drools JBoss Rules 5.0 Developer's Guide

Django 1.0 Website Development
Django 1.0 Website Development

Learning jQuery 1.3
Learning jQuery 1.3

 

Code Download and Errata
Packt Anytime, Anywhere
Register Books
Print Upgrades
eBook Downloads
Video Support
Contact Us
Awards Voting Nominations Previous Winners
Judges Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software
Resources
Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software