(Quick Reference)

Grails Platform Core - Reference Documentation

Authors: Marc Palmer (marc@grailsrocks.com), Stéphane Maldini (smaldini@vmware.com)

Version: 1.0.RC3

1 Overview

The Plugin Platform provides APIs and utilities that provide the glue required for advanced Grails plugin development and integration across multiple Grails versions, and turbo-charge the development of a new generation of plugins.

The founding principle is that these platform features should not be part of Grails core because this would tie plugins that use the APIs to specific Grails versions.

This relative freedom from Grails versions means that plugins that use the platform should remain compatible with more Grails versions for longer, and that new features used by all plugins can be added outside of the Grails release cycle.

1.1 The APIs

The features include in this release include:
  • Configuration API - Plugin-namespaced config, Config merging, validation and default values
  • Security API - An abstraction of basic security features that most applications require, with implementations to be provided by plugins or your application
  • UI Extensions - A set of tags and helper properties and functions
  • Navigation API - A standard artefact and DSL for navigation and tags for accessing this
  • Events API - A standard event bus that can be plugged in to any event implementation, with a lightweight default provider for in-app messaging

Each feature is covered in more detail later in this documentation.

The platform is very self-referential - it uses own APIs - for example to provide event hooks for Grails application lifecycle, to declare dynamic methods on your controller and service artefacts, and to declare the configuration values that it uses.

All of these APIs are designed to be as simple as possible.

Use this platform to add tighter and more consistent integration to your own plugins.

1.2 Change log

1.0.RC1

  • Injected methods will not overwrite existing methods now, will warn instead
  • Added userExists method to security API
  • Unified i18N text and body handling in UI Extension tags
  • Improved dev-mode UI at /platform
  • Bug fixes and improvements to Events API

1.0.M3

Was: 1.0.M2 but due to release error we had to roll to M3.

  • Events API
  • New UI Extension tags p:text and p:textScope
  • New UI Extension properties pluginFlash, pluginSession, pluginRequestAttributes
  • Documentation for Events API, Navigation API, new UI Extensions
  • A little Grails treat for those browsing /platform/ on Mac

1.0.M2-SNAPSHOT

  • Refined and documented Navigation API
  • There are no longer any "g" namespaced tags. All g: tags have move to p: namespace
  • Added "site.url" Config setting for siteLink tag to use instead of grails.serverURL if the two differ for your use case
  • Refactored Injection, Conventions and Navigation implementations into public interface + implementation classes
  • Config reloading supported now - all plugin configs and constraints etc. are re-applied
  • legacyPrefix support in doWithConfigOptions - automatically copies over old Config values to your new plugin namespaced config
  • Added cssPrefix attribute to displayMessage

1.0.M1

First public release with Config and Security APIs and some UI Extensions. Work-in-progress APIs for Events, Injection, Conventions and Navigation.

1.3 Known Issues

1.0.M3

  • Automatic convention controller navigation includes all actions, not just those with GET allowedMethod
  • Platform "dev" navigation scope items do not render when browsing /platform in your own applications.

1.0.M1

  • Config API - false validation errors with platform-ui due to no x.y.'*' support yet
  • Navigation API - controllers do not auto-register in Grails 1.3.x, no DSL artefact, no reloading
  • Conventions API - API not for public use. Not fully implemented / TBD
  • Events - API not for public use. Scopes not fully implemented / TBD
  • Injection - injection may not re-apply dynamic methods and properties to reloaded or new artefacts, API is not for public use at all yet

2 Getting Started

To get started you need to install the platform-core plugin.

Add the plugin platform as a dependency to your application or plugin, either directly or transitively. To do so directly edit your BuildConfig.groovy to look something like this:

grails.project.dependency.resolution = {
	// inside your plugin dependencies block
	plugins {
	    compile ':platform-core:1.0.M1'
	}
}

You can run your code now and browse to http://localhost:8080/<appname>/platform/, a special page available only in the development environment. From there you can explore some of the plugin platform features in the debug interface.

There is no default security implementation for the Security API, this will need to come from the security plugin you use, or your application. See implementing a security bridge

3 Configuration API

The Configuration API adds the following features:
  • A way to declare the Config properties that a plugin supports
  • Automatic namespacing of plugin Config values to avoid clashes
  • Validation of Config values
  • Merging of config from plugins into main Application config
  • The ability for plugins to configure other plugins
  • An injected "pluginConfig" variable in all artefacts containing the plugin's configuration
  • Automatic merging of legacy Config settings into the new plugin namespace

All of this adds up to more powerful integration and less frustration and confusion for developers.

3.1 Changing Application and Plugin Config Values

To change application or plugin configuration from within a plugin you need to declare the doWithConfig hook in your plugin descriptor.

The code uses a simple DSL that is identical to normal Config except:

  1. The top level nodes are plugin names or "application" to determine what scope of config you are changing
  2. The closure is passed the existing config as its first and only argument

The application Config is loaded first. All the doWithConfig blocks are evaluated and the results merged in.

def doWithConfig = { config ->
    platformUi {
        ui.Bootstrap.button.cssClass = 'btn'
        ui.Bootstrap.tab.cssClass = 'tab-pane'
        ui.Bootstrap.field.cssClass = 'input'
    }
    
    application {
        // set something based on another config value that has already been
        // by the application
        grails.x.y = config.p.q == 'something' ? true : false
    }
}

See doWithConfig for more details.

3.2 Declaring Configuration Options

To make use of the plugin configuration features and make life easier for developers, your plugin must define the configuration options it accepts.

This allows the platform to warn users when they mistype a config name or supply and invalid value - and to formalise definition of default values rather than a plugin merging in default values.

Do declare the options your plugin supports, add the doWithConfigOptions closure to your plugin descriptor:

def doWithConfigOptions = {
    'organization.name'(type: String, defaultValue: 'My Corp (set plugin.platformCore.organization.name)')
    'site.name'(type: String, defaultValue: 'Our App (set plugin.platformCore.site.name)')
}

This block, from the platform core, defines two configuration values of type String, with a default value.

You can also supply a custom validator:

def doWithConfigOptions = {
    'concurrentConnections'(type: Integer, defaultValue: 10, 
        validator: { v -> v < 500 ? null : 'concurrent.connections.too.big' }
}

Behaving just like constraint validators, your validator returns null for "ok" or an i18n message string for the error.

When implementing this config in your plugins, you may want to use the legacyPrefix value so that applications that use your existing Config settings will continue to work, and users will be warned to update their Config.

See doWithConfigOptions for more details.

3.3 Accessing Plugin Config

Plugins that declare their configuration with doWithConfigOptions can get access to their "slice" of the Config using the pluginConfig variable.

The pluginConfig variable is automatically injected into all artefacts of your plugin, automatically namespace for your plugin using the plugin.<pluginName>. prefix.

So in a service you can trivially access this config inside a service or controller for example:

class MyPluginService {
    def doSomething() {
       if (pluginConfig.enabled) {
          println "It worked!"
       }
    }
}

4 Security API

The Security API provides common security related functionality so that plugins and in some cases applications no longer need to be tied to a specific security implementation.

It is deliberately minimal to place few requirements on implementations.

It is not intended to be a complete security abstraction, but "just enough" for the needs of most applications and plugins that do not require advanced security manipulation - which will likely always require direct knowledge of the underlying provider.

This API provides the most basic security features to enable this interoperability, using a bridging interface that security plugins must implement to actually provide these services.

Security plugins must implement the "provider" bean - out of the box there is no security implementation.

Dynamic methods

These methods and properties are added to Services, Controllers, TagLibs and Domain Classes:

  • securityIdentity - The string used to identify the current logged in user. Typically a user id, user name or email address. The nature of this value is dependent on your security implementation
  • securityInfo - The object representing the current logged in user's information.
  • userHasAnyRole - test if the current user has any of the roles
  • userHasAllRoles - test if the current user has all of the roles
  • userIsAllowed - test if the current user has a specified permission on an object
  • withUser - run a section of code as another user

See the reference guide for Security Properties and Security Methods for further details on these.

All of these features and more can be accessed by any code in your application by using the grailsSecurity bean.

Tags

Here's an example of some of the security tags available:

<s:identity/>
<s:info property="email"/>
<s:ifPermitted role="ROLE_ADMIN">
    …
</s:ifPermitted>
<s:ifNotPermitted role="ROLE_ADMIN">
    …
</s:ifNotPermitted>
<a href="${s.createLoginLink()}">Log in</a>
<a href="${s.createLogoutLink()}">Log out</a>
<a href="${s.createSignupLink()}">Sign up</a>

See the Tags Security reference section for details.

grailsSecurity bean

The Security bean provides access to all the basic security functions. These are passed through to the security bridge implementation.

This includes methods for applications and plugins to use such as:

  • String getUserIdentity()
  • def getUserInfo()
  • boolean userHasRole(role)
  • boolean userIsAllowed(object, action)
  • def ifUserHasRole(role, Closure code)

You simply auto wire this bean into your classes using the name "grailsSecurity"

For more details see grailsSecurity.

4.1 Implementing a Security Bridge

To use the Security API, an application must have a Security Bridge bean that implements the SecurityBridge interface,

Typically this bean will be provided by the security plugin you are using. However you can easily implement your own in your own plugin or application.

Simply implement the interface and register the bean as "grailsSecurityBridge" in your Spring context either in a plugin's doWithSpring or your application's resources.groovy:

grailsSecurityBridge(com.mycorp.security.MySecurityProvider) {
    // wire in any other dependencies it has
    grailsApplication = ref('grailsApplication')
}

The interface is defined here:

interface SecurityBridge {
    
    /**
     * Implementations must return the name of their security provider 
     * @return A name such as "Spring Security"
     */
    String getProviderName() 

    /**
     * Get user id string i.e. "marcpalmer" of the currently logged in user, from whatever
     * underlying security API is in force
     * @return the user name / identity String or null if nobody is logged in
     */
    String getUserIdentity()

    /**
     * Get user info object containing i.e. email address, other stuff defined by the security implementation
     * @return the implementation's user object or null if nobody is logged in
     */
    def getUserInfo()

    /**
     * Return true if the current logged in user has the specified role
     */
    boolean userHasRole(role)

    /**
     * Can the current user access this object to perform the named action?
     * @param object The object, typically domain but we don't care what
     * @param action Some application-defined action string i.e. "view" or "edit"
     */
    boolean userIsAllowed(object, action)
    
    /**
     * Create a link to the specified security action
     * @param action One of "login", "logout", "signup"
     * @return Must return a Map of arguments to pass to g:link to create the link
     */
    Map createLink(String action)
    
    /**
     * Execute code masquerading as the specified user, for the duration of the Closure block
     * @return Whatever the closure returns
     */
    def withUser(identity, Closure code)
}

5 Events Bus API

Why an events bus ? Today's applications rely more and more on non-blocking processing, elasticity and modularity. An events bus loosely couples modules, enabling different codes and frameworks to work together, because the right tool for the right purpose paradigm is becoming a reality. The bus may also support publish/subscribe pattern, distributing the same message across the handling modules and giving an excellent opportunity to deploy the same application into a cluster or cloud .

Within the bus, an event is often as simple as a "callback" with no parameters, but usually there is some extra state passed as an event object. An event can be sent to multiple listeners, and any result returned from any listeners is passed back to the original sender of the event. An event belongs to a "topic" and often has a "subject". The topic is like a channel that identifies the kind of events. The optional subject is the object that the event "happened to". So for example a simple "app started" notification has no subject but may have topic "grails", but a "user logged in" even may have topic "security" and subject set to the user principal supplied by the security plugin you are using.

With Platform-Core plugin we have implemented a couple of features and artifacts to let you simply manage an event bus and get maximum flexibility when required :

  • Sending Events methods injected in your Domains, Controllers and Services
  • @Listener annotations for your Services methods.
  • Events mapping DSL artifact to select and control events topics
  • Spring beans with access to underlying API
  • More cool stuff with Events Spring Integration and Events Push
  • Simple config keys

In a nutshell, you will :

  • Send events :

class UserController{

   def registration(){
         def user = new User(params).save()
         if(user){

            //non-blocking call, will trigger application listeners for this topic
            event('mailRegistration', user)

            //blocking call :
            //event('mailRegistration', user).waitFor()

            //can also be written like that
            //event topic:'mailRegistration', data:user

            //and if you need to reuse the current thread
            //event topic:'mailRegistration', data:user, fork:false

            render(view:'sendingRegistrationMail')
         }else{
            render(view:'errorRegistration')
         }
  }
}
  • Write listeners (or event handler, or event reactor or whatever you call it):

class UserService{

   //use method name 'mailRegistration' as topic name
   //can also use custom topic name using topic arg: @Listener(topic='test')
   @grails.events.Listener
   def mailRegistration(User user){
         sendMail{
            to user.mail
            subject "Confirmation"
            html g.render(template:"userMailConfirmation")
        }
  }

  //Can also receive an EventMessage to get more information on this particular event)
  @grails.events.Listener(topic="mailRegistration")
  def mailRegistration2(org.grails.plugin.platform.events.EventMessage msg){
        sendMail{
           to msg.data.mail
           subject "Confirmation"
           html g.render(template:"userMailConfirmation")
        }
  }
}

5.1 Sending Events

Sending an event is simple. You only need to remind 1 method name and 2 different signatures:
  • event(topic, [data, params, callbackClosure])
  • event(Map args, [callbackClosure])

We recommend using the former signature if you don't have any params, otherwise the latter is more elegant in that case.

Let's see what the key arguments are doing:

  • Topic argument is a String which represents channel subscribed by listeners.
  • optional Data argument is an Object - preferrably Serializable for IO facilities - which represents the subject of your event such as a domain class.
  • optional Params argument is a Map which represents sending behaviors including namespace.
  • optional callbackClosure is a Closure triggered after an event completion.
  • The map notation allows you to reuse the same arguments than params plus topic for topic, data for data and for (shortcut for 'namespace'). If you specify params, it will use it for the params argument otherwise the first level map is used as params.

There are several params arguments :

KeyTypeDefaultDescription
forkBooleanfalseForce the event to reuse the caller thread, therefore executing the method synchronously and propagating any errors.
namespace / forString'app'Target a dedicated topic namespace. To avoid overlapping topic names, the events bus supports a scoping concept called namespace. E.g. 'gorm' is used by gorm events and 'browser' is used for Javascript listeners in events-push plugin.
onReplyClosure{EventReply reply} Same behavior than callbackClosure argument, overrides it if both are defined.
onErrorClosure{List<Exception> errors} If exceptions has been raised by listeners, this callback will be triggered. If undefined, exceptions will be propagated on EventReply.getValue(s).
gormSessionBooleantrueOpens a GORM session for the new thread which carries event execution.
timeoutLong Define a maximum time in millisecond for the event execution.
headersMap<String, Serializable> Additional headers for the event message enveloppe.

The event method returns EventReply which implements Future<Object> and provides usefuls methods :

  • List<Object> getValues() : Returns as many values as listeners has replied.
  • Object getValue() : First element of getValues().
  • int size() : Invoked listeners count.
  • List<Throwable> getErrors() : Available errors.
  • boolean hasErrors() : Scans for any errors.
  • EventReply waitFor() : blocks current thread and return this reply.
  • EventReply waitFor(long time) : blocks current thread for T milliseconds and returns this reply.

Events workflow

Events can be sent from domains, services and controllers artefacts by using EventReply event(String topic, Object data) . Platform-core Events bus provides a non-blocking way to send events by default, however you can block on several methods from EventReply :
  • size
  • waitFor
  • get
  • getValues
  • getValue

Therefore you have the control on the execution flow if you want. Just keep in mind it does not block for processing right after event() call, which seems to be a sensible default for the bus. Eventual Exceptions will be raised after using one of the mentioned blocking methods except if onError parameter is used.

class SomeController{

   def logout(){
      def reply = event("logout", session.user)
      //doesn't wait for event execution
      
      render reply.value  //wait and display value
      
      event(topic:"afterLogout").waitFor()
      
      //Only triggered when "afterLogout" finished
      def errorHandler = {errs -> }
      //Use a dedicated error handler
      event(topic:"afterAfterLogout", onError:errs)
   }
}

Non forked events

If you want to reuse the current thread and force synchronous processing, use the fork param. Be aware that each exception will be directly propagated to caller even without using blocking methods except if onError parameter is used.

class SomeController{

   def logout(){
      def reply = event('logout', session.user, [fork:false])
      //block for processing

      //no need to wait for reply since it has been populated on event call.
      render reply.value
   }
}

Assigning a namespace

All listeners get a property called namespace which prevents topic naming collisions and undesired events. By default, they are all assigned to app. This is the same default used when you send an event, but what if you want to reach others namespaced listeners, like 'browser' ones if you use events-push plugin ? Simply use namespace argument or use for if you stick with Map notation.

 class SomeController{

    def logout(){
        //we use the Map form, the namespace argument is identified by the 'for' key
       event for:'browser', topic:'logout', data:session.user
    }
 }

It's mandatory to declare namespace when using events bus from a plugin in order to avoid any conflicts.

Wildcard support

It's possible to call multiple topics/namespaces in a single shot using wildcard as the last character.

class SomeController{

    def logout(){
        /*
          We send to every listeners starting with "chat-"
          on every namespaces starting with "role-"
        */
        event for:'role-*', topic:'chat-*', data:session.user

        //Here we can trigger every listeners in the default namespace 'app'
        event '*'
    }
}

This feature will probably evolve to a smarter implementation behaving like UrlMappings and authorizing substring captures

5.2 Listening Events

Listening for events simply requires registering the method that should receive the event notifications.

There are few ways to register events.

Defining listeners at compile time

Within Grails services you can use the @Listener annotation. It takes a topic string, but you can omit it and use the method name as the topic to listen for:

class SomeService{

   @grails.events.Listener(topic = 'userLogged')
   def myMethod(User user){
   }

   //use 'mailSent' as topic name
   @grails.events.Listener
   def mailSent(){
  }
}

Event methods can define a single argument, and the value is the object sent with the event. Usually this is the "subject" of the event. However an event is carried by an enveloppe called EventMessage which contains several useful metadata like additionnal headers, current topic :

class SomeService{

   @grails.events.Listener(topic = 'userLogged')
   def myMethod(org.grails.plugin.platform.events.EventMessage userMessage){
    println userMessage.headers // display opt headers
    println userMessage.event // displays current topic
    println userMessage.data // displays data
   }
}

If a listener argument type is not assignable to an event data type, the event silently skips the mismatching listener. If you want to catch every event types, use Object type or if the argument is not necessary, do not declare it.

Filtering on the EventMessage<D> generic type doesn't work, e.g. EventMessage<Book> won't prevent EventMessage<Author> invokation. For such fine grained control, you can rely on Events Artifact

Namespacing

Your declared events belongs to the app namespace, unless you tune it using the namespace argument or the Events DSL we will introduce later.

class SomeService{

   @grails.events.Listener(topic = 'userLogged', namespace = 'security')
   def myMethod(User user){
   }

   //will subscribe this method to topic 'afterInsert' on namespace 'gorm'
   @grails.events.Listener(namespace = 'gorm')
   def afterInsert(User user){
  }
}

Remember that you will need to specify the scope when triggering events if you customize it with a different value than app :

class SomeController{
   def myAction(){
        event for:'security', topic:'userLogged', data:session.user
   }
}

It's mandatory to declare namespace when using events bus from a plugin in order to avoid any conflicts.

Proxy (AOP) support

By default, listeners try to call the original method (unproxified bean). Using proxySupport you can tweak this setting :

class SomeService{

   static transactional = true

   //Will invoke transactional logic, similar to someSerice.myMethod()
   @grails.events.Listener(proxySupport=true)
   def myMethod(User user){
   }

}

Dynamic listeners

Some edge cases need runtime registration. If you meet this use case, use the injected on method :

class SomeController{

 def testInlineListener = {
        //register with 'logout' topic on 'app' default namespace
        def listener = on("logout") {User user ->
            println "test $user"
        }
        render "$listener registered"
 }

 def testInlineListener2 = {
        //register a 'gorm' namespaced handler on 'afterInsert' topic.
        def listener = on("gorm", "afterInsert") {Book book ->
            println "test $book"
        }
        render "$listener registered"
 }
}

Wildcard support

Capturing a wider group of events can be useful, specially for monitoring purposes. It's possible to listen for multiple topics/namespaces in a single shot using wildcard as the last character.

class SomeService{

 @grails.events.Listener(namespace='role-*', topic = 'chat-*')
   def myMethod(org.grails.plugin.platform.events.EventMessage userMessage){
      println userMessage.namespace
      println userMessage.event
   }
}

This feature will probably evolve to a smarter implementation behaving like UrlMappings and authorizing substring captures

Listener ID

Registered listeners generate a unique id (ListenerId) applying the following pattern :
[namespace://]topic[:package.Class][#method][@hashcode]

The above square brackets determine each optional part of the sequence id thus allowing to target group of listeners depending of the known arguments: namespace, class, method, hashcode.

This pattern is useful when using countListeners, removeListeners or extensions. For instance, overriding a generated channel with events-si plugin requires to use namespace://topic if namespace is different from "app". Another example to count listeners:

//count every listeners subscribed to 'mytopic' inside TestService
countListeners("mytopic:my.TestService")

//count every listeners using gorm namespace
countListeners("gorm://*")

//remove every listeners in TestService
removeListeners("*:my.TestService")

Reloading in Development mode

It works.

5.3 Replying from Listeners

Usually, an event is fired and forgot . In some cases, you may expect an answer to transform your messaging architecture into a controlled flow. For instance, a negative reply can be used in GORM events to veto database writing for the current subjet. Another usual example is the aggregation of multiple workers products.

Simple reply

Replying is a simple matter of returning an object from the listener method :

class SomeService{
    @Listener
    def logout(User user){
       Date disconnectDate = new Date()

       //do something with user

       return disconnectDate
    }
}

If listeners return non null objects, the caller can access them through the EventReply enveloppe returned immediatly after calling event method. The other option is the use of a reply handler :

class SomeController{

   def logout(){
       def reply = event topic:"logout", data:session.user, fork:false
       render reply.value //display value

       //Using callback closure
       def replyHandler = {EventReply reply-> }
       event topic:"logout", data:session.user, onReply:replyHandler

       //Or as last argument
       event(topic:"logout", data:session.user){ EventReply r-> }


       //EventReply object is a Future implementation
       def reply_future = event topic:"logout", data:session.user
       render reply_future.get(30, TimeUnit.SECONDS)
   }
}

Whenever an event is triggered, a task is submitted into the events bus thread pool and a Future returned, wrapped into EventReply. It's also planned to fully support reply-address pattern in a future version (replyTo parameter) which brings interesting features out of the box : non blocking response, streaming handler response one by one, forwarding using topic name instead of closure…

Multiple replies

Multiple listeners can return values for the same topic/namespace. In this case, EventReply will wait for all handlers before returning any value. Remember that a valid result is a non null value, hence why even if 3 handlers have reacted but only 2 did return something, then you will only see 2 values in the EventReply.values.

class SomeController{

   def logout(){
       def reply = event topic:"sendMails", data:session.user

       //wait for all listeners and then display the first value from the aggregated results
       render reply.value
       
       //display all results as List
       render reply.values
   }
}

Exceptions

Because no code is perfect, exceptions can happen in the event process for 3 reasons :

  • RuntimeException in one or more handlers
  • InterruptedException if the process has been cancelled
  • TimeoutException if the maximum process time has been reached (timeout parameter)

An onError parameter is available and accepts a Closure{List<Throwable> errors}. If non set, exceptions are propagated to the caller when blocking the EventReply object ( getValue etc) and/or when fork == false.

Exceptions in multiple listeners scenario don't interrupt the execution flow and leave a chance to others listeners to execute as well. The return value from a failing listener becomes the raised exception.

class SomeController{

   def logout(){
       on('test'){
         sleep(5000)
         throw new MyException('haha')
       }

       def reply = event topic:"test"
       reply.values //throws MyException after 5s


       def errorsHandler = {println it}
       reply = event topic:"test", onError:errorsHandler
       reply.values //calls errorsHandler and returns values which contain at MyException

       event(topic:"test", onError:errorsHandler, timeout:1000){
          //executes both this and errorsHandler closures with a TimeoutException
       }

       reply = event(topic:"test", onError:errorsHandler, timeout:1000)
       reply.cancel() //executes errorsHandler closure with an InterruptedException

       event(topic:"test", fork:false)
       //wait 5s and raises an exception in the caller thread

   }
}

Waiting replies

In domains, services and controllers artefacts you can wait for events using "EventReply waitFor(EventReply… eventReplies)". It accepts as many events replies you want and returns the same array for functiunal programming style. EventReply also have a waitFor method for one-line waiting.

class SomeController{

   def logout(){
      def reply = event('logout', session.user)
      def reply2 = event('logout', session.user)
      def reply3 = event('logout', session.user)

      waitFor(reply,reply2,reply3).each{EventReply reply->
        render reply.value +'</br>'
      }

      //same with 20 seconds timeout on each reply
      waitFor(20, TimeUnit.SECONDS, reply,reply2,reply3).each{EventReply reply->
              render reply.value
      }

      //other style :
      event('logout', session.user).waitFor() //blocks event
      event('logout', session.user).waitFor(2000) //blocks event for maximum 2 seconds

   }
}

5.4 Routing configuration -- The XxxEvents Artifact

An extensible Events DSL is available in grails-app/conf for routing configuration. This artifact does detail behaviors of event method by selecting topics and namespaces to apply :
  • Filtering
  • Disabling
  • Sending behaviors
  • Extensions
  • Security
  • Declarations

The DSL is intended to evolve. One of the most wanted features is topic/namespace declaration: assigning a definition to a property would generate an injectable eponym bean with streaming methods.

The DSL requires to assign a closure to an events variable. Each call is a definition, the method name is parsed as a topic name and key/value arguments are definitions attributes. Wildcard topics/namespaces are supported as well.

An Events artifact is a script with some bound variables:

VariableDescription
grailsApplicationGrails application object, retrieves artifacts, context etc.
ctxSpring context, useful for beans access, e.g. ctx.myService.method()
configConfiguration object

Each definition supports the following attributes:

Attribute nameTypeDefaultDescription
namespaceString "app" Define which namespace the current definition is bound
filterClosure(Object)
Closure(EventMessage)
Class
 If a closure is passed, the return value matched as the condition for event propagation.
If a class is passed, the subject data type must match.
disabledbooleanfalseDisable event propagation
forkbooleanfalseUse the current thread for event processing (blocking call)
onErrorClosure(List<Throwable>) Default onError handler for the current topic(s)
onReplyClosure(EventReply) Default onReply handler for the current topic(s)
timeoutLong Default timeout for execution time, throwing a TimeoutException and calls handlers
** Any attributes can be written to be used by plugins through EventDefinition.othersAttributes

events = {
    //prevents any events in gorm namespace
    '*' namespace:'gorm', disabled:true

    //filters any events on 'testTopic' where data <= 2
    testTopic filter:{it > 2}

    //filters any events on 'testTopic2' where data is not a TestTopic class type
    testTopic2 filter:TestTopic

    //filters any events on 'testTopicX' using boolean method from service
    testTopicX filter:ctx.myService.&someMethod

    //only if using events-push plugin, allows client-side listener on this topic
    testTopic3 browser:true

    //Default Error Handling, Global Reply Handling, timeout and fork
    testTopicD onError:{}, onReply:{}, timeout:1000l
    testTopicD2 fork:false


    //-------------- roadmap --------------

    //not yet implemented: Assigning and merging definitions
    //myTopic = testTopic4(filter:{i>2})
    //myTopic = testTopic4(filter:{i<4})

    //not yet implemented: Enabling security context for target listeners
    //testTopic5 secured:true

    //not yet implemented: Topic Forwarding
    //testTopic6 to:'anotherTopic'

    //not yet implemented: Topic Handlers
    //testTopic9 onError:'anotherTopicErrors', onReply:'anotherTopicReplies'

}

Reloading in Development mode

It works.

5.5 Listening GORM events

Starting from Grails 2, the Events Bus supports GORM events.

GORM Listeners

To listen for GORM, simply declare listeners on the gorm namespace using the following supported topics table :

Event TypeTarget Topic
PreInsertEventbeforeInsert
PreUpdateEventbeforeUpdate
PreDeleteEventbeforeDelete
ValidationEventbeforeValidate
PostInsertEventafterInsert
PostUpdateEventafterUpdate
PostDeleteEventafterDelete
SaveOrUpdateEventonSaveOrUpdate

Same listeners behaviors apply, e.g. using EventMessage for the argument type, using wildcard topics etc. Because listeners are called if there are no arguments or the argument type is assignable to current event data type, specifying a domain class is the only required step to filter domains events.

class SomeService{

    @Listener(namespace = 'gorm')
    void afterInsert(Author author) {
        println "after save author -  $author.name"
    }

    @Listener(topic = 'beforeInsert', namespace = 'gorm')
    void beforeInsertBook(Book book) {
        println "will insert book - $book.title"
    }

    //Will catch everything since we don't filter on the subject by using EventMessage
    @Listener(topic = 'before*', namespace = 'gorm')
    void beforeEachGormEvent(EventMessage message) {
        println "gorm event $message.event on domain $message.data.class"
    }

}

Filtering with Events Artifact

Setting a filter through an Events artifact allows more fined control and efficient selection since it prevents events to be propagated :

events = {
    'afterInsert' namespace:'gorm', filter:Book
    'afterDelete' namespace:'gorm', filter:{it.id > 5}
    'afterUpdate' namespace:'gorm', filter:{it in Book || it in Author}
    'beforeDelete' namespace:'gorm', disabled:true
}

GORM may generate tons of events. Consider using it wisely, combine it with routing filtering. You can also totally disable gorm bridge by using events.gorm.disabled configuration key.

Threading behaviors

GORM Listeners are executed in the same thread than the caller in order to reuse the current opened session, if any. Avoid blocking logic if possible or use the listener body to call another event.

Vetoing changes

If a listener handles one of the before* topics and returns a boolean value, it becomes part of the vetoing chain:

  • Returning false will cancel the current database write
  • Returning true will just let the chain continuing

class SomeService{

    //veto any Book insert
    @Listener(topic = 'beforeInsert', namespace = 'gorm')
    boolean beforeInsertBook(Book book) {
        false
    }

}

5.6 Spring Beans

Plugin developpers and any crazy tweakers may need to override one or more Events Bus beans, like the Spring Integration plugin does. The grailsEvents bean is also useful to inject events methods into unhandled artifacts (other than domain, service, controller).

Bean NameTypeDefault ImplementationDescription
grailsEvents org.grails.plugin.platform.events .Events org.grails.plugin.platform.events .EventsImpl Main events gateway which contains injected methods in artifacts
grailsEventsPublisher org.grails.plugin.platform.events .publisher.EventsPublisher org.grails.plugin.platform.events .publisher.DefaultsEventsPublisher Publisher bean, triggers events. To be implemented by extensions if required (e.g. events-si)
grailsEventsRegistry org.grails.plugin.platform.events .registry.EventsRegistry org.grails.plugin.platform.events .registry.DefaultsEventsRegistry Registry bean, store listeners and route events. To be implemented by extensions if required (e.g. events-si)
gormTopicSupport org.grails.plugin.platform.events .dispatcher.GormTopicSupport org.grails.plugin.platform.events .dispatcher.GormTopicSupport2X Translates gorm events to topic names and processes veto(s).
grailsEventsGormBridge org.grails.plugin.platform.events .publisher.GormBridgePublisher  Listens for GORM events and publishes to the right bus using gormTopicSupport.
grailsTopicExecutor org.springframework .core.task.TaskExecutor org.springframework.scheduling .concurrent.ThreadPoolTaskExecutor Carries on events execution.

5.7 Securing events

To be implemented. You can still use headers or data to pass security context for instance. M4 release will bring platform-security abstraction ready for events.

5.8 Extensions

Writing extensions is one of the greatest habbits of grails developpers. Groovy and Grails communites are like that, pragmatic and pleasant.

Having seen the referenced beans in the previous chapter should already give you ideas to improve or customize your needs. There are two available examples of extensions:

  • events-si : This plugin overrides the publisher and registry beans in order to replace the default mechanisms with the much

more flexible Spring Integration channels and endpoints.

  • events-push : This plugin registers new Listeners from your cool browsers using javascript, authorized through the

new Events DSL attributes browser and browserFilter

5.9 Configuration properties

Based on Platform-Core configuration mechanism, the plugin provides the following Events-Bus related keys:

Configuration KeyTypeDefaultDescription
grails.plugin.platform.events.disabledBooleanfalseFully disable Events Bus mechanism, no events methods will be injected
grails.plugin.platform.events.poolSizeInteger10Allow X concurrent workers to process events
grails.plugin.platform.events.gorm.disabledBooleanfalseDisable GORM bridge, stopping GORM events from being published
grails.plugin.platform.events.catchFlushExceptionBooleantrueCatch any GORM flushing exceptions which could be noisy specially when vetoing changes

In addition, you can override beans values such as gormTopicSupport

beans{
    gormTopicSupport {
        //transform gorm Events Objects types into topics
        translateTable = [
            'PreInsertEvent': 'beforeInsert', 'PreUpdateEvent': 'beforeUpdate',
            /*'PreLoadEvent': 'beforeLoad',*/ 'PreDeleteEvent': 'beforeDelete',
            'ValidationEvent': 'beforeValidate', 'PostInsertEvent': 'afterInsert',
            'PostUpdateEvent': 'afterUpdate', 'PostDeleteEvent': 'afterDelete',
            /*'PostLoadEvent': 'afterLoad',*/ 'SaveOrUpdateEvent': 'onSaveOrUpdate'
        ]
    }
}

6 Navigation API

The Navigation API provides a standard way to expose information about the menus available in your application and plugins.

Aside from application navigation, plugins can expose their controllers and actions so that application can reuse them in their own navigation structure. Applications can also add items to the navigation structure of plugins, to merge items into the UI of plugins.

There are only three concepts to understand in the Navigation API - items, scopes and the activation path.

Out of the box, scopes are created for all your application and plugin controllers automatically by convention. Items are created in these scopes for every action on the controller.

You will typically move from this to using the navigation DSL artefact for more control over the navigation structure.

What is a navigation item?

An item is a place the user can reach in your navigation structure. Every item results in a menu item and link. Whether it is visible or enabled can be determined at runtime.

Items are always inside one scope.

Items can have child items.

Items must be resolvable from a controller/action pair, so the navigation API can always tell where the user is in the structure if the current controller/action is known and you have an item declared for them.

What is a Scope?

A scope is a name that identifies one or more navigation items. Top-level scopes are called root scopes and represent your main groupings of navigation items. For example you may have your application navigation for regular users and an admin root scope for backend administration.

Example of scope names:

app                     // typically your default app navigation root scope
app/messages            // the "messages" item in the "app" root scope
admin/scaffolding/book  // the "book" item under "scaffolding" item in the "admin" scope
plugin.cms/admin        // the "admin" item supplied by the "CMS" plugin
plugin.socialFeed/feeds // the "feeds" item supplied by the "social-feed" plugin

Root scopes do not generate any menu links themselves, they are merely containers for your top level navigation items. They enable you to have multiple sets of navigation for different contexts.

The items that scopes refer to can be nested arbitrarily. It is however generally recommended that you use at most 2 levels of navigation, sometimes three if really necessary. This is purely because of the user experience issues with deep navigation.

Usually you should factor out deep navigation into separate root scopes. For example most applications would have the "app" scope, a "footer" scope for footer links like Terms of Use, Support etc., and a "user" scope for log in/out and so on.

What is an Activation Path?

An activation path is a string that represents the currently active navigation item. This may be a few levels down in your navigation structure and represents the breadcrumb trail the user would see to get to the location they are currently viewing.

Breadcrumbs themselves represent a navigational superset of your app's primary navigation structure. They are not supported in this release of the API, because the work has not yet been done to declare breadcrumbs that represent non-navigational items i.e. nested content inside a multi-page document is not part of your regular site navigation.

The activation path is set on the current request and indicates which node is currently active. By default navigation API attempts to identify the correct activation path in your structure using the current controller and action, much like reverse URL mapping.

However you can explicitly set the activation path using a tag or some code, for cases where you need to "fudge" it - for example if your action performs some odd redirection, or the endpoint is simply a GSP view which cannot be reversed to a location in the structure.

The first thing you need to do is install the platform-core plugin if you haven't already.

If you then run your application and you have some existing controllers you'll find that if you add the nav:primary tag to one of your sitemesh layouts or GSP pages you will see top-level navigation for each of your controllers.

To get you started quickly, all your controllers will be automatically registered in the "app" scope and each controller has sub-items for each of it actions.

All the tags default to the "app" scope if you don't supply a scope and the current controller/action are within that scope, so it just works out of the box for the simple cases. So add the following to your sitemesh layout or GSP:

<nav:primary/>
<nav:secondary/>

This will render one or two <ul> tags for the "app" scope based on the currently active controller/action pair.

By default all your controllers are automatically declared for you inside the "app" scope if they are not explicitly declared in a navigation DSL artefact and the navigationScope property is not set on them.

These controller scopes have a nested item for each action defined on the controller, including the default action (the same as the link for e controller scope itself).

Moving some controllers from the default app navigation scope

You often have some controllers that you don't want to appear in the main navigation of the application. You may want these to appear in an admin interface for example. To do this with convention based navigation you can just add a static navigationScope property to controllers.

class BookController {
    static scaffold = Book
    
    static navigationScope = 'admin'
}

This allows you to push controllers into another scope. Note that plugin controllers are automatically namespaced into a scope under "plugin.<pluginName>", in a scope beneath this with the value of the navigationScope property.

You will not need to change your tags to render the admin navigation - if the controller/action the user is viewing resolves to an item inside the admin scope, the nav:primary tag will render the admin scope.

6.4 What is primary and secondary navigation?

The primary navigation is the top level application the user sees, and the secondary is the context-sensitive sub items of the currently active primary item.

Contemporary site styles typically separate out the primary and secondary navigation.

The primary and secondary tags are geared up for this and automatically lookup up the scope and activation path to work out what to render.

Normally you will only use these once in a page.

You can render any part of your navigation structure as a menu as many times as you like anywhere in your pages, using the menu tag.

Rendering multiple navigation scopes on the same page

A typical contemporary application will have something like three separate menus used on most pages; main, user and footer.

The main menu would use primary & secondary tags.

You would then render the user and footer navigation using the menu tag, and passing the user and footer scopes:

<html>
<body>
    <nav:primary/>
    <nav:secondary/>

    <div id="user-nav">
        <nav:menu scope="user"/>
    </div>    

    <g:layoutBody/>

    <div id="footer-nav">
        <nav:menu scope="footer"/>
    </div>    
</body>
</html>

This results in a page where there are actually for navigation renderings, showing different scopes.

6.6 Using the Navigation DSL

To declare navigation items you use navigation DSL artefacts to determine the items in each scope. Scopes are named and can be nested to provide a hierarchy.

Navigation artefacts are groovy scripts end in the name "Navigation" in grails-app/conf.

Here's an example for the various ways to use the DSL to declare scopes and items:

Example contents of grails-app/conf/AppNavigation.groovy:

navigation = {
    // Declare the "app" scope, used by default in tags
    app {
    
        // A nav item pointing to HomeController, using the default action
        home()

        // Items pointing to ContentController, using the specific action
        about(controller:'content')
        contact(controller:'content')
        help(controller:'content')
        
        // Some user interface actions in second-level nav
        // All in BooksController
        books {
            // "list" action in "books" controller
            list()
            // "create" action in "books" controller
            create()
        }
        
        // More convoluted stuff split across controllers/locations
        support(controller:'content', action:'support') {
            faq(url:'http://faqs.mysite.com') // point to CMS
            makeRequest(controller:'supportRequest', action:'create')
        }
    }
    
    // Some back-end admin scaffolding stuff in a separate scope
    admin {
        // Use "list" action as default item, even if its not default action
        // and create automatic sub-items for the other actions
        books(controller:'bookAdmin', action:'list, create, search')
        
        // User admin, with default screen using "search" action
        users(controller:'userAdmin', action:'search') {
            // Declare action alias so "create" is active for both "create" and "update" actions
            create(action:'create', actionAliases:'update')
        }
    }
}

Using tags such as the primary and secondary navigation tags you can render all the page elements you need.

The Navigation DSL Definition

The script must return a Closure in the navigation variable in the binding.

This closure represents the DSL and method invocations have a special meaning within the DSL.

The name used in method calls is used to construct the activation path of each item. So a call to "app" that has a call to "messages" which has a closure that calls "inbox" will create the following:

  • A scope called "app"
  • A top-level item in the "app" scope, called "messages", with activation path "app/message"
  • A nested item under "messages" called "inbox" with activation path "app/messages/inbox"

Top level method invocations (root scopes)

The top-level method calls that pass a Closure define root scopes in the navigation structure.

The "app" scope is a prime example of this:

navigation = {
    app {
        home controller:'test', data:[icon:'house']
    }
}

By default scopes defined by Navigation artefacts within plugins are automatically namespaced to prevent collisions with application namespaces.

Thus the scope "app" in a plugin called "SpringSecurityCore" would become the scope "plugin.springSecurityCore.app". If a plugin defines the scope with the global:true argument, this will not happen:

// Example of a plugin exposing a root scope without namespacing
navigation = {
    app(global:true) {
        contact controller:'test', data:[icon:'mail']
    }
}

Nested method calls - defining navigation items

The DSL supports the following arguments when defining a navigation items.

Linking arguments

These are controller, action, uri, url and view. These are passed to g:link to create links. The "view" attribute is handled internally and removed and converted to "uri" for the purpose of calling g:link

These values are passed through to the navigation tags for link rendering just as you would expect when calling g:link.

There are some special behaviours however:

ArgumentUsage
controllerOptional - it will be inherited from the parent node if the parent uses controller to create its link, or failing that it will use the name of the DSL method call
actionOptional - it will fall back to the name of the method call if the controller is specified or inherited. If the controller was not specified either (and hence "uses up" the method call name), this will use the default action of the controller or "index" if none is set. The action value can be a List or comma-delimited string. If it is, the first element is the action used to generate the item's link, and any other actions listed will have sub-items created for them, in alphabetical order.
actionAliasesOptional - list of actions that will also activate this navigation item. The link is always to the action defined for the item in the DSL, but if the current controller/action resolves to an action in this alias list, the navigation item will appear to be active. Used for those situations where you have multiple actions presenting the same user view i.e. create/save, edit/update

Visibility and Status

You can control per request whether items are visible or enabled, or set this in the navigation structure statically.

The arguments:

ArgumentUsage
visibleDetermines whether the item is visible and can be a boolean or a Closure. If it is a Closure, it will receive a delegate that supplies request and application properties (see below)
enabledDetermines if the item is enabled or not and can be a boolean or a Closure. If it is a Closure, it will receive a delegate that supplies request and application properties (see below)

Typically you will want to hide items if the user is not permitted to see them. An example of doing this with Spring Security Core:

import org.codehaus.groovy.grails.plugins.springsecurity.SpringSecurityUtils

def loggedIn = { -> 
    springSecurityService.principal instanceof String
}
def loggedOut = { -> 
    !(springSecurityService.principal instanceof String)
}
def isAdmin = { -> 
    SpringSecurityUtils.ifAllGranted('ROLE_ADMIN')
}

navigation = {
    app {
        home controller:'test', data:[icon:'house']
        …
    }
    
    admin {
        superUserStuff controller:'admin', visible: isAdmin
        …
    }

    user {
        login controller:'auth', action:'login', visible: notLoggedIn
        logout controller:'auth', action:'logout', visible: loggedIn
        signup controller:'auth', action:'signup', visible: notLoggedIn
        profile controller:'auth', action:'profile', visible: loggedIn
    }
}

Note how the Closures are "def"'d in the script to make them reusable and reachable within the DSL

The closures receive a delegate which resolves the following standard Grails properties:

  • grailsApplication
  • pageScope
  • session
  • request
  • controllerName
  • actionName
  • flash
  • params

Any unresolved properties will resolve to the model (pageScope) and failing that, to the application's bean context, so you can resolve service beans etc by just accessing them by name.

Title text

The title of an item is the text used to display the navigation item.

Two arguments are used for this:

ArgumentUsage
titleOptional. Represents an i18n message code to use. It defaults to "nav." plus the the item's activation path with "/" converted to "." so path app/messages/inbox becomes the i18n code nav.app.messages.inbox
titleTextOptional. represents literal text to use for the navigation item title if the i18n bundle does not resolve anything for the value of title

For automatically created action navigation items, the titleText defaults to the "human friendly" form of the action name. i.e. "index" becomes "Index", "showItems" becomes "Show Items".

Application custom data

Each item can have arbitrary data associated with it - but note that this data is singleton and should not change at runtime.

Typically you would use this to associate some extra data such as an icon name, which you then use in custom menu rendering code.

Just put the values into the "data" Map:

navigation = {
    app {
        home controller:'test', action:'home', data:[icon:'house']
    }
}

Ordering of items

Items are ordered naturally in the order they are declared in the DSL.

However you may wish to manually order items, for example so that plugins (or the application) can inject items into certain positions in your navigation.

Just pass the integer value in the order argument:

navigation = {
    app {
        home controller:'test', action:'home', order:-1000
        about controller:'test', action:'about', order:100
        contact controller:'test', action:'contact', order:500 data:[icon:'mail']
        messages(controller:'test', data:[icon:'inbox'], order:10) {
            inbox action:'inbox'
            archive action:'archive'
            trash action:'trash', order:99999999 // always last
        }
    }
There are a few Navigation tags available, all detailed in the reference section.

The most common tags you will use are explained here.

It is important to understand that all the tags work by default using the current scope and activation path as determined by the request - but you can override scope and path on all of these tags to render anything you like.

Navigation is rendered by default as an HTML <ul> tag with an <li> containing a single link for each of the items. Nested items are rendered as nested <ul>.

All navigation rendering tags support attributes for CSS class, id and custom rendering of items if required. These are still always rendered within <ul>.

nav:primary

Use this tag to render the primary user navigation of your site:

<nav:primary scope="admin" id="nav" class="admin"/>

<%-- With custom item rendering --%>
<nav:primary scope="admin" id="nav" class="admin" custom="true">
   <li>
   <p:callTag tag="g:link" attrs="${linkArgs + [class:'nav button']}">
      <nav:title item="${item}"/>
   </p:callTag>
   </li>
</nav:primary>

This supports custom rendering in the same way as the menu tag.

See the primary tag reference for full details.

nav:secondary

Use this tag to render the second-level navigation based on the selected item within the current primary navigation. The scope resolved by nav:primary is stored in the request so that this tag knows which scope to use:

<nav:secondary id="secondary-nav" class="admin"/>

This supports custom rendering in the same way as the menu tag.

See the secondary tag reference for full details.

nav:menu

The menu tag is used internally by the primary/secondary tags and can be called directly to render any part of the navigation structure, with any activation path.

<nav:menu id="main-nav"/>

<%-- Render the admin nav 3-deep, including all nested descendents whether active or not --%>
<nav:menu scope="admin" depth="3" forceChildren="true"/>

<%-- With custom item rendering --%>
<nav:menu scope="admin" id="nav" class="admin" custom="true">
   <li>
   <p:callTag tag="g:link" attrs="${linkArgs + [class:'nav button']}">
      <nav:title item="${item}"/>
   </p:callTag>
   </li>
</nav:menu>

See the menu tag reference for full details.

nav:title

This renders the i18n title of a specific navigation item passed to it; for use in custom menu rendering.

<nav:primary scope="admin" id="nav" class="admin" custom="true">
   <li><nav:title item="${item}"/></li>
</nav:primary>

See the menu tag reference for full details.

nav:set

You can call this tag from inside a controller or GSP if you need to define request-specific parameters.

You can "fudge" the current request's activation path or set the default scope to be used by navigation tags.

You may need to do this inside an error.gsp for example, or inside admin pages to reuse a generic theme that renders navigation using nav:primary.

<html>
<body>
    <!-- pretend we are in messages/inbox even though we are in a GSP with no controller -->
    <nav:set path="app/messages/inbox"/>
    <nav:set scope="admin"/>

    <!-- or set those together -->
    <nav:set path="app/messages/inbox" scope="admin"/>

    <!-- these will use whatever the current active path and scope are -->
    <nav:primary/>
    <nav:secondary/>

    <p>Something went wrong!</p>
</body>
</html>

See the set tag reference for full details.

7 UI Extensions

Several simple UI Extensions are included in platform-core.

The tags supplied make it trivial to render links to controllers and actions using i18n messages, display messages to the end user, and render buttons and labels in i18n friendly ways.

7.1 Tags

Linking tags

The smartLink tag renders links for controllers and actions, automatically working out the text of the link using i18n.

<%-- Link to default action of BooksController --%>
<p:smartLink controller="books"/>

<%-- Link to list action of current controller --%>
<p:smartLink action="list"/>

These will use i18n messages located by convention of the form: action.controllerName.actionName

Label tag

The label tag will render a <label> with the text optionally loaded from i18n:

<p:label text="field.user.name"/>

See label for full details of the attributes, which include passing arguments to the i18n message.

Button tag

The button tag will render a text-based button using either a <button>, <input type="submit"> or <a> tag, with the text optionally loaded from i18n:

<p:button text="button.save"/>

See button for full details of the attributes, which include setting the kind of button rendered and passing arguments to the i18n message.

Display message tag

The displayMessage tag works with the displayMessage and displayFlashMessage controller methods to make it easy to render messages to the user in a uniform way.

<p:displayMessage/>

See displayMessage for full details of the attributes and the displayMessage and displayFlashMessage controller methods.

The tag will render both request and flash messages, and wraps them in a div with CSS classes according to the type of message.

Branding tags

There are several simple but useful site branding tags included. Commonly to be used in site footers and emails templates.

  • organization - Renders the name of the business, taken from Config var plugin.platformCore.organization.name
  • siteName - Renders the name of the site/product, taken from Config var plugin.platformCore.site.name
  • siteURL - Renders an absolute URL for the root of the site
  • siteLink - Renders an absolute link to the site, with the site name as the link text
  • year - Renders the current year, for use in copyright footers

7.2 Properties

New auto-namespaced equivalents of session, request and flash attributes are added to all controllers and taglibs exposed by plugins.

These properties are pluginRequestAttributes, pluginSession and pluginFlash.

They allow you to access these attributes from plugin code without having to worry about key name clashes with other plugins or the application:

class MyController {
	def beginPasswordReset = {
		pluginSession.resetMode = true
		pluginFlash.resetToken = resetTokenFactory.newTicket()
	}
}

7.3 Beans and utilities

There are a some UI utility classes and beans available:
  • grailsUiExtensions - Provides methods for setting and getting displayMessages
  • TagLibUtils - Provides helper functions for manipulating attributes, CSS class name lists etc.

8 Injection API

This API is partly implemented but for internal use only at this time.

9 Convention API

This API is partly implemented but for internal use only at this time.