Using Metro - A Transfer ORM Audit Observer

2009 January 08
by Paul Marcotte

The Transfer ORM Event Model provides an API for notifying components in your application of events in the Transfer object life cycle. You can use it to setup your component dependencies as demonstrated by Brian Kotek's TDOBeanInjectorObserver (part of his ColdSpring Utils library), or set specific properties on the Transfer object prior to creating or updating the object (see Bob Silverberg's post on the subject). Depending on your application needs, you can also tap into other Transfer events to create an audit trail and history for specific classes in your model. That's where the idea for the TransferAuditObserver (which is included in the Metro security package) was born. Setting CreateDate or LastModifiedDate properties on an object didn't sit well with me, because the observer was acting on the object state. Instead, I wanted an observer that recorded the events and state of of the object when those events were fired. Therefore, the TransferAuditObserver registers itself to listen for AfterCreate, AfterUpdate and BeforeDelete events, depending on a simple configuration map.

To enable the TransferAuditObserver, you will need to run the sql file in the Metro security package, then include the transfer object definitions in your Transfer xml.

<includes>
<include path="/metro/security/transfer.xml" />
</includes>

The final configuration required is to create the ColdSpring bean definition for the audit map and observer. <bean id="AuditMap" class="coldspring.beans.factory.config.MapFactoryBean">
<property name="sourceMap">
<map>
<entry key="create">
<value>security.User</value>
</entry>
<entry key="update">
<value>security.User</value>
</entry>
<entry key="delete">
<value>security.User</value>
</entry>
</map>
</property>
</bean>

<bean id="TransferAuditObserver" class="metro.security.TransferAuditObserver" lazy-init="false">
<constructor-arg name="transfer">
<ref bean="transfer" />
</constructor-arg>
<constructor-arg name="auditMap">
<ref bean="auditMap" />
</constructor-arg>
<property name="SecurityService">
<ref bean="SecurityService" />
</property>
</bean>

For each entry in the audit map, you can specify a create, update and delete key, the value for each key may contain a list of class names for which the observer will record an audit record. The example above is taken from the xml bean definitions for the Metro unit tests.

Below is the complete code for the TransferAuditObserver.

<cfcomponent displayname="TransferAuditObserver" output="false" hint="I am a Transfer AfterCreate, AfterUpdate and BeforeDelete observer.">

<cffunction name="init" access="public" output="false" returntype="any" hint="I return the TransferAuditObserver">
<cfargument name="transfer" type="transfer.com.Transfer" required="true">
<cfargument name="auditMap" type="struct" required="true">
<cfset setTransfer(arguments.transfer)>
<cfif StructKeyExists(arguments.auditMap,"create")>
<cfset setCreateList(arguments.auditMap['create'])>
<cfset getTransfer().addAfterCreateObserver(this)>
</cfif>
<cfif StructKeyExists(arguments.auditMap,"update")>
<cfset setUpdateList(arguments.auditMap['update'])>
<cfset getTransfer().addAfterUpdateObserver(this)>
</cfif>
<cfif StructKeyExists(arguments.auditMap,"delete")>
<cfset setDeleteList(arguments.auditMap['delete'])>
<cfset getTransfer().addBeforeDeleteObserver(this)>
</cfif>
<cfreturn this>
</cffunction>

<cffunction name="actionAfterCreateTransferEvent" access="public" output="false" returntype="void" hint="I determine if the event object class name is present in getCreateList(), if found, I invoke createAuditRecord().">
<cfargument name="event" hint="The event object" type="transfer.com.events.TransferEvent" required="true">
<cfset var obj = arguments.event.getTransferObject()>
<cfif ListContains(getCreateList(),obj.getClassName())>
<cfset createAuditRecord(obj,'create')>
</cfif>
</cffunction>

<cffunction name="actionAfterUpdateTransferEvent" access="public" output="false" returntype="void" hint="I determine if the event object class name is present in getUpdateList(), if found, I invoke createAuditRecord().">
<cfargument name="event" hint="The event object" type="transfer.com.events.TransferEvent" required="true">
<cfset var obj = arguments.event.getTransferObject()>
<cfif ListContains(getUpdateList(),obj.getClassName())>
<cfset createAuditRecord(obj,'update')>
</cfif>
</cffunction>

<cffunction name="actionBeforeDeleteTransferEvent" access="public" returntype="void" output="false" hint="I determine if the event object class name is present in getDeleteList(), if found, I invoke createAuditRecord().">
<cfargument name="event" hint="The event object" type="transfer.com.events.TransferEvent" required="Yes">
<cfset var obj = arguments.event.getTransferObject()>
<cfif ListContains(getDeleteList(),obj.getClassName())>
<cfset createAuditRecord(obj,'delete')>
</cfif>
</cffunction>

<!--- private --->

<cffunction name="createAuditRecord" access="private" output="true" returntype="void" hint="I create an Audit Record.">
<cfargument name="obj" type="transfer.com.TransferObject" required="true" hint="The event object">
<cfargument name="action" type="string" required="true" hint="The action that occurred (create|update).">
<cfset var pkValue = "">
<cfset var pkName = getTransfer().getTransferMetadata(arguments.obj.getClassName()).getPrimaryKey().getName()>
<cfset var audit = getTransfer().new("security.Audit")>
<cfinvoke component="#arguments.obj#" method="get#pkName#" returnvariable="pkValue">
<cfset audit.setObjectClass(arguments.obj.getClassname())>
<cfset audit.setPrimaryKey(pkValue)>
<cfset audit.setAction(arguments.action)>
<cfset audit.setAuditDate(now())>
<cfset audit.setJsonMemento(arguments.obj.toJSON())>
<cfif (getSecurityService().userInSession())>
<cfset audit.setUser(getSecurityService().getCurrentUser())>
</cfif>
<cfset getTransfer().save(audit)>
</cffunction>

<cffunction name="setCreateList" access="private" returntype="void" output="false" hint="I set the createList property.">
<cfargument name="CreateList" type="string" required="true">
<cfset variables.CreateList = arguments.CreateList >
</cffunction>

<cffunction name="getCreateList" access="private" returntype="string" output="false" hint="I return the createList property.">
<cfreturn variables.CreateList />
</cffunction>

<cffunction name="setUpdateList" access="private" returntype="void" output="false" hint="I set the updateList property.">
<cfargument name="UpdateList" type="string" required="true">
<cfset variables.UpdateList = arguments.UpdateList >
</cffunction>

<cffunction name="getUpdateList" access="private" returntype="string" output="false" hint="I return the updateList property.">
<cfreturn variables.UpdateList />
</cffunction>

<cffunction name="setDeleteList" access="private" returntype="void" output="false" hint="I set the deleteList property.">
<cfargument name="DeleteList" type="string" required="true">
<cfset variables.DeleteList = arguments.DeleteList >
</cffunction>

<cffunction name="getDeleteList" access="private" returntype="string" output="false" hint="I return the deleteList property.">
<cfreturn variables.DeleteList />
</cffunction>

<!--- dependencies --->

<cffunction name="setTransfer" access="public" returntype="void" output="false" hint="I set a reference to Transfer.">
<cfargument name="Transfer" type="transfer.com.Transfer" required="true">
<cfset variables.Transfer = arguments.Transfer >
</cffunction>

<cffunction name="getTransfer" access="public" returntype="transfer.com.Transfer" output="false" hint="I return Transfer.">
<cfreturn variables.Transfer />
</cffunction>

<cffunction name="setSecurityService" access="public" returntype="void" output="false" hint="I set a reference to the SecurityService.">
<cfargument name="SecurityService" type="any" required="true">
<cfset variables.SecurityService = arguments.SecurityService >
</cffunction>

<cffunction name="getSecurityService" access="public" returntype="any" output="false" hint="I return the SecurityService.">
<cfreturn variables.SecurityService />
</cffunction>

</cfcomponent>

The observer will only register for those events that are found in the audit map. By convention, they are "create", "update" and "delete".

For each audit record, a snapshot of the object state is serialized as JSON. I incorporated this into the Metro core Decorator after reading Bob Silverberg's post on using transfer metadata to create a memento.

Thus, with little effort, one can create a diff tool for comparing an object's state between records and the ability to revert to a previous state.