Sections

  1. Basic Artefacts
  2. Dependencies
  3. Configuration
  4. Domain Classes
  5. User Interface

Grails Plugin Development Best Practices

This is a guide to best practices for developing Grails plugins. (This guide is currently a work in progress, and may be inaccurate!)

Basic Artefacts

Providing Grails Scripts

Grails plugins can provide scripts that a user can execute from the Grails console. More information on providing scripts can be found in the Providing Basic Artefacts section of the Grails user guide.

Provide Usage statements with Grails scripts

Plugins that provide Grails scripts should always include a usage statement. Users will be able to access this information by using the 'grails help' command. You can provide a usage statement by assigning a String to the 'USAGE' script variable.

USAGE = """
quick-start [--prefix=PREFIX]
where
PREFIX = The prefix to add to the names of the realm and
domain classes.
"""
target(default: "Task description ...") {
…
}

Exiting a Grails script

If you need to exit a Grails script early, call the exit() method. Do NOT call System.exit() - this will kill Grails interactive mode.

Script Scoping

It is important that code is contained within your script's target closure. Code outside of a target closure will be executed during build script evaluation.

target(doSomething: "Task description ...") {
    mySharedFunction(argsMap["repo"], 50)
}

Promoting Code Re-use

includeTargets << new File(myPluginDir, "scripts/_CmdShared.groovy")
CmdOne.groovy
CmdTwo.groovy
_CmdShared.groovy
=> grails cmd-one
grails cmd-two
grails cmd-shared (strikethrough)

The class loading question

Script compile Direct import => Ooops, class not found! Your custom class hasn't been compiled and isn't on the classpath!

target(customTask: "...") {
    depends(compile)
    def myCustomInstance = classLoader.loadClass("org.example.MyUtil").newInstance()
    myCustomInstance.doSomething()
}

Displaying Information to Users

Grails plugins have a few options for displaying information on a user's terminal.

Getting user input

Grails has provided support for gathering user input from the command line since 1.3, through the use of the CommandLineHelper class.

def inputHelper = new CommandLineHelper()
username = inputHelper.userInput( "Please enter username for repository:" )
doCommit = inputHelper.userInput( "Commit code?", ["y", "N"] as String[] )

Dependencies

Three rules:

– export = false – excludes – transitive = false

Example: Shiro

dependencies {
    compile 'org.apache.shiro:shiro-core:1.1.0',
    'org.apache.shiro:shiro-web:1.1.0',
    'org.apache.shiro:shiro-ehcache:1.1.0',
    'org.apache.shiro:shiro-quartz:1.1.0',
    'org.apache.shiro:shiro-spring:1.1.0', {
        excludes 'ejb', 'jsf-api', 'jms',
        'connector-api', 'ehcache-core', 'slf4j-api'
    }
}

Example: Spring Security Core

dependencies {
    compile 'org.springframework.security:spring-security-core:3.0.5.RELEASE', {
        transitive = false
    }
    compile 'org.springframework.security:spring-security-web:3.0.5.RELEASE', {
        transitive = false
    }
}
plugins {
    build ':release:1.0.0.RC3', {
        export = false
    }
}

Dependency DSL vs dependsOn

Configuration

Should I read from Config or BuildConfig?

What if I want a setting in both?

Example: Cloud Foundry plugin might need username & password for deployment and at runtime

Copy relevant properties


scripts/_Events.groovy
eventPackageAppEnd = {
def propsFile = new File(grailsSettings.classesDir, "dummy-plugin.properties")
propsFile.withWriter("UTF-8") { writer ->
def config = grailsSettings.config.grails.plugin.cloudfoundry
def props = config.collectEntries { k, v ->
["grails.plugin.cloudfoundry.$k", v]
} as Properties
props.store writer, "Dummy plugin build properties"
}
}

Load properties in plugin descriptor DummyGrailsPlugin.groovy

class DummyGrailsPlugin {
    …
    def doWithSpring = {
        def buildProps = new Properties()
        def url = getClass().classLoader.getResource("dummy-plugin.properties")
        url.withReader { reader ->
            buildProps.load reader
        }
        application.config.merge(new ConfigSlurper().parse(buildProps))
    }
}

DSLs vs ConfigSlurper syntax

Plugin metadata

Use the Release Plugin! Preferably latest version of plugin & Grails (1.0.0.RC3 & 2.0.0.RC1 at this time)

Conventions and sensible defaults

Example: Searchable Plugin
static searchable = true
static searchable = {
    root false
    name name: "tag"
}

User configuration overrides

• Consider: Application Taggable Searchable Tag How do we configure Tag domain class for search?

User configuration overrides


Config.groovy
searchable {
    domain {
        comment = {
            root false
            only = ["body"]
            body name: "comment"
        }
        tag = {
            root false
            name name: "tag"
        }
        screencast = [only: ["title", "description"]]
    }
}

Domain Classes

GORM Datasources

Although the original GORM implementation utilized Hibernate, Grails users now have many GORM based storage solutions at their disposal. When appropriate, Grails plugins should not assume the Hibernate based GORM implementation will be available. This means plugin developers should avoid making use of HQL queries, and Hibernate specific method calls when possible.

TODO: talk about query options, generic gorm session, etc.

Neighborly domain classes

class Book {
    …
    static mapping = {
        autoImport false
    }
}

GORM Sessions

persistenceInterceptor?.init()
try {
    // Execute user code here
} finally {
    persistenceInterceptor?.flush()
    persistenceInterceptor?.destroy()
}

Providing Domain Classes

Developers have a few options when providing domain classes as part of a plugin.

Plugin Packaged Domain class

Domain classes located under a plugin's grails-app/domain directory will be automatically packaged with the plugin.

Domain Template - create-*

The third way

User Interface

TODO: talk about resources plugin

The simple stuff

if (!attrs.containsKey(name)) {
    throwTagError "Tag [$tag] is missing required " + "attribute [$name]"
}

Use packages for your TagLibs (and a custom namespace).

static namespace = "jsec"

Complete UIs

Complete UIs

Other discussion points

Summary

Grails Plugin Platform