Note that in onFlushDirty() a session is started on the same connection as the parent, whilst in postFlush() a session is started afresh from the sessionFactory with its own transaction.
In session startup code:
Code:
if(readOnly)
{
session = sessionFactory.openSession();
}
else
{
AuditInterceptor interceptor = new AuditInterceptor();
session = sessionFactory.openSession(interceptor);
interceptor.setUser(user);
interceptor.setSessionFactory(sessionFactory);
interceptor.setConnection(session.connection());
}
Persistable interface (akin to Rob's Auditable interface, but I have more uses for it) - ALL of my hibernate objects implement this interface
Code:
package persistence;
import java.io.Serializable;
public interface Persistable extends java.io.Serializable
{
Serializable getIdentifier();
boolean isNew(); // Not used by AuditInterceptor
}
The AuditInterceptor:
Code:
package persistence;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.sql.Connection;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.CallbackException;
import org.hibernate.EmptyInterceptor;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.type.Type;
import security.User;
/**
* Hibernate Interceptor for logging saves, updates and deletes to the Audit
* table.
* Does not handle Components! See Rob's original code for this.
* We only have a couple of components for which the added complexity
* was not worth it. Components handled with toString().
*
* @author Stewart Cambridge. Adapted from code by Rob Monie <a
* href="http://www.hibernate.org/318.html">Audit Logging</a>
*/
public class AuditInterceptor extends EmptyInterceptor
{
private Log log = LogFactory.getLog( AuditInterceptor.class );
private SessionFactory sessionFactory = null;
private Connection connection = null;
private User user = null;
private Map<AuditRecord, Persistable> auditRecords = new HashMap<AuditRecord, Persistable>();
// package access
AuditInterceptor()
{
}
// package access
AuditInterceptor( SessionFactory sessionFactory, User user )
{
this.sessionFactory = sessionFactory;
this.user = user;
}
void setConnection( Connection connection )
{
this.connection = connection;
}
void setSessionFactory( SessionFactory sessionFactory )
{
this.sessionFactory = sessionFactory;
}
void setUser( User user )
{
this.user = user;
}
public boolean onFlushDirty( Object object, Serializable id,
Object[] newValues, Object[] oldValues,
String[] properties, Type[] types )
throws CallbackException
{
log.debug( "AuditInterceptor.onFlushDirty() for " + object.toString() );
if ( object instanceof Persistable )
{
Persistable newPersistable = (Persistable) object;
Session session = null;
try
{
session = sessionFactory.openSession(connection);
Persistable oldPersistable = (Persistable) session.get(
newPersistable.getClass(), newPersistable.getIdentifier() );
logChanges( newPersistable, oldPersistable, AuditRecord.UPDATE );
}
catch ( Exception e )
{
log.error( e.getMessage(), e );
}
finally
{
if ( session != null )
{
session.close();
}
}
}
// only return true if we modified any of the Object[] newValues array
return false;
}
public boolean onSave( Object object, Serializable id, Object[] newValues,
String[] properties, Type[] types )
throws CallbackException
{
log.debug( "AuditInterceptor.onSave() for " + object.toString() );
if ( object instanceof Persistable )
{
try
{
logChanges( (Persistable) object, null, AuditRecord.INSERT );
}
catch ( Exception e )
{
log.error( e.getMessage(), e );
}
}
// only return true if we modified any of the Object[] newValues array
return false;
}
public void onDelete( Object object, Serializable id, Object[] newValues,
String[] properties, Type[] types )
throws CallbackException
{
log.debug( "AuditInterceptor.onDelete() for " + object.toString() );
if ( object instanceof Persistable )
{
try
{
logChanges( null, (Persistable) object, AuditRecord.DELETE );
}
catch ( Exception e )
{
log.error( e.getMessage(), e );
}
}
}
public void postFlush( Iterator entities ) throws CallbackException
{
log.debug( "AuditInterceptor.postFlush()" );
Session session = null;
try
{
session = sessionFactory.openSession();
session.beginTransaction();
for ( AuditRecord auditRecord : auditRecords.keySet() )
{
// get the id value after the new entity has been persisted
if ( auditRecord.getAction().equals( AuditRecord.INSERT ) )
{
Persistable persistable = auditRecords.get( auditRecord );
auditRecord.setIdentifier( persistable.getIdentifier().toString() );
// brand new collections will appear as [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1]
// unless we get all the id values after the new owning entity has been persisted
if(auditRecord.getAfterValue().contains("-1")) // -1 indicates unsaved value
{
Field field = getField(persistable.getClass(), auditRecord.getField());
field.setAccessible( true );
try
{
Object object = field.get( persistable );
if(object instanceof Collection)
{
auditRecord.setAfterValue(toString((Collection) object));
}
}
catch(IllegalAccessException e)
{
log.error( "IllegalAccessException on accessing field " +
auditRecord.getField() + " on entity " +
persistable + ": " +
e.getMessage(), e);
}
}
}
log.debug( "AuditInterceptor: saving AuditRecord "
+ auditRecord.toString() );
session.save( auditRecord );
}
}
catch ( HibernateException e )
{
log.error( e.getMessage(), e );
throw new CallbackException( e );
}
finally
{
auditRecords.clear();
if ( session != null )
{
try
{
session.getTransaction().commit();
session.close();
}
catch ( HibernateException e )
{
log.error( e.getMessage(), e );
throw new CallbackException( e );
}
}
}
}
public void afterTransactionCompletion( Transaction transaction )
{
// clear any audit log records potentially remaining from a rolled back
// transaction
auditRecords.clear();
}
/**
* Logs changes to persistent data
*
* @param newPersistable
* the object being saved, updated
* @param oldPersistable
* the existing object in the database. Used for updates and deletes
* @param event
* the type of event being logged.
* @throws IllegalArgumentException
* @throws IllegalAccessException
* @throws InvocationTargetException
*/
private void logChanges( Persistable newPersistable,
Persistable oldPersistable, String event )
throws IllegalArgumentException,
IllegalAccessException,
InvocationTargetException
{
// get an array of all fields in the class including those in
// superclasses if this is a subclass.
Field[] fields = getAllFields((newPersistable == null ) ? oldPersistable.getClass() : newPersistable.getClass(), null );
// Iterate through all the fields in the object
for ( int ii = 0; ii < fields.length; ii++ )
{
// make private fields accessible so we can access their values
fields[ ii ].setAccessible( true );
// if the current field is static, transient or final then don't log
// it as
// these modifiers are v.unlikely to be part of the data model.
if ( Modifier.isTransient( fields[ ii ].getModifiers() )
|| Modifier.isFinal( fields[ ii ].getModifiers() )
|| Modifier.isStatic( fields[ ii ].getModifiers() ) )
{
continue;
}
String beforeValue = "";
String afterValue = "";
if ( oldPersistable != null )
{
beforeValue = getFieldValue(fields[ ii ].get( oldPersistable ));
}
if ( newPersistable != null )
{
afterValue = getFieldValue(fields[ ii ].get( newPersistable ));
}
if ( event.equals( AuditRecord.UPDATE ) )
{
if ( afterValue.equals( beforeValue ) )
{
// Values haven't changed so loop to next property
continue;
}
else
{
auditRecords.put(
new AuditRecord( user, newPersistable, fields[ ii ]
.getName(), event, beforeValue, afterValue ),
newPersistable );
}
}
else if ( event.equals( AuditRecord.DELETE ) )
{
auditRecords.put( new AuditRecord( user, oldPersistable,
fields[ ii ].getName(), event, beforeValue, afterValue ),
oldPersistable );
}
else if ( event.equals( AuditRecord.INSERT ) )
{
auditRecords.put( new AuditRecord( user, newPersistable,
fields[ ii ].getName(), event, beforeValue, afterValue ),
newPersistable );
}
}
}
private String getFieldValue(Object object)
{
String value = "";
try
{
if ( object != null )
{
value = (object instanceof Collection) ? toString((Collection) object) : toString(object);
}
}
catch ( Exception e )
{
log.warn( e.getMessage(), e );
}
return value;
}
private static final String START = "[";
private static final String END = "]";
private static final String DELIM = ", ";
private String toString(Collection collection)
{
StringBuilder str = new StringBuilder(START);
for(Object object : collection)
{
str.append(toString(object));
str.append(DELIM);
}
if(!collection.isEmpty())
str.delete(str.lastIndexOf(DELIM), str.length());
str.append(END);
return str.toString();
}
private String toString(Object object)
{
return (object instanceof Persistable) ? ((Persistable) object).getIdentifier().toString() : object.toString();
}
/**
* Returns an array of all fields used by this object from its class and all
* superclasses.
*
* @param objectClass
* the class
* @param fields
* the current Field list
* @return an array of Fields
*/
private Field[] getAllFields( Class objectClass, Field[] fields )
{
Field[] newFields = objectClass.getDeclaredFields();
int fieldsSize = ( fields == null ) ? 0 : fields.length;
int newFieldsSize = ( newFields == null ) ? 0 : newFields.length;
Field[] totalFields = new Field[ fieldsSize + newFieldsSize ];
if ( fieldsSize > 0 )
System.arraycopy( fields, 0, totalFields, 0, fieldsSize );
if ( newFieldsSize > 0 )
System.arraycopy( newFields, 0, totalFields, fieldsSize,
newFieldsSize );
Class superClass = objectClass.getSuperclass();
return ( superClass != null &&
!superClass.equals( Object.class ) )
? getAllFields( superClass, totalFields )
: totalFields;
}
private Field getField( Class clazz, String fieldName)
{
Field field = null;
try
{
field = clazz.getDeclaredField(fieldName);
}
catch(NoSuchFieldException e)
{
Class superClass = clazz.getSuperclass();
if( superClass != null &&
!superClass.equals( Object.class ))
field = getField(superClass, fieldName);
}
return field;
}
}
The AuditRecord object:
Code:
package persistence;
import java.io.Serializable;
import java.util.Date;
import java.util.HashSet;
import security.User;
public class AuditRecord implements Serializable
{
public static final String INSERT = "INSERT";
public static final String UPDATE = "UPDATE";
public static final String DELETE = "DELETE";
static final HashSet<String> actions = new HashSet<String>();
private long timeMillis = System.currentTimeMillis();
private User user = null;
private String entity = "";
private String field = "";
private String identifier = "";
private String beforeValue = "";
private String afterValue = "";
private String action = "";
static
{
actions.add( INSERT );
actions.add( UPDATE );
actions.add( DELETE );
}
public AuditRecord()
{
}
public AuditRecord( User user, Persistable persistable, String field,
String action, String beforeValue, String afterValue )
{
this.user = user;
this.entity = persistable.getClass().getSimpleName();
this.field = field;
this.identifier = persistable.getIdentifier().toString();
this.setAction( action );
this.beforeValue = beforeValue;
this.afterValue = afterValue;
}
public String getAfterValue()
{
return this.afterValue;
}
public void setAfterValue( String afterValue )
{
this.afterValue = afterValue;
}
public String getBeforeValue()
{
return this.beforeValue;
}
public void setBeforeValue( String beforeValue )
{
this.beforeValue = beforeValue;
}
public String getEntity()
{
return this.entity;
}
public void setEntity( String entity )
{
this.entity = entity;
}
public String getField()
{
return this.field;
}
public void setField( String field )
{
this.field = field;
}
public String getIdentifier()
{
return this.identifier;
}
public void setIdentifier( String identifier )
{
this.identifier = identifier;
}
public long getTimeMillis()
{
return this.timeMillis;
}
public void setTimeMillis( long timeMillis )
{
this.timeMillis = timeMillis;
}
public User getUser()
{
return this.user;
}
public void setUser( User user )
{
this.user = user;
}
public String getAction()
{
return this.action;
}
public void setAction( String action )
{
if ( actions.contains( action ) )
{
this.action = action;
}
else
{
throw new IllegalStateException( "Action must be one of: "
+ actions.toString() );
}
}
public boolean equals( Object object )
{
if ( object == this ) return true;
if ( !( object instanceof AuditRecord ) ) return false;
AuditRecord that = (AuditRecord) object;
return ( this.timeMillis == that.timeMillis
&& this.user.equals( that.user )
&& this.entity.equals( that.entity )
&& this.field.equals( that.field ) && this.identifier
.equals( that.identifier ) );
}
public int hashCode()
{
return ( ( 37 + ( (int) ( timeMillis ^ ( timeMillis >>> 32 ) ) ) )
+ (user.hashCode() * entity.hashCode() * field.hashCode() * identifier.hashCode()) );
}
public String toString()
{
return "[" + timeMillis + "] [" + new Date(timeMillis).toString() + "] [" + user.toString() + "] ["
+ entity + "] [" + field + "] [" + identifier + "] [" + action
+ "] [" + beforeValue + "] [" + afterValue + "]";
}
}