2. About Me
• Mike Hugo, Independent Software Developer
• http://piragua.com
• http://WhenWorksForYou.com
• Groovy/Grails since 2007
• Author of several Grails plugins
• code-coverage • greenmail
• hibernate-stats • build-info
• test-template • ????
3. • Plugins overview
• Build your own plugin
• testing
• modularization
• configuration
• events
• did I mention testing?
what’s on the menu for tonight?
4. App Plugin
a plugin is just another grails application, with a few extra files
* GrailsPlugin.groovy
* Install, Uninstall and Upgrade scripts
5. Cool hooks into the runtime environment of a running grails app, plus ability to add new
‘artefacts’ and participate in reloading events
6. Existing Plugins
there are a ton of existing plugins from security to Rich UI to searching to...you name it.
7. common problem - this is one i’ve solved about 6 times. time for a plugin
8. • grails create-plugin build-status
• cd build-status
• mkdir test/projects
• cd test/projects
• grails create-app statusapp
gives you a client you can
use to test your plugin.
why?
9. development
• make a change to the plugin
• grails package-plugin
• cd test/projects/statusapp/
• grails install-plugin ../../../grails-build-status-0.1.zip
• grails run-app
• wash, rinse, repeat
10. In Place Plugins
• In the application just created, modify
BuildConfig.groovy and add:
• grails.plugin.location."build-status" = "../../.."
• TADA! You’re now working with an in-
place plugin
14. Functional Testing
• in the app, install the functional test plugin
• grails create-functional-test
class BuildInfoPageFunctionalTests extends
functionaltestplugin.FunctionalTestCase {
void testSomeWebsiteFeature() {
get('/buildInfo')
assertStatus 200
assertContentContains 'app.version'
assertContentContains 'app.grails.version'
}
}
15. Introduce i18n
• i18n allows a form of customization
• create a messages.properties in the plugin
i18n directory for default values
• override it in the app to test to make sure
it works
16. Introducing Config
• Allow the client application the ability to
add or remove properties to display
void testIndex_overrideDefaults(){
mockConfig """
buildInfo.properties.exclude = ['app.version']
buildInfo.properties.add = ['custom.property']
"""
controller.index()
assertEquals 'index', renderArgs.view
def expectedProperties = controller.buildInfoProperties - 'app.version'
expectedProperties = expectedProperties + 'custom.property'
assertEquals expectedProperties, renderArgs.model.buildInfoProperties
}
18. Events
• We want to capture the start of WAR file
being built and log the Date/Time
• What events are available?
• Search $GRAILS_HOME/scripts for
“event”
• What variables are available?
• binding.variables.each {println it}
http://grails.org/doc/latest/guide/4.%20The%20Command%20Line.html#4.3%20Hooking
%20into%20Events
19. test it
• grails.test.AbstractCliTestCase
• Thank you Peter Ledbrook:
http://www.cacoethes.co.uk/blog/
groovyandgrails/testing-your-grails-scripts
http://grails.org/doc/latest/guide/4.%20The%20Command%20Line.html#4.3%20Hooking
%20into%20Events
22. return the favor
• publish your own events to allow clients
to hook into plugin workflow
eventCreateWarStart = {warname, stagingDir ->
event("BuildInfoAddPropertiesStart", [warname, stagingDir])
Ant.propertyfile(file:
"${stagingDir}/WEB-INF/classes/application.properties") {
entry(key: 'build.date', value: new Date())
}
event("BuildInfoAddPropertiesEnd", [warname, stagingDir])
}
23. Gradle Build
• http://adhockery.blogspot.com/2010/01/
gradle-build-for-grails-plugins-with.html
• http://www.cacoethes.co.uk/blog/
groovyandgrails/building-a-grails-project-
with-gradle
• One build file in the plugin that runs tests
on all your ‘apps’ in the test/projects
directory
24. in-place plugin caveats
• Reloading plugin artifacts doesn’t always work
• Grails 1.1.1
• see next slide
• Grails 1.2.1
• Plugin controllers lose GrailsPlugin annotation
and views cannot be resolved after reloading
http://jira.codehaus.org/browse/GRAILS-5869
25. def watchedResources = ["file:${getPluginLocation()}/web-app/**",
"file:${getPluginLocation()}/grails-app/controllers/**/*Controller.groovy",
"file:${getPluginLocation()}/grails-app/services/**/*Service.groovy",
"file:${getPluginLocation()}/grails-app/taglib/**/*TagLib.groovy"
]
def onChange = { event ->
if (!isBasePlugin()) {
if (event.source instanceof FileSystemResource && event.source?.path?.contains('web-app')) {
def ant = new AntBuilder()
ant.copy(todir: "./web-app/plugins/PLUGIN_NAME_HERE-${event.plugin.version}") {
fileset(dir: "${getPluginLocation()}/web-app")
}
} else if (application.isArtefactOfType(ControllerArtefactHandler.TYPE, event.source)) {
manager?.getGrailsPlugin("controllers")?.notifyOfEvent(event)
// this injects the tag library namespaces back into the controller after it is reloaded
manager?.getGrailsPlugin("groovyPages")?.notifyOfEvent(event)
} else if (application.isArtefactOfType(TagLibArtefactHandler.TYPE, event.source)) {
manager?.getGrailsPlugin("groovyPages")?.notifyOfEvent(event)
} else if (application.isArtefactOfType(ServiceArtefactHandler.TYPE, event.source)) {
manager?.getGrailsPlugin("services")?.notifyOfEvent(event)
}
}
// watching is modified and reloaded. The event contains: event.source,
// event.application, event.manager, event.ctx, and event.plugin.
}
ConfigObject getBuildConfig() {
GroovyClassLoader classLoader = new GroovyClassLoader(getClass().getClassLoader())
ConfigObject buildConfig = new ConfigSlurper().parse(classLoader.loadClass('BuildConfig'))
return buildConfig
}
String getPluginLocation() {
return getBuildConfig()?.grails?.plugin?.location?.'PLUGIN_NAME_HERE'
}
26. the real deal
http://plugins.grails.org/grails-build-info/trunk/
source code available in the grails plugin svn repository, or browse on the web at:
http://plugins.grails.org/grails-build-info/trunk/