Grails - Tracking Principals

We use the Grails auto timestamp feature in nearly all of our domain classes. It basically allows the definition of two special domain class properties dateCreated and lastUpdated and automatically sets the creation and modification date whenever a domain object is inserted or updated.

In addition to dateCreated and lastUpdated we wanted to have a way to define two additional properties userCreated and userUpdated to save the principal who created, updated or deleted a domain class (deletion because we have audit log tables that track all table changes, so when an entry is deleted and the principal is set before, we can see who deleted an entry).

PersistenceEventListener

Grails provides the concept of GORM events, so we thought its implementation might be a good hint on how to implement our requirement for having userCreated and userUpdated. And indeed, we found DomainEventListener, a descendant class of AbstractPersistenceEventListener. It turns out that DomainEventListener is responsible for executing the GORM event hooks on domain object inserts, updates and deletes.

The event listener is registered at the application context as the PersistenceListener interface (which is implemented by AbstractPersistenceListener) extends from Spring’s ApplicationListener and therefore actually uses the Spring event system.

In order to create a custom persistence listener, we just have to extend AbstractPersistenceEventListener and listen for the GORM events which are useful to us. Here is the implementation we ended up with:

@Log4j
class PrincipalPersistenceListener extends AbstractPersistenceEventListener {

    public static final String PROPERTY_PRINCIPAL_UPDATED = 'userUpdated'
    public static final String PROPERTY_PRINCIPAL_CREATED = 'userCreated'

    SpringSecurityService springSecurityService

    PrincipalPersistenceListener(Datastore datastore) {
        super(datastore)
    }

    @Override
    protected void onPersistenceEvent(AbstractPersistenceEvent event) {

        def entityObject = event.entityObject

        if (hasPrincipalProperty(entityObject)) {
            switch (event.eventType) {
                case EventType.PreInsert:
                    setPrincipalProperties(entityObject, true)
                    break

                case EventType.Validation:
                    setPrincipalProperties(entityObject, entityObject.id == null)
                    break

                case EventType.PreUpdate:
                    setPrincipalProperties(entityObject, false)
                    break

                case EventType.PreDelete:
                    setPrincipalProperties(entityObject, false)
                    break
            }
        }
    }

    protected boolean hasPrincipalProperty(def entityObject) {
        return entityObject.metaClass.hasProperty(entityObject, PROPERTY_PRINCIPAL_UPDATED) || entityObject.metaClass.hasProperty(entityObject, PROPERTY_PRINCIPAL_CREATED)
    }

    protected void setPrincipalProperties(def entityObject, boolean insert)  {
        def currentUser = springSecurityService.currentUser

        if (currentUser instanceof User) {
            def propertyUpdated = entityObject.metaClass.getMetaProperty(PROPERTY_PRINCIPAL_UPDATED)
            if (propertyUpdated != null)  {
                propertyUpdated.setProperty(entityObject, currentUser.uuid)
            }

            if (insert)  {
                def propertyCreated = entityObject.metaClass.getMetaProperty(PROPERTY_PRINCIPAL_CREATED)
                if (propertyCreated != null)  {
                    propertyCreated.setProperty(entityObject, currentUser.uuid)
                }
            }
        }
    }

    @Override
    boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
        return eventType.isAssignableFrom(PreInsertEvent) ||
                eventType.isAssignableFrom(PreUpdateEvent) ||
                eventType.isAssignableFrom(PreDeleteEvent) ||
                eventType.isAssignableFrom(ValidationEvent)
    }
}

As you can see in the code above, the implementation intercepts the PreInsert, PreUpdate and PreDelete events. If any of these event types is thrown, the code checks the affected domain object for the existence of either the userCreated or userUpdated property. If available, it uses the springSecurityService to access the currently logged-in principal and uses its uuid property, as this is the unique identifier of our users in this application.

To register the PrincipalPersistenceListener and attach it to a Grails datastore, we need to add the following code to BootStrap.groovy:

def ctx = grailsApplication.mainContext
ctx.eventTriggeringInterceptor.datastores.each { key, datastore ->

    def listener = new PrincipalPersistenceListener(datastore)
    listener.springSecurityService = springSecurityService

    ctx.addApplicationListener(listener)
}

To make this work, the springSecurityService needs to be injected, the same is true for grailsApplication.

But that’s all we have to do to support our new domain class properties userCreated and userUpdated. The last step is to add both properties to the domain class(es) we want to track.

Conclusion

Grails integrates with Spring’s event mechanism and provides the AbstractPersistenceEventListener base class to listen to certain GORM events. Grails uses this mechanism internally for example for the GORM event hooks but it can of course be used by the application logic too. This article showed how to introduce support for userCreated and userUpdated which are similar to dateCreated and lastUpdated but store the principal how created, updated or deleted a domain object.