Building Plug-in using Grails
From ThemesWiki
| Official Page |
| Project Documentation |
| Download |
|
Contents |
[edit] The tagger plug-in
Grails plug-ins are developed in almost exactly the same way a regular application is. They can even be run as an application so that they can be tested independently from a real application. You can create a plug-in by using the create-plugin script in the command line. In your terminal window, go to the folder above the teamwork folder, where the application is present, and run:
grails create-plugin tagger
Notice that your plug-in has almost exactly the same structure as your application , as shown in the following screenshot:
The only difference is the presence of a plug-in descriptor file called TaggerGrailsPlugin.groovy. This is the main descriptor and configuration file for the plug-in. It contains version information, author details, and a number of hooks for Grails lifecycle events that your plug-in can participate in.
Fill in the version information and author details now:
def version = 0.1 //the version of the plugin def grailsVersion = GrailsPluginUtils.grailsVersion //the version of Grails the plugin is for def author = "<your_name>" def authorEmail = "<your_email>" def title = "Allow domain classes to be tagged" def description = '''\ Enable tagging of domain classes and provide UI tools to work with tags. ''' // URL to the plugin's documentation def documentation = "http://grails.org/Tagger+Plugin"
All of the lifecycle hooks that are available are optional. You will not need to use them for the first iteration of the plug-in, but they will be required later to make the plug-in more flexible.
[edit] Extract the tagging code
In the first instance, you will take all of the tagging specific code out of your application and put it into the Tagger plug-in. This means moving the following folders from the teamwork/grails-app folder into the tagger/grails-app folder:
- Controllers/tagging
- Domain/tagging
- Services/tagging
- Views/taggable
Now run the grails clean command and then try to run the Teamwork application. You should see lots of compile errors. This is because the domain classes for tagging have disappeared.
In order to allow the application to run again, you need to compile the Tagger plug-in and reference it from the Teamwork application. In the command line, go to the tagger folder and run:grails compile
To reference the Tagger plug-in from the Teamwork application, go to BuildConfig.groovy in the grails-app/conf folder. Make the following change:
grails.plugin.location.tagger = "../tagger" grails.project.plugins.dir="/tools/grails-plugins"
Run the Teamwork application again. This time the application should compile and start. However, if you try to log into the application you will see that our plug-in isn't quite working yet. You will see an error when the _file.gsp template tries to use the _showTags.gsp template. This is because you have moved the _showTags.gsp template to a different folder in the plug-in, and the application does not know where to find it.
[edit] Accessing plug-in templates through Tag Libraries
In order to make the templates available to applications, we will need to expose them in some way. The simplest way is to provide a Tag Library with the plug-in that is able to render the templates. Since the tag is within the context of the plug-in, it will know where to look for the templates.
So far you have used tags provided by the Grails framework and some plug-ins, but you have not needed to implement one. If you have worked with the JSP Tag Libraries, you are in for a pleasant surprise.
Tag libraries are defined by the convention that they must go in the grails-app/taglibfolder and must end in with TagLib Create the file tagger/TaggerTagLib.groovy in the grails-app/taglib folder in your Tagger plug-in project. Add the following code to the new Groovy class:
package tagger
class TaggerTagLib {
static namespace = 'tagger'
def showTags = { attrs ->
renderTemplate( '/taggable/showTags', attrs.bean )
}
def editTagsForm = { attrs ->
renderTemplate( '/taggable/editTagsForm', attrs.bean )
}
def renderTemplate( template, bean ) {
out << g.render( template: template,
model: ['taggable': bean],
contextPath: pluginContextPath )
}
}
Notice that this class has a static namespace property, two closure properties (showTags and editTagsForm), and a utility method called renderTemplate. Each closure property on a Tag library creates a tag definition that is callable from within a GSP. The namespace property defines the namespace that is used in the GSP to access the tag. The code shown above has created two new tags that can be called as follows:
<tagger:showTags bean="${message}"/>
<tagger:editTagsForm bean="${message}"/>
The <code>attrs parameter declared on each of the closures will contain a Map of all of the attributes defined when the tag is called from a GSP. You can see that both of the tags are expecting to receive an object as the value of the bean attribute.
The renderTemplate method is simply a utility that renders the specified template and lets Grails know that the context path of the template should be the same as the context path of the plug-in. This means that Grails knows to look for the template under the plug-in rather than in the main application.
Notice that the renderTemplate method makes a call to a render method on a g object. This is actually calling the Grails render tag, which we have used many times before, but only from a GSP. Grails makes all Tag libraries available to other Tag libraries by injecting an object for each Tag library namespace into all Tag library classes. Therefore, all Tag libraries that we implement will have an object available with the name of g that exposes each of the available Grails Tag libraries as closures. Now, restart the Teamwork application and you will be able to modify the _message.gsp and _file.gsp templates to use the new showTags Tag library, like so:
<tagger:showTags bean="${message}"/>
and
<tagger:showTags bean="${file}"/>
[edit] Calling tags from controllers
Your GSP templates are not the only places that need to render tag view and edit templates. The TaggableController, which we have moved into the plug-in, must also be able to render tags in the view and edit states. The current line of code that handles this rendering looks something like:
render(template: "editTagsForm", model: [taggable: taggable])
When the plug-in is loaded into the Teamwork application, the call to this render method will look for the template within the context of the application rather than the plug-in. This means that the editTagsForm template will not be found as we have moved it to our plug-in.
There are two approaches that we can take in order to deal with this. The first is to modify the existing call to the render method to include a plugin attribute. This is used by the render method to look up the location of the plug-in based on the name. At first glance, this looks like the simplest solution, but what happens if we need to change the name of our plug-in? We would need to find all of the places that we have statically referenced the name of the plug-in and change it to the new name. A better solution would be to reuse the Tag libraries that we have already created that do not hard code the plug-in name.
As with tags, Grails dynamically adds all tags to controllers by creating a property on the controller for each tag namespace that is defined. All of the tags declared within that namespace are then added to the object and can be called from within the controller. This means that your showTags and editTagsForm Tag libraries will be made available in the controller through the tagger property. In order to call the Tag libraries from the TaggableController, you will need to modify the actions in the following way:
def editTags = {
def taggable = Taggable.get(params.id)
render tagger.editTagsForm( bean: taggable )
}
def saveTags = {
def taggable = Taggable.get(params.id)
taggable.clearTags()
taggable.addTags(params.tags)
render tagger.showTags( bean: taggable )
}
def showTags = {
def taggable = Taggable.get(params.id)
render tagger.showTags( bean: taggable )
}
The highlighted code shows how to call the necessary tag libraries from within the controller.
[edit] Current limitations
We have a fully functioning plug-in that can be used to add tagging to any future Grails application. This is sufficient for now, but the current implementation does have one quite serious limitation. When using this plug-in, any domain class that is to support tagging must extent the Taggable class. This is quite a serious design flaw, as it greatly reduces the flexibility of future applications using the plug-in, because they will not be able to extend any other class. For this reason, we need to take a closer look at how other plug-ins enhance the behavior of domain classes without forcing the use of inheritance.
[edit] Packaging a plug-in
Before we improve the design of the plug-in, let's take a quick look at how we can package our plug-in. Go to the command line in the tagger folder and run:
grails package-plugin
This will create a distributable ZIP file called grails-tagger-0.1.zip. The filename is constructed from the name of the plug-in plus the current version of the plug-in. The plug-in can now be installed into any Grails application by specifying the filename of the plug-in when running the grails install-plugin command. For example:
grails install-plugin grails-tagger-0.1.zip
[edit] Using plug-in events
When we installed the Searchable plug-in to make a domain class searchable, we added a static property (static searchable = true) to the domain class and it became searchable. This approach is possible in Groovy and Grails because Groovy allows methods and properties to be added to classes at run-time. The Grails plug-in architecture provides hooks into the lifecycle of a plug-in so that dynamic behavior can be added on startup.
The goal for the rest of this tutorial is to convert the Taggable plug-in so that domain classes can be made taggable by defining the property (static taggable = true). You will achieve this through dynamically adding methods to domain classes at runtime.
[edit] Grails plug-in lifecycle events
Before moving on to the implementation details it is worth having a brief look at the lifecycle events that are made available by the Grails plug-in architecture.
There are two types of lifecycle events that are available to Grails plug-ins:
- Build events
- Runtime events
The following build events are available:
- Plug-in installation, through the
scripts/_Install.groovyscript file. - Plug-in upgrade, through the
scripts/_Upgrade.groovyscript file. - General scripting events, of which there are a large number of pre-defined events and a potentially infinite number of custom plug-in events. See the online Grails documentation for more details about these events: Hooking intoEvents. http://grails.org/doc/1.1/guide/single.html#4.3 Hooking intoEvents.
The available runtime events are:
- doWithSpring where the plug-in is able to inject objects into the Spring application context
- doWithWebDescriptor where the plug-in can participate in the dynamic generation of the web.xml file for the application
- doWithApplicationContext where the plug-in is able to perform some runtime configuration after the Spring application context has been created
- doWithDynamicMethods where the plug-in is able to add dynamic methods to classes within the application
- onChange where the plug-in can perform runtime operations based on changes to specified monitored resources
- onConfigChange where the plug-in can respond to changes in the configuration files at run time
Handlers for runtime events are provided in the *Plugin class in the root folder for the plug-in. In our case, this will be the TaggerGrailsPlugin.groovy file. To enable a plug-in to respond to each of the runtime events, a closure is declared with the relevant name in the *Plugin descriptor class.
[edit] doWithSpring
This closure takes no arguments and the contents of the closure will be executed against the Spring Bean Builder that is used by Grails in order to construct the Spring context of an application. The basics of this builder are:
- Each node represents the id of a bean in the Spring context
- Each node takes an argument that is the class of the object to be constructed
- Properties for the bean are defined within the node
For example, if we needed to manually inject a service into Spring that was a client wrapper around a remote service, then we might create a doWithSpring closure as shown below:
def doWithSpring = {
remoteService(OurServiceClient) {
endPoint = 'http://www.aservice.com'
}
}
This would register an object in the Spring context with the id of remoteService. When this object is constructed, it will be a new instance of the OurServiceClient class and the endPoint property will be set to http://www.aservice.com.
A full description of how to use the Spring Bean Builder can be found on the Grails site (http://grails.org/doc/1.1/guide/single.html#14.4) along with the explanation of the BeanBuilder DSL.
[edit] doWithWebDescriptor
This closure receives the web.xml file as an XMLSlurper GPathResult. The closure is then able to use the GPathResult to find and manipulate the necessary elements for the plug-in. This happens when the application is started up. The web.xml file cannot be modified as a result of this closure, only the resulting behavior a loaded application.
[edit] doWithApplicationContext
This closure receives the fully constructed Spring ApplicationContext (http://static.springframework.org/spring/docs/2.5.x/api/org/springframework/context/ApplicationContext.html). At this point, it is too late to modify the context. The aim of this closure is to perform runtime configuration based on the final state of the context.
[edit] doWithDynamicMethods
This closure allows plug-in developers to add dynamic behavior to applications. No other runtime lifecycle event handlers are allowed to add dynamic methods to Grails classes. This closure also receives a fully constructed Spring ApplicationContext, which allows us to create dynamic methods that can interact with the Spring context.
[edit] onChange and onConfigChange
Both of these events are fired in reaction to a watched resource being modified. In order to handle these events, the *Plugin class must declare a watchedResources property, which is either a String or a List of String objects. When one of these resources is changed and reloaded by Grails, either the onChange or the onConfigChange closure will be executed, depending on the type of the changed object.
def watchedResources = "file:./grails-app/controllers/*Controller.groovy"
def onChange = { event
//do something when a controller changes.
}
In the example above, all of the controller files are being watched for changes. The event object that is passed into the onChange and onConfigChange closures has the following properties:
- source the source of the event, being either a re-loaded class or a spring esource
- ctx the Spring ApplicationContext object
- plugin the plug-in object that manages the resource
- application the GrailsApplication object
We will implement the handler for the doWithDynamicMethods and doWithSpring events in order to remove the design constraint with the Tagging plug-in. For more details on how to use the other runtime events, take a look at the plug-in section in the Grails on-line documentation: http://grails.org/doc/1.1/guide/12.%20Plug-ins.html.
[edit] Inspecting Grails artifacts
Go back to the TaggerGrailsPlugin.groovy class in the root of your Tagger plug-in. This is where we had added author and version information previously. There are a number of closures defined in the body of this class, one for each of the runtime events that were described above.
Apart from the arguments declared, each of the closures has an instance of GrailsApplication (http://grails.org/doc/1.1/api/org/codehaus/groovy/grails/commons/GrailsApplication.html) injected into its scope. This is the key to inspecting specific Grails class types (for example, domain classes) in order to see if your plug-in should add dynamic behavior.
[edit] The GrailsApplication class
The GrailsApplication class represents a running Grails application. It can be used to return information regarding classes that match Grails conventions, as well as providing runtime configuration information. Each class within a Grails application is referenced by an instance of the GrailsClass interface. In order to see all the classes in a Grails application you can use the GrailsApplication interface in the following way:
application.allClasses.each { println it.name }
Here application is the instance of the GrailsApplication interface that is provided to all plug-in event handling closures. To see all of the classes that follow a particular convention in a Grails application, you can use the *Classes dynamic method convention:
application.controllerClasses.each { println it.name }
The following table defines the dynamic method conventions that are available in the GrailsApplication class:
| Dynamic Method | Description |
|---|---|
*Classes
| All classes of a particular type, for example, domainClasses
|
get*Class
| Retrieves a GrailsClass instance for the short name of a class for example, getControllerClass('MyController'). [ Note - I am pretty sure the argument should be the fully package qualified name of the controller class. For details see 'discussion' panel / Chris ]
|
is*Class
| Determines if a Class is of a particular artifact type for example, isControllerClass( MyController.class )
|
add*Class
| Adds a class for a particular artifact type for example, addControllerClass( MyController.class )
|
[edit] Find Taggable domain classes
Now that we can list all of the classes that are adhering to a particular Grails convention, it is time to check each domain class to see if it has thetaggable property. Open the
TaggerGrailsPlugin.groovy class and add the following implementation to the
doWithDynamicMethods closure:
def doWithDynamicMethods = { ctx ->
application.domainClasses.each { domainClass ->
def isTaggable = GrailsClassUtils.getStaticPropertyValue(
domainClass.clazz, 'taggable' )
if( isTaggable ) {
println "Make ${domainClass} taggable"
}
}
}
In the code above, we iterate over all of the domain classes, and retrieve the value of the static property taggable for each class. If a class does not have a static property with the given name, then GrailsClassUtils will return null. For the moment, if the domain class has the taggable property set, then we just print the name of the class. Notice that we are using a utility class provided by Grails called GrailsClassUtils. This class has lots of useful methods for finding out information about classes in Grails. We must be sure to import it into the plug-in class:
import org.codehaus.groovy.grails.commons.GrailsClassUtils
Now, add the taggable property to the Message and File domain classes and restart the application. The following output will be displayed in the console:
Make Artefact > Message taggable Make Artefact > File taggable
[edit] Re-modeling tag relationships
It is necessary to re-model the relationship between a domain object and its tags as we wish to remove the dependency on the Tagger superclass. Create a new package called tagger in the Tagger plug-in in the grails-app/domain folder to work on the new domain structure. We need a new class to represent tags in the new structure. Create the class TagData as shown below:
package tagger
class TagData {
String name
static constrains = {
name( blank: false )
}
}
We also require a class to store the relationship between tags and domain objects, as before. Create the new TagRelationship domain class shown as follows:
package tagger
class TagRelationship {
long taggedItem
Class taggedType
TagData tag
Date dateCreated
static constraints = {
tag( nullable: false )
}
public String toString() {
return tag.name
}
}
This looks very similar to the previous incarnation of the domain class that held relationships between tags and domain classes. The big difference is the two new properties:
- taggedItem this will store the id of the domain object that has a particular tag (for example, the id of a
Message). - taggedType this holds the
Classof the domain object that is related to the tag (for example,File).
These properties should provide all the information you need to be able to retrieve tags for a given domain object and to retrieve the domain objects for a particular tag.
[edit] Adding Taggable behavior
It is time to look at adding the dynamic behavior to the domain classes. Previously, by virtue of extending the Taggable class, domain classes would inherit the following:
- tags the property containing a list of the tags that the domain class instance was associated with
- tagsAsString a property to return tags for a domain class instance as a space delimited String
- addTags allows a list of tags to be added to a domain class instance
- addTag allows a single tag to be added to a domain class instance
- hasTag determines if a class has a particular tag
- clearTags removes all of the tags from a domain class instance
- withTag retrieves all of the instances of either all of the children of
Taggableor a given child type that had a specific tag - withTags retrieve all of the instances of any child of
Taggablethat has any one of a supplied list of tags
To add the properties and methods dynamically, you will have to create a Groovy class to take responsibility for applying the dynamic behavior to a particular domain class. Create the tagger package under the src/groovy folder and a new Groovy class called TaggablePrototype under this package.
There is quite a lot of code to rewrite to add the properties and methods dynamically to our taggable classes, so let's take it in small steps.
[edit] Groovy MetaClass
Before you start adding dynamic behavior to the domain classes, it is necessary to understand a little more about metaprogramming in Groovy. We saw in Tutorial 4 that all Groovy classes are constructed by the GroovyClassGenerator, which is responsible for adding the GroovyObject interface, along with an implementation of this interface to your class. The methods provided by the GroovyObject interface are:
-
invokeMethod -
getProperty -
setProperty -
getMetaClass -
setMetaClass
Calling the getMetaClass method on the class retrieves the MetaClass implementation for a class. There is a specific implementation of the MetaClass interface called ExpandoMetaClass, which is returned by default. The beauty of ExpandoMetaClass is that it provides an incredibly simple syntax for adding dynamic methods and properties to a class. By creating a simple test class, we can see some examples of how to use ExpandoMetaClass to dynamically add the following to a class:
- Read-only properties
- Read/write properties
- Simple stateless methods
- Methods that interact with the state of the object
In the tagger plug-in, in the test/unit folder, create a groovy unit test class called MetaProgrammingTests under a package called metatest, shown as follows:
package metatest
class MetaProgrammingTests extends GroovyTestCase {
// tests to go here
}
class ExtendMe {
def myName
}
The ExtendMe class is a simple class that we will add dynamic behavior to during the tests. Add the following test that demonstrates how to add a read-only property to the ExtendMe class:
public void testAddingAReadOnlyProperty() {
ExtendMe.metaClass.getInterest = {->return 'skiing'}
def extended = new ExtendMe()
assertEquals( 'skiing', extended.interest )
assertEquals( 'skiing', extended.getInterest() )
}
In the example above, we get access to the MetaClass instance for the ExtendMe class and add a read-only property called interest to the class. A closure, that returns the String 'skiing', is assigned to the interest property on the ExtendMe instance and is executed when this property is called.
|
It is critical to specify that the closure has no arguments through use of the -> notation, otherwise Groovy will not recognize the method as a property when it is added to the class. |
The following code shows how to add a read/write property dynamically to a class:
public void testAddingAReadWriteProperty() {
ExtendMe.metaClass.interest = null
def extended = new ExtendMe()
assertNull( extended.interest )
extended.interest = 'shopping'
assertEquals( 'shopping', extended.interest )
}
You simply have to assign a value to the property that you wish to add on the MetaClass of the class and the property will be created with the specified default value.
|
When adding properties in this manner beware that the property is stored in a |
The following example shows how to add a stateless method to an object:
public void testAddingAMethod() {
ExtendMe.metaClass.greet = { name ->
def greeting = "Hi ${name}, how are you?"
println greeting
return greeting
}
def extended = new ExtendMe()
assertEquals( 'Hi fred, how are you?', extended.greet( 'fred' ) )
}
The syntax for adding a method is the same as adding a read-only property. In the example above, we have specified that the greet method should take a single name argument.
The last example will add another method that can interact with the existing state of the class that it is added to:
public void testAddingMethodToInteractWithObjectState() {
ExtendMe.metaClass.introduceTo = { name ->
def introduction = "Hi ${name}, my name is ${delegate.myName}."
println introduction
return introduction
}
def extended = new ExtendMe( myName: 'Jon' )
assertEquals( 'Hi fred, my name is Jon.', extended.introduceTo( 'fred' ) )
}
In the example above, the definition of the introduceTo method uses the object represented by the delegate property to get the value of the myName property from our extended object. All closures have a delegate property created within their scope. This is, by default, the declaring context of the closure. However, the ExpandoMetaClass sets the delegate property for closures that represent methods on an object to the object itself. This is so that we can interact with the state of the object.
That is the introduction you need for Groovy metaprogramming to be able to add dynamic behavior to your taggable domain classes. So let's move on to the implementation.
[edit] Getting the home page working
The first thing we need to do is to remove the dependency on the old style of making a domain class taggable and switch over to the new style. Stop the Message and File domain classes from extending the Taggable class and remove the withTag implementations from each of these classes shown as follows:
package app
class Message {
static taggable = true
//properties and constraints will remain the same
}
And your <code>File</code> domain class:
package app
class File {
static taggable = true
//properties and constraints will remain the same
}
At this point, if we try running the application everything will be broken! To fix the application to the point where users can log in and go to the home page, we need to:
- Allow tags to be added to a taggable class using the
addTagmethod, this is required by theBootstrapclass - Add the
hasTagmethod, required by theaddTagmethod - Add the
tagsproperty, required by thehasTagmethod - Add the
tagsAsStringproperty so that tags can be rendered on the home page
Let's take a look at the first pass of TaggablePrototype that will add these methods onto the domain classes. Create the TaggablePrototype.groovy file in the tagger/src/groovy/tagger folder and add the code shown below:
package tagger
class TaggablePrototype {
def addMethodsTo(domainClass) {
ExpandoMetaClass metaClass = domainClass.metaClass
//add properties
metaClass.getTags = getTags
metaClass.getTagsAsString = getTagsAsString
//add instance methods
metaClass.hasTag = hasTag
metaClass.addTag = addTag
}
def getTags = {->
def taggedItem = delegate
return TagRelationship.withCriteria {
eq('taggedItem', taggedItem.id)
}.findAll { tagRelationship ->
tagRelationship.taggedType == taggedItem.class
}
}
def getTagsAsString = {->
return delegate.tags.collect {it.tag.name}.join(' ')
}
def hasTag = {String tagName ->
return delegate.tags.find {it.tag.name == tagName}
}
def addTag = {String tagName ->
def tag = TagData.findByName(tagName)
if( !tag ) {
tag = new TagData(name: tagName).save()
}
if( !delegate.hasTag(tagName) ) {
def tagRel = new TagRelationship(tag: tag, taggedItem: delegate.id, taggedType: delegate.class)
tagRel.save()
}
}
}
The first method in the listing (addMethodsTo) determines which methods are added dynamically to the provided class. You can see that two properties (tags and tagsAsString) and two instance methods (addTag and hasTag) are being added. All of the closures that are to be used as implementations for the dynamic methods and properties are declared separately as properties of the TaggablePrototype class. This allows us to separate adding the dynamic behavior from the implementation of the dynamic behavior.
The first dynamic property we provide is tags, which allows the object to return its list of tags. The behavior of the tags property is implemented by the getTags closure. This closure queries the TagRelationship class for all of the items where the taggedItem property equals the id of the delegate object. It then filters out any TagRelationship instances that are for a different class other than the delegate object. Notice that the delegate object has been assigned to another variable (taggedItem). This is necessary because we need to have access to the delegate object for the getTags closure inside the withCriteria closure. If you try accessing the delegate property from within the withCriteria closure, then you will find that it is set to HibernateCriteriaBuilder. This is because the withCriteria closure uses the HibernateCriteriaBuilder.
The next property (tagsAsString) is much simpler, but it relies on the existence of the tags property. It simply calls the tags property on the delegate, collects all of the names of the tags and then joins them into a space-delimited string.
The hasTag method is declared next. This method takes a tag as a string and checks if the tag exists in the list of tags for the delegate object.
Finally, the addTag method is declared. It checks if the tag name exists and creates the tag if it does not. Then the delegate is checked to make sure it does not have the specified tag already. If the delegate does not have a relationship with the tag, a new relationship is created.
We now have a class that is capable of adding dynamic tagging behavior to the domain classes. The next step is to use this class in the plug-in initialization. Go to the TaggerGrailsPlugin class and update the doWithDynamicMethods in the following way:
def doWithDynamicMethods = { applicationContext ->
def prototype = new TaggablePrototype()
application.domainClasses.each { domainClass ->
def isTaggable = GrailsClassUtils.getStaticPropertyValue ( domainClass.clazz, 'taggable' )
if( isTaggable ) {
prototype.addMethodsTo( domainClass )
}
}
}
Don't forget to import the TaggablePrototype class:
import tagger.TaggablePrototype
Now, restart the Teamwork application. Users will be able to log in and see the bootstrap data on the home page with any tags being displayed. This is shown in the following screenshot:
Unfortunately, that is about as far as you will be able to go. We now need to go through and add support for the features provided in the rest of the application.
[edit] Items of Interest
Currently, if a user attempts to register interest in a tag it will have no effect. That is, nothing will be displayed on the home page in the Items of Interest section. Here are the steps necessary to get this feature working again with the new tagging plug-in:
- Delete the tagging package under the domain folder for the plug-in. This will create errors making it easier to spot where the old domain classes are being used.
- Modify the structure of the
Userdomain class so that watched tags can be handled in the new structure. - Add a method to the existing the
TagServiceclass in the plug-in that will allow applications to query all of the tagged items for a set of tags. - Update the
ContentServiceclass in the Teamwork application to use the new method on theTagServiceclass.
The first task is fairly straightforward. The second task, to remodel the User domain class, is a bit trickier. We need to implement a unidirectional many-to-many relationship between the User class and the TagData class. We don't want to use the TagRelationship class to do this, as we could end up getting users in our results when searching for messages and files via tags. Therefore, in order to create the relationship, we need to create a new WatchedTag domain class in the Teamwork application:
package app
import tagger.TagData
class WatchedTag {
TagData tag
public String toString() {
return tag.name
}
}
Once this class is in place, make the changes to User, as shown below:
package app
import tagger.TagData
class User {
static hasMany = [watchedTags: WatchedTag]
//no change to the properties
static constraints = {//no change}
def needToHash(String password) {//no change}
def overrideTags(String tags) {
watchedTags.each {it.delete()}
watchedTags = []
watchedTags.addAll( tags?.split(' ')?.collect {
tagName ->
def tagData = TagData.findByName( tagName )
?:new TagData( name: tagName )
new WatchedTag( tag: tagData )
} )
}
def getTagsAsString() {//no change}
def getTags() {
return (watchedTags?:[]).collect{it.tag}
}
}
We have made two basic changes here. We have made the watchedTags property a relationship to the new WatchedTag class. We have had to re-implement the overrideTags method to work with this new class.
Let's now add the new method to the TagService class that will allow us to query all of the taggable items that are related to one or more of the entries in the tags list. Also, we no longer need the createTagRelationship methods that were previously on the service, so let's remove them.
package tagging
import tagger.TagRelationship
class TagService {
boolean transactional = true
def allWithTags( tags ) {
return TagRelationship.withCriteria {
inList( 'tag', tags )
}.collect { it.taggedType.get( it.taggedItem ) }
}
def cloudData(type) {//no change yet}
}
The allWithTags method above provides the implementation necessary to find all tagged items of any type that have a relationship to one of the tags supplied. The withCriteria method is called on the TagRelationship class to return all TagRelationship instances that have a tag in the supplied list. The tagged item is then collected from each of the matching TagRelationship instances. Remember that both the class of the tagged item and the id of the tagged item are stored in the TagRelationship class.
Finally, we must update the allWatchedItems method on the ContentService class in the Teamwork application as shown below:
class ContentService {
def userService
def tagService
def allWatchedItems() {
def watchedTags = userService.authenticatedUser.tags
return watchedTags ?
tagService.allWithTags( watchedTags ) : []
}
//no other changes...
}
When the application is restarted, users will be able to set the tags they wish to watch again.
[edit] Create messages and files
Now onto making the create messages and files features work again. You will need to:
- Make the addTags method part of the dynamic behavior of a Taggable class.
- Make sure that the tags are added to messages and files after the object has been saved, because you need the id of the object to be populated before it can have a TagRelationship
To make the addTags method part of our taggable behavior, you will need to create a closure in the TaggablePrototype class that implements the behavior of the addTags method. Add the following closure property to the TaggablePrototype class:
def addTags = {String spaceDelimitedTags ->
if( spaceDelimitedTags ) {
spaceDelimitedTags.split(' ').each {
addTag(it)
}
}
}
Now update the addMethodsTo method to add the new behavior to the Taggable domain classes:
def addMethodsTo(domainClass) {
ExpandoMetaClass metaClass = domainClass.metaClass
//add properties
metaClass.getTagsAsString = getTagsAsString
metaClass.getTags = getTags
//add instance methods
metaClass.addTag = addTag
metaClass.hasTag = hasTag
metaClass.addTags = addTags
}
The last change is to update MessageController and FileService to make the call to addTags. This takes place after the message or file object has been successfully saved. Remember that the TagRelationship class has the property taggedItem. This is the hibernate-generated id of the taggable object. If the addTags method is called before the taggable object is saved, the id property will not be populated and an invalid id of 0 will be given to the TagRelationship instance.
Here is the updated save action for MessageController:
def save = {
def message = new Message(params)
message.user = userService.getAuthenticatedUser()
if (!message.hasErrors() && message.save()) {
message.addTags(params.userTags)
flash.toUser = "Message [${message.title}] has been added."
redirect(action: 'create')
} else {
render(view: 'create', model: [message: message,
userTags: params.userTags])
}
}
And the updated <code>saveNewVersion</code> method for <code>FileService</code>:
def saveNewVersion( params, multipartFile ) {
def version = createVersionFile( params, multipartFile )
def file = applyNewVersion( params.fileId, version )
file.save()
file.addTags( params.userTags )
return file
}
[edit] Update tags
You may have noticed that the in-line editing that allows users to update tags is also broken now. This is because we are calling the GORM get method on the Taggable class in order to retrieve an instance of either Message or File. We have now deleted the Taggable class, so we need a mechanism that will allow us to get an instance of the correct class for a given id. We can solve this problem by registering a lookup table in Spring that uses the short name of the taggable domain object as the key and the Class of the object as the value. Requests to plug-in actions can then pass a key for the lookup table and the TaggableController will use the supplied key to determine which object type to use.
This means you get to see how to make a plug-in add new objects to the Spring context on start up. First though, modify the doWithDynamicMethods closure in the TaggerGrailsPlugin class as follows:
def doWithDynamicMethods = { applicationContext ->
def prototype = new TaggablePrototype()
def taggedClasses = applicationContext.getBean('taggedClasses')
application.domainClasses.each { domainClass ->
def isTaggable = GrailsClassUtils.getStaticPropertyValue(
domainClass.clazz, 'taggable' )
if( isTaggable ) {
prototype.addMethodsTo( domainClass )
taggedClasses[domainClass.shortName] = domainClass.clazz
}
}
}
You can see that we pull an object from the Spring application
context with the name taggedClasses. This object is the lookup
table and is implemented as a Map.Each domain class that is marked
as taggable is added to the map with a key determined
by the short name of the class.
How did the taggedClasses Mapget into the Spring context?
def doWithSpring = {
taggedClasses(HashMap)
}
You need to modify the doWithSpring closure to register a HashMap in the Spring context with the name taggedClasses. When the plug-in is started, the doWithSpring closure is executed against the Spring Bean Builder that Grails uses to create the Spring context for an application.
The next change is to stop the TaggableController actions from using the Taggable class and instead use the taggedClasses lookup table.
package tagger
import grails.converters.JSON
class TaggableController {
def taggedClasses // injected by Spring
def editTags = {
def taggable = taggableFromType( params )
render tagger.editTagsForm( bean: taggable )
}
def saveTags = {
def taggable = taggableFromType( params )
taggable.clearTags()
taggable.addTags(params.tags)
render tagger.showTags( bean: taggable )
}
def showTags = {
def taggable = taggableFromType( params )
render tagger.showTags( bean: taggable )
}
def suggestTags = { }
def renderAsXml(tags) { }
def taggableFromType( params ) {
def clazz = taggedClasses[ params.type ]
return clazz.get( params.id )
}
}
As with services, the taggedClasses object will be injected because its property has the same name as the object in the Spring context. Each action that used to call Taggable.get( params.id ) has been updated to load the taggable object through the taggableFromType> utility method. This method takes the request paramsmap and assumes a parameter called type, which contains the key to a taggable class has been sent on the request. The Class that is retrieved from the taggedClasses map is then used to load the taggable object from the database.
The last step to hook up this mechanism is to make sure that all of the links to these actions will send the type parameter. Modify the _showTags template as follows:
<%@ page import="org.codehaus.groovy.grails.commons.DefaultGrailsDomainClass" %>
<%
def taggableShortName = new DefaultGrailsDomainClass( taggable.class ).shortName
def moreParams = [type: taggableShortName]
%>
<span class="tags">${taggable.getTagsAsString()}</span>
<g:remoteLink controller="taggable" action="editTags"
id="${taggable.id}" update="tags${taggable.id}"
params="${moreParams}">edit</g:remoteLink>
And the <code>_editTagsForm</code> template:
<%@ page import="org.codehaus.groovy.grails.commons.DefaultGrailsDomainClass" %>
<%
def taggableShortName = new DefaultGrailsDomainClass( taggable.class ).shortName
def moreParams = [type: taggableShortName]
%>
<g:formRemote name="tagForm" url="[controller:'taggable',action:'saveTags']"
update="tags${taggable.id}" class="inlineedit">
<input type="hidden" name="id" value="${taggable.id}"/>
<input type="hidden" name="type"
value="${taggableShortName}"/>
<fieldset>
<dl>
<dt>Tags</dt>
<dd>
<richui:autoComplete name="tags"
action="${createLinkTo('dir': 'taggable/suggestTags')}"
delimChar=" " value="${taggable. getTagsAsString()}"
forceSelection="false"/>
</dd>
</dl>
</fieldset>
<input type="submit" value="Save"/> |
<g:remoteLink controller="taggable" action="showTags"
id="${taggable.id}" update="tags${taggable.id}"
params="${moreParams}">Cancel</g:remoteLink>
</g:formRemote>
The important part about both of these templates is the code at the top that extracts the key to be sent on the type parameter from the taggable object. An instance of DefaultGrailsDomainClass is instantiated, passing in the taggable object. An instance of this class represents a domain class in Grails and implements the GrailsClass interface. Therefore, you can be sure that it will extract the shortName from our taggable object in the same way as the plug-in initialization code.
The final change we need to make is adding the clearTags method to our taggable domain classes. In TaggablePrototype class, add the following closure property:
def clearTags = {
delegate.tags.each { tagRel ->
tagRel.delete( flush: true )
}
}
Make sure this implementation is bound to the clearTags
method on taggable classes by adding the following highlighted code:
def addMethodsTo(domainClass) {
ExpandoMetaClass metaClass = domainClass.metaClass
//add properties
metaClass.getTagsAsString = getTagsAsString
metaClass.getTags = getTags
//add instance methods
metaClass.addTag = addTag
metaClass.hasTag = hasTag
metaClass.addTags = addTags
metaClass.clearTags = clearTags
}
Not only have we fixed in-line editing for the new plug-in structure, but we have also seen how a plug-in can register objects into the Spring context. Now, let's take a look at listing messages and files.
[edit] List messages and files
If you remember, there are two actions related to listing messages and files. The first is to list all by last modified date, while the second is to list a filtered subset by a tag selected from the tag cloud. The problem we have when listing all of the messages or files is that a tag cloud is displayed on that page. So we need to modify the implementation of the cloudData method on the TagService to handle our new domain structure.
def static cloudData( type ) {
def data = [:]
Tag.list().each { tag ->
def count = TagRelationship.withCriteria {
eq( 'tag', tag )
}.findAll { it.taggedType == type }.size()
if( count ) {
data.put( tag.name, count )
}
}
return data
}
This method simply iterates over all of the tags in the application, counting how many TagRelationship instances with the specified type exists for each tag. The logic is the same as before, just updated for the new tagger domain structure. This change should be enough to get the list pages working for messages and files.
To enable filtering by a specific tag requires the following changes:
- Two new TagService methods, one to return all of the items with a single specified tag and the other to return all of the items of a particular type, that have a single specified tag
- Update the MessageController class to use the new service methods
- Update the FileController class to use the new service methods
The two new service methods are very simple and are built on the existing method of finding tagged items, given a list of tags:
package tagging
class TagService {
def allWithTags( tags ) { }
def allWithTag( String tagName ) {
return allWithTags( [TagData.findByName( tagName )] )
}
def withTag( String tagName, Class type ) {
return allWithTag( tagName ).findAll {
it.class == type
}
}
def cloudData(type) { }
}
Now, we must update the filterByTag implementation to use TagService:
package app
import tagger.Tag
class MessageController {
def tagService
def contentService
def userService
def post = { }
def save = { }
def list = { }
def filterByTag = {
def messages = tagService.withTag(
params.selectedTag, Message )
.sort( contentService.lastUpdatedSort )
render(view: 'list', model: [
messages: messages,
tagCloudData: tagService.cloudData(Message)])
}
}
The updates to FileController are very similar:
package app
class FileController {
def tagService
def contentService
def fileService
def post = { }
def save = { }
def download = { }
def newVersion = { }
def list = { }
def filterByTag = {
def files = tagService.withTag(
params.selectedTag, File )
.sort( contentService.lastUpdatedSort )
render(view: 'list', model: [
files: files,
tagCloudData: tagService.cloudData(File)])
}
}
[edit] RSS
Finally, the RssController must be modified to allow the RSS feed to continue to work with the new tagger plug-in. Rather than relying on polymorphic querying by simply calling Taggable.list(), it is necessary to build the list from the individual classes that are to be published via RSS. The RssController must be modified as follows:
package app
class RssController {
def contentService
def index = {
def content = Message.list()
content.addAll( File.list() )
content.sort( contentService.lastUpdatedSort )
RssSupport rssBuilder = new RssSupport( content: content )
render( contentType: 'text/xml', rssBuilder.build )
}
}
[edit] Source
The source of this content is Chapter 13: Building Plug-in using Grails of Grails 1.1 Web Application Development by Jon Dickinson (Packt Publishing, 2009).
