Extending Audit Logging Plugin to track changes to Persistent Collections

23 / Jan / 2012 by Ankur Tripathi 2 comments

In one of our project we needed to maintain history of domain objects when they are updated. We saw Grails Audit Logging Plugin as a good candidate. But later, found that it doesn’t take care of persistent collections. So with help of my colleague Vivek and this Stack Overflow thread, we extended this plugin without making it inline, to handle this limitation.

Audit Logging plugin provides a bean named auditLogListener to handle Hibernate events and provide handlers in Grails Domain classes with old values map and new values map. So what we have to do is create a class named CustomAuditLogListener which extends AuditLogListener from the plugin and overrides the onPostUpdate() method. Implemetation for this class is:

import org.codehaus.groovy.grails.plugins.orm.auditable.AuditLogListener
import org.hibernate.collection.PersistentCollection
import org.hibernate.engine.CollectionEntry
import org.hibernate.engine.PersistenceContext
import org.hibernate.event.PostUpdateEvent

class CustomAuditLogListener extends AuditLogListener {

    @Override
    void onPostUpdate(final PostUpdateEvent event) {
        if (isAuditableEntity(event)) {
            log.trace "${event.getClass()} onChange handler has been called"
            onChange(event)
        }
    }

    private void onChange(final PostUpdateEvent event) {
        def entity = event.getEntity()
        String entityName = entity.getClass().getName()
        def entityId = event.getId()

        // object arrays representing the old and new state
        def oldState = event.getOldState()
        def newState = event.getState()

        List<String> propertyNames = event.getPersister().getPropertyNames()
        Map oldMap = [:]
        Map newMap = [:]

        if (propertyNames) {
            for (int index = 0; index < newState.length; index++) {
                if (propertyNames[index]) {
                    if (oldState) {
                        populateOldStateMap(oldState, oldMap, propertyNames[index], index)
                    }
                    if (newState) {
                        newMap[propertyNames[index]] = newState[index]
                    }
                }
            }
        }

        if (!significantChange(entity, oldMap, newMap)) {
            return
        }

        // allow user's to over-ride whether you do auditing for them.
        if (!callHandlersOnly(event.getEntity())) {
            logChanges(newMap, oldMap, event, entityId, 'UPDATE', entityName)
        }
         executeHandler(event, 'onChange', oldMap, newMap)
        return
    }

    private populateOldStateMap(def oldState, Map oldMap, String keyName, index) {
        def oldPropertyState = oldState[index]
        if (oldPropertyState instanceof PersistentCollection) {
            PersistentCollection pc = (PersistentCollection) oldPropertyState;
            PersistenceContext context = sessionFactory.getCurrentSession().getPersistenceContext();
            CollectionEntry entry = context.getCollectionEntry(pc);
            Object snapshot = entry.getSnapshot();
            if (pc instanceof List) {
                oldMap[keyName] = Collections.unmodifiableList((List) snapshot);
            }
            else if (pc instanceof Map) {
                oldMap[keyName] = Collections.unmodifiableMap((Map) snapshot);
            }
            else if (pc instanceof Set) {
                //Set snapshot is actually stored as a Map
                Map snapshotMap = (Map) snapshot;
                oldMap[keyName] = Collections.unmodifiableSet(new HashSet(snapshotMap.values()));
            }
            else {
                oldMap[keyName] = pc;
            }
        } else {
            oldMap[keyName] = oldPropertyState
        }
    }
}

Now we need to register CustomAuditLogListener class as implementation for auditLogListener which will be done in resources.groovy. The bean has to be defined in resources.groovy as:

 auditLogListener(CustomAuditLogListener) {
        sessionFactory   = ref('sessionFactory')
        verbose          = application.config?.auditLog?.verbose?:false
        transactional    = application.config?.auditLog?.transactional?:false
        sessionAttribute = application.config?.auditLog?.sessionAttribute?:""
        actorKey         = application.config?.auditLog?.actorKey?:""
    }

Now we will be able to fetch older values for persistent collections in onChange handler as documented in plugin documentation.

Hope you find this helpful.

Ankur Tripathi
ankur@intelligrape.com

FOUND THIS USEFUL? SHARE IT

comments (2)

  1. John D Giotta

    This works well and good except when the order of the Collection changes or when comparing null to an empty Collection.

    Reply

Leave a comment -