Monday, February 28, 2011

How to support custom artifact type in Griffon 2nd revision

Last year June I wrote a blog post about how to support custom artifact type in Griffon framework. Now Griffon 0.9.2 release is just around the corner. In this release some major refactoring was done with the Artifact API, that's why I am updating this guide to match the new API.

Out-of-box Griffon supports 4 different types of artifacts: Model, View, Controller (MVC) plus Service, these are the major building blocks of any Griffon application. Just like it's cousin Grails, in Griffon plugins and addons can also introduce new artifact types, however the process is fairly different from what Grails employs. As part of Griffon Validation Plugin I implemented the support for a new custom artifact type - Constraint, and I would like to share some of my learning here so it would be a little bit easier if you are planning to do something similar.

Step 1 - Handle your artifact

To support a new artifact type you have to tell the Griffon core about the artifact type first. You can achieve this by implementing your own ArtifactClass and ArtifactHandler, for most of the common cases extending from DefaultGriffonClass and ArtifactHandlerAdapter should be enough. Here is what it looks like for the Constraint artifact type:

public interface GriffonConstraintClass extends GriffonClass {
/** "constraint" */
String TYPE = "constraint";
/** "Constraint" */
String TRAILING = "Constraint";
}

public class ConstraintArtifactHandler extends ArtifactHandlerAdapter {
public ConstraintArtifactHandler(GriffonApplication app) {
super(app, GriffonConstraintClass.TYPE, GriffonConstraintClass.TRAILING);
}

protected GriffonClass newGriffonClassInstance(Class clazz) {
return new DefaultGriffonConstraintClass(getApp(), clazz);
}
}

public class DefaultGriffonConstraintClass extends DefaultGriffonClass implements GriffonConstraintClass {
public DefaultGriffonConstraintClass(GriffonApplication app, Class clazz) {
super(app, clazz, GriffonConstraintClass.TYPE, GriffonConstraintClass.TRAILING);
}
}
One note of caution on the artifact class and it's handler:
I ran into some problem when implementing them in Groovy initially, and had to change all implementation to Java instead. Thanks to Andres for the investigation and input on this discovery.

Step 2 - Register your artifact

As shown above, if you have experience working with Grails artefact support, you will notice the handler implementation is almost identical in Griffon, however things starting to differ from this point forward. Now you have the handler implemented, next thing is to register it with Griffon core. This is best achieved during initialization phase in your addon. Open the [PluginName]GriffonAddon.groovy file add the following callback if its not already there:


def addonInit = {app ->
....
app.artifactManager.registerArtifactHandler(new ConstraintArtifactHandler(app))
....
}


Step 3 - Find your artifacts

Now we have the new artifact type registered, next step is to tell Griffon where to find the artifacts. Ever wonder how Griffon knows to look under griffon-app/models for model classes? This is what we are going to do in this step. This is also where Griffon custom artifact support truly differs from Grails. Instead of handling it at run-time, Griffon chooses to handle this at build time, so to achieve this you need to tap into the Griffon event model. Open the _Events.groovy script and implement the following event listener:


eventCollectArtifacts = { artifactsInfo ->
if(!artifactsInfo.find{ it.type == 'constraint' }) {
artifactsInfo << [type: 'constraint', path: 'constraints', suffix: 'Constraint'] } }


This event listener will tell Griffon to look for anything under griffon-app/constraints folder with suffix 'Constraint' and register them as constraint artifacts.

Step 4 - Measure your artifacts

Now we pretty much have the basic bolts and nuts in place, its time to make our newly found artifact type to be more integrated with Griffon as any other first class artifact types do. One of the nice feature of Griffon is the stats command, it gives you an instant overview of how big your app is in terms of how many files and Line of Code per type including artifacts. Won't it be nice to have it also display the metrics about our own custom artifacts? fortunately its actually pretty easy to achieve in Griffon, similar to the previous step we will add another listener to the _Events script.


eventStatsStart = { pathToInfo ->
if(!pathToInfo.find{ it.path == 'constraints'} ) {
pathToInfo << [name: 'Constraints', path: 'constraints', filetype: ['.groovy','.java']] } }


Griffon event model is a very powerful concept, usually when I am not sure how to do something funky in Griffon this is the first place I look.

Step 5 - Automate your artifacts

One of big selling point of the next generation Rails-like RIA framework is the ability to create any artifact simply by using one of the built-in command, for example grails create-controller or griffon create-mvc. To make our new artifact type a true first-class citizen of Griffon, of course we need all the bells and whistles. To add a new command to Griffon, you need to create a new Groovy script under scripts folder:

CreateConstraint.groovy


includeTargets << griffonScript("_GriffonInit")
includeTargets << griffonScript("_GriffonCreateArtifacts")

target('default': "Creates a new constraint") {
depends(checkVersion, parseArguments)

promptForName(type: "Constraint")

def name = argsMap["params"][0]
createArtifact(name: name, suffix: "Constraint", type: "Constraint", path: "griffon-app/constraints")
createUnitTest(name: name, suffix: "Constraint")
}


Like other convention-over-configuration framework, Griffon relies heavily on simple naming conventions, so in the script make sure you naming everything consistent to avoid unnecessary complexity. This script will create artifact for the type of Constriant and related unit test case, as you can see it will be a simple matter to create integration test case if need be.

Now with the command in place, you can finally provide the template for the new artifact being created. Again naming convention is being used to determine where to find template file, for our example the template file should be placed under src/templates/artifacts and named Constraint.groovy:


@artifact.package@class @artifact.name@ {

def validate(propertyValue, bean, parameter) {
// insert your custom constraint logic
}

}


Phew, now finally we are done, hope I did not miss anything :) This is a long post and as you can see a lot of plumbing; this is exactly why currently there are some discussion going on within Griffon dev mail list to provide declaration based custom artifact support either using Grails style or Groovy AST transformation, so stay tuned for future updates on this topic.

No comments: