Member Menu
 
 Monthly JBoss newsletter:
 
Hibernate Books
CaveatEmptor

Audit Logging

It appears to be a common request for users of Hibernate to want to log changes to data in an Audit History table. I'm yet to see any solution that works in the forums, mainly due to problems with getting pre-update values from the database to compare with new values.

Interceptor Approach

The following audit logging solution explicitly gets the current object from the database and compares values to create the audit history before saving. This is achieved using reflection so that any class can be audited regardless of its id type. It can also cater for component and polymorphic mappings as well. Fields with static, final or transient modifiers are not logged but this can be easily changed. This code does impose some tagging of your hibernate classes with interfaces though which could be potentially be modified to use a more polymorphic approach which I plan to do when I get some time. Any improvements will be posted here.

At the moment these are the requirements for this to work:

1. All classes to be audited should implement the Auditable interface. This doesn't need to define any specific methods but if all your ids are Integers you could define a getId() method that returns an Integer.

2. Any component mappings such as Address for a person or company should implement an Interface called Component, or change the code to something else you'd prefer. This tags a property as a component so that the Audit log interceptor will climb into the componenent class and compare its old/new values along with the owning class.

3. Every class should override toString() and return something useful for logging such as the id. The reason for this is so that when it tries to compare parent classes, it can compare something consistent and useful for the log. Some people may not like tying toString() to such a specific purpose so may choose to do something else.

4. As mentioned in other posts you must use a different sessionFactory for your auditLogging than your usual DAOs. There is good discussion in other places about the reasons for this.

5.This will log all properties in your object unless they have static, final or transient modifiers.

6. The getUserName() method ties this code to Acegi Security so change if you do not use this security framework.

I initially tried to get this working using Hibernate event listeners but this same code would not work for some reason. It seems to be a bit more fragile in there. From what I gather, the event listeners are the way forward so if anyone gets it working in there please post your code.

/*
 * Created on May 18, 2005
 *
 * Created By Rob Monie
 */
package com.mycompany.model.audit;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import net.sf.acegisecurity.Authentication;
import net.sf.acegisecurity.UserDetails;
import net.sf.acegisecurity.context.ContextHolder;
import net.sf.acegisecurity.context.security.SecureContext;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.CallbackException;
import org.hibernate.EntityMode;
import org.hibernate.HibernateException;
import org.hibernate.Interceptor;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.type.Type;

/**
 * @author Rob
 * 
 * Hibernate Interceptor for logging saves, updates and deletes to the
 * AuditLogRecord Table
 */
public class AuditLogInterceptor implements Interceptor {

    private Log log = LogFactory.getLog(AuditLogInterceptor.class);

    private SessionFactory sessionFactory;
    private static final String UPDATE = "update";
    private static final String INSERT = "insert";
    private static final String DELETE = "delete";

    /**
     * @param sessionFactory
     *            The sessionFactory to set.
     */
    public void setSessionFactory(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    private Set inserts = new HashSet();
    private Set updates = new HashSet();
    private Set deletes = new HashSet();

    /*
     * (non-Javadoc)
     * 
     * @see net.sf.hibernate.Interceptor#onLoad(java.lang.Object,
     *      java.io.Serializable, java.lang.Object[], java.lang.String[],
     *      net.sf.hibernate.type.Type[])
     */
    public boolean onLoad(Object arg0, Serializable arg1, Object[] arg2, String[] arg3, Type[] arg4)
            throws CallbackException {
        // TODO Auto-generated method stub
        return false;
    }

    /*
     * (non-Javadoc)
     * 
     * @see net.sf.hibernate.Interceptor#onFlushDirty(java.lang.Object,
     *      java.io.Serializable, java.lang.Object[], java.lang.Object[],
     *      java.lang.String[], net.sf.hibernate.type.Type[])
     */
    public boolean onFlushDirty(Object obj, Serializable id, Object[] newValues, Object[] oldValues,
            String[] properties, Type[] types) throws CallbackException {

        if (obj instanceof Auditable) {
            
            Session session = sessionFactory.openSession();
            Class objectClass = obj.getClass();
            String className = objectClass.getName();

            // Just get the class name without the package structure 
            String[] tokens = className.split("\\.");
            int lastToken = tokens.length - 1;
            className = tokens[lastToken];

            // Use the id and class to get the pre-update state from the database
            Serializable persistedObjectId = getObjectId(obj);
            Object preUpdateState = session.get(objectClass,  persistedObjectId);
            
            try {
                
                logChanges(obj, preUpdateState, null, persistedObjectId.toString(), UPDATE, className);
                
            } catch (IllegalArgumentException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            
            session.close();
        }
        
        return false;
    }

    /*
     * (non-Javadoc)
     * 
     * @see net.sf.hibernate.Interceptor#onSave(java.lang.Object,
     *      java.io.Serializable, java.lang.Object[], java.lang.String[],
     *      net.sf.hibernate.type.Type[])
     */
    public boolean onSave(Object obj, Serializable id, Object[] newValues, String[] properties, Type[] types)
            throws CallbackException {
        if (obj instanceof Auditable) {
            
            try {
                Class objectClass = obj.getClass();
                String className = objectClass.getName();
                String[] tokens = className.split("\\.");
                int lastToken = tokens.length - 1;
                className = tokens[lastToken];
                
                logChanges(obj, null, null, null, INSERT, className);
                
            } catch (IllegalArgumentException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        return false;
    }

    /*
     * (non-Javadoc)
     * 
     * @see net.sf.hibernate.Interceptor#onDelete(java.lang.Object,
     *      java.io.Serializable, java.lang.Object[], java.lang.String[],
     *      net.sf.hibernate.type.Type[])
     */
    public void onDelete(Object obj, Serializable id, Object[] newValues, String[] properties, Type[] types)
            throws CallbackException {
        

        if (obj instanceof Auditable) {

            try {
                
                Class objectClass = obj.getClass();
                String className = objectClass.getName();
                String[] tokens = className.split("\\.");
                int lastToken = tokens.length - 1;
                className = tokens[lastToken];
                
                logChanges(obj, null, null, id.toString(), DELETE, className);
                
            } catch (IllegalArgumentException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

        }

    }

    /*
     * (non-Javadoc)
     * 
     * @see net.sf.hibernate.Interceptor#preFlush(java.util.Iterator)
     */
    public void preFlush(Iterator arg0) throws CallbackException {
        // TODO Auto-generated method stub

    }

    /*
     * (non-Javadoc)
     * 
     * @see net.sf.hibernate.Interceptor#postFlush(java.util.Iterator)
     */
    public void postFlush(Iterator arg0) throws CallbackException {
        log.debug("In postFlush of AuditLogInterceptor..");

        Session session = sessionFactory.openSession();

        try {
            for (Iterator itr = inserts.iterator(); itr.hasNext();) {
                AuditLogRecord logRecord = (AuditLogRecord) itr.next();
                logRecord.setEntityId(getObjectId(logRecord.getEntity()).toString());
                session.save(logRecord);
            }
            for (Iterator itr = updates.iterator(); itr.hasNext();) {
                AuditLogRecord logRecord = (AuditLogRecord) itr.next();
                session.save(logRecord);
            }
            for (Iterator itr = deletes.iterator(); itr.hasNext();) {
                AuditLogRecord logRecord = (AuditLogRecord) itr.next();
                session.save(logRecord);
            }
        } catch (HibernateException e) {
            throw new CallbackException(e);
        } finally {
            inserts.clear();
            updates.clear();
            deletes.clear();
            session.flush();
            session.close();
        }

    }

    /*
     * (non-Javadoc)
     * 
     * @see net.sf.hibernate.Interceptor#isUnsaved(java.lang.Object)
     */
    public Boolean isUnsaved(Object arg0) {
        // TODO Auto-generated method stub
        return null;
    }

    /*
     * (non-Javadoc)
     * 
     * @see net.sf.hibernate.Interceptor#findDirty(java.lang.Object,
     *      java.io.Serializable, java.lang.Object[], java.lang.Object[],
     *      java.lang.String[], net.sf.hibernate.type.Type[])
     */
    public int[] findDirty(Object arg0, Serializable arg1, Object[] arg2, Object[] arg3, String[] arg4, Type[] arg5) {
        // TODO Auto-generated method stub
        return null;
    }

    /*
     * (non-Javadoc)
     * 
     * @see net.sf.hibernate.Interceptor#instantiate(java.lang.Class,
     *      java.io.Serializable)
     */
    public Object instantiate(Class arg0, Serializable arg1) throws CallbackException {
        // TODO Auto-generated method stub
        return null;
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.hibernate.Interceptor#isTransient(java.lang.Object)
     */
    public Boolean isTransient(Object arg0) {
        // TODO Auto-generated method stub
        return null;
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.hibernate.Interceptor#instantiate(java.lang.String,
     *      org.hibernate.EntityMode, java.io.Serializable)
     */
    public Object instantiate(String arg0, EntityMode arg1, Serializable arg2) throws CallbackException {
        // TODO Auto-generated method stub
        return null;
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.hibernate.Interceptor#getEntityName(java.lang.Object)
     */
    public String getEntityName(Object arg0) throws CallbackException {
        // TODO Auto-generated method stub
        return null;
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.hibernate.Interceptor#getEntity(java.lang.String,
     *      java.io.Serializable)
     */
    public Object getEntity(String arg0, Serializable arg1) throws CallbackException {
        // TODO Auto-generated method stub
        return null;
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.hibernate.Interceptor#afterTransactionBegin(org.hibernate.Transaction)
     */
    public void afterTransactionBegin(Transaction arg0) {
        // TODO Auto-generated method stub

    }

    /*
     * (non-Javadoc)
     * 
     * @see org.hibernate.Interceptor#beforeTransactionCompletion(org.hibernate.Transaction)
     */
    public void beforeTransactionCompletion(Transaction arg0) {
        // TODO Auto-generated method stub

    }

    /*
     * (non-Javadoc)
     * 
     * @see org.hibernate.Interceptor#afterTransactionCompletion(org.hibernate.Transaction)
     */
    public void afterTransactionCompletion(Transaction arg0) {
        // clear any audit log records potentially remaining from a rolled back
        // transaction
        inserts.clear();
        updates.clear();
        deletes.clear();

    }

    
  /**
   * Logs changes to persistent data
 * @param newObject the object being saved, updated or deleted
 * @param existingObject the existing object in the database.  Used only for updates
 * @param parentObject the parent object. Set only if passing a Component object as the newObject
 * @param persistedObjectId the id of the persisted object.  Used only for update and delete
 * @param event the type of event being logged.  Valid values are "update", "delete", "save"
 * @param className the name of the class being logged.  Used as a reference in the auditLogRecord
 * @throws IllegalArgumentException
 * @throws IllegalAccessException
 * @throws InvocationTargetException
 */
private void logChanges(Object newObject, Object existingObject, Object parentObject,
                        String persistedObjectId, String event, String className)
     throws IllegalArgumentException, IllegalAccessException, InvocationTargetException  {     

      Class objectClass = newObject.getClass();      
      //get an array of all fields in the class including those in superclasses if this is a subclass.
      Field[] fields = getAllFields(objectClass, null);

      // Iterate through all the fields in the object

      fieldIteration: 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 fieldIteration;
          }
          
          String fieldName = fields[ii].getName();
          if(! fieldName.equals("id")) {
             
          Class interfaces[] = fields[ii].getType().getInterfaces();
              for (int i = 0; i < interfaces.length;) {
                  if (interfaces[i].getName().equals("java.util.Collection")) {
                      continue fieldIteration;
                      
                  //If the field is a class that is a component (Hibernate mapping type) then iterate through its fields and log them
                  } else if(interfaces[i].getName().equals("com.mycompany.model.audit.Component")){
                      
                     
                      Object newComponent = fields[ii].get(newObject);
                      Object existingComponent = null;
                      
                      if(event.equals(UPDATE)) {
                          existingComponent = fields[ii].get(existingObject);
                          if(existingComponent == null && newComponent != null){
                              try {
                                  existingComponent = newComponent.getClass().newInstance();
                              } catch (InstantiationException e) {
                                // TODO Auto-generated catch block
                                e.printStackTrace();
                              } catch (IllegalAccessException e) {
                                // TODO Auto-generated catch block
                                e.printStackTrace();
                              }
                         } else {
                             //if neither objects contain the component then don't log anything
                              continue fieldIteration;
                         }
                      }
                      
                      if(newComponent == null) {
                          continue fieldIteration;
                      }
                      
                      logChanges(newComponent, existingComponent, newObject, persistedObjectId, event, className);
                      continue fieldIteration;
                     
                  }
                  i++;
              }

              String propertyNewState;
              String propertyPreUpdateState;

              //get new field values
              try {
                  Object objPropNewState = fields[ii].get(newObject);
                  if (objPropNewState != null) {
                      propertyNewState = objPropNewState.toString();
                  } else {
                      propertyNewState = "";
                  }

              } catch (Exception e) {
                  propertyNewState = "";
              }
              
              if(event.equals(UPDATE)) {
              
                  try {
                      Object objPreUpdateState = fields[ii].get(existingObject);
                      if (objPreUpdateState != null) {
                          propertyPreUpdateState = objPreUpdateState.toString();
                      } else {
                          propertyPreUpdateState = "";
                      }
                  } catch (Exception e) {
                      propertyPreUpdateState = "";
                  }
                  
                  // Now we have the two property values - compare them
                  if (propertyNewState.equals(propertyPreUpdateState)) {
                      continue; // Values haven't changed so loop to next property
                  } else  {
                      AuditLogRecord logRecord = new AuditLogRecord();
                      logRecord.setEntityName(className);
                      logRecord.setEntityAttribute(fieldName);
                      logRecord.setMessage(event);
                      logRecord.setUpdatedBy(this.getUserName());
                      logRecord.setUpdatedDate(new Date());
                      logRecord.setNewValue(propertyNewState);
                      logRecord.setOldValue(propertyPreUpdateState);
                      logRecord.setEntityId(persistedObjectId);
                      if(parentObject == null) {
                          logRecord.setEntity((Auditable) newObject);
                      } else {
                          logRecord.setEntity((Auditable) parentObject);
                      }

                      updates.add(logRecord);

                  }
                  
                  
              } else if(event.equals(DELETE)) {
                      Object returnValue = fields[ii].get(newObject);
                  
                      AuditLogRecord logRecord = new AuditLogRecord();
                      logRecord.setEntityName(className);
                      logRecord.setEntityAttribute(fieldName); 
                      logRecord.setMessage(event);
                      logRecord.setUpdatedBy(this.getUserName());
                      logRecord.setUpdatedDate(new Date());
                      logRecord.setNewValue("");
                      if (returnValue != null)
                          logRecord.setOldValue(returnValue.toString());
                      if (persistedObjectId != null)
                          logRecord.setEntityId(persistedObjectId);

                      if(parentObject == null) {
                          logRecord.setEntity((Auditable) newObject);
                      } else {
                          logRecord.setEntity((Auditable) parentObject);
                      }

                      deletes.add(logRecord);
                      
                  } else if(event.equals(INSERT)) {
                      
                      Object returnValue = fields[ii].get(newObject);

                      AuditLogRecord logRecord = new AuditLogRecord();
                      logRecord.setEntityName(className);
                      logRecord.setEntityAttribute(fieldName); 
                      logRecord.setMessage(event);
                      logRecord.setUpdatedBy(this.getUserName());
                      logRecord.setUpdatedDate(new Date());
                      logRecord.setOldValue("");

                      if (returnValue != null) {
                          logRecord.setNewValue(returnValue.toString());
                      } else
                          logRecord.setNewValue("");
 

                      if(parentObject == null) {
                          logRecord.setEntity((Auditable) newObject);
                      } else {
                          logRecord.setEntity((Auditable) parentObject);
                      }

                      inserts.add(logRecord);

              }

              
          }
      }
}

  

    /**
     * Returns an array of all fields used by this object from it's 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 = 0;
        int newFieldsSize = 0;
        
        if(fields != null) {
            fieldsSize = fields.length;
        }
        
        if(newFields != null) {
            newFieldsSize = 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();
       
       Field[] finalFieldsArray;
       
       if (superClass != null && ! superClass.getName().equals("java.lang.Object")) {
           finalFieldsArray = getAllFields(superClass, totalFields);
       } else {
           finalFieldsArray = totalFields;
       }
       
       return finalFieldsArray;

    }

    /**
     * Gets the id of the persisted object
     * @param obj the object to get the id from
     * @return object Id
     */
    private Serializable getObjectId(Object obj) {
        
        Class objectClass = obj.getClass();
        Method[] methods = objectClass.getMethods();

        Serializable persistedObjectId = null;
        for (int ii = 0; ii < methods.length; ii++) {
            // If the method name equals 'getId' then invoke it to get the id of the object.
            if (methods[ii].getName().equals("getId")) {
                try {
                    persistedObjectId = (Serializable)methods[ii].invoke(obj, null);
                    break;      
                } catch (Exception e) {
                    log.warn("Audit Log Failed - Could not get persisted object id: " + e.getMessage());
                }
            }
        }
        return persistedObjectId;
    }
    
    /**
     * Gets the current user's id from the Acegi secureContext
     * 
     * @return current user's userId
     */
    private String getUserName() {
        SecureContext secureContext = (SecureContext) ContextHolder.getContext();

        // secure context will be null when running unit tests so leave userId
        // as null
        if (secureContext != null) {
            Authentication auth = (Authentication) ((SecureContext) ContextHolder.getContext()).getAuthentication();

            String userName = null;
            if (auth.getPrincipal() instanceof UserDetails) {
                UserDetails userDetails = (UserDetails) auth.getPrincipal();
                userName = userDetails.getUsername();
            } else {
                userName = auth.getPrincipal().toString();
            }
            
            if(userName == null || userName.equals("")) {
                return "anonymousUser";
            } else {
                return userName;
            }
            
        } else {
            return "anonymousUser";
        }
    }

}


Pre[X]EventListener Approach

A simple alternative to the interceptor approach (add capture to the actor/user as needed) is to use the hibernate event model.

We first need an AuditTrail model that we will use to log CRUD operations:

CREATE TABLE AUDIT_TRAIL
(
    ID                          NUMBER NOT NULL,
    ENTITY_ID                   VARCHAR2(50) NOT NULL,
    ENTITY_NAME                 VARCHAR2(50) NOT NULL,
    ENTITY_PROPERTY             VARCHAR2(50) NOT NULL,
    ENTITY_PROPERTY_OLD_VALUE   VARCHAR2(4000),
    ENTITY_PROPERTY_NEW_VALUE   VARCHAR2(4000),
    OPERATION_TYPE              VARCHAR2(50),
    ACTOR_ID                    NUMBER NOT NULL,
    RELEVANCE_TIME              TIMESTAMP(6) NOT NULL,
    CONSTRAINT PK_AUDIT_TRAIL PRIMARY KEY (ID)
)

A possible enhancement would be to add a configuration setting to determine what entities to log (currently logs every entity). Creating an obtrusive IAuditable interface that has a getter/setter for the actor/user can be used to check an instanceof before an insert is made to AuditTrail, but a configuration that holds all of the mappings/entities that need audit trails seem like a better solution? You decide!


/*
 | File:    HibernateAuditLogListener.java
 | Created: Mar 3, 2008
 | Author:  Will Hoover
 */
import java.io.Serializable;
import java.util.Collection;
import java.util.Date;

import org.hibernate.EntityMode;
import org.hibernate.HibernateException;
import org.hibernate.StatelessSession;
import org.hibernate.cfg.Configuration;
import org.hibernate.event.Initializable;
import org.hibernate.event.PreDeleteEvent;
import org.hibernate.event.PreDeleteEventListener;
import org.hibernate.event.PreInsertEvent;
import org.hibernate.event.PreInsertEventListener;
import org.hibernate.event.PreLoadEvent;
import org.hibernate.event.PreLoadEventListener;
import org.hibernate.event.PreUpdateEvent;
import org.hibernate.event.PreUpdateEventListener;
import org.mycompany.model.impl.AuditTrail;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Audit Log Listener is used to log insert, update, delete, and load operations. Complete list of listeners/events can be obtained at <tt>org.hibernate.event.EventListeners</tt>.
 * 
 * @see org.hibernate.event.EventListeners
 * @author whoover
 */
public final class HibernateAuditLogListener implements
PreDeleteEventListener, PreInsertEventListener, PreUpdateEventListener,
PreLoadEventListener, Initializable {

    private static final long serialVersionUID = 1L;
    private static final Logger LOG = LoggerFactory.getLogger(HibernateAuditLogListener.class);
    public static final String OPERATION_TYPE_INSERT = "INSERT";
    public static final String OPERATION_TYPE_UPDATE = "UPDATE";
    public static final String OPERATION_TYPE_DELETE = "DELETE";


    /**
     * {@inheritDoc}
     */
    @Override
    public final void initialize(final Configuration cfg) {
        //
    }

    /**
     * Log deletions made to the current model in the the Audit Trail.
     * 
     * @param event
     *            the pre-deletion event
     */
    @Override
    public final boolean onPreDelete(final PreDeleteEvent event) {
        try {
            final Long actorId = 0L;
            final Serializable entityId = event.getPersister().hasIdentifierProperty() ? event.getPersister().getIdentifier(event.getEntity(), event.getPersister().guessEntityMode(event.getEntity())) : null;
            final String entityName = event.getEntity().getClass().toString();
            final Date transTime = new Date(); // new Date(event.getSource().getTimestamp());

            // need to have a separate session for audit save
            StatelessSession session = event.getPersister().getFactory().openStatelessSession();
            session.beginTransaction();

            if (LOG.isDebugEnabled()) {
                LOG.debug("{} for: {}, ID: {}, actor: {}, date: {}", new Object[] { entityName, entityId, actorId, transTime });
            }
            session.insert(new AuditTrail(entityId.toString(), entityName, OPERATION_TYPE_DELETE, null, null, OPERATION_TYPE_DELETE, actorId, transTime));

            session.getTransaction().commit();
            session.close();
        } catch (HibernateException e) {
            LOG.error("Unable to process audit log for DELETE operation", e);
        }
        return false;
    }
    /**
     * Log insertions made to the current model in the the Audit Trail.
     * 
     * @param event
     *            the pre-insertion event
     */
    @Override
    public final boolean onPreInsert(final PreInsertEvent event) {
        try {
            // TODO : need to get the actor ID somehow
            final Long actorId = 0L;
            final String entityId = event.getPersister().hasIdentifierProperty() ? event.getPersister().getIdentifier(event.getEntity(), event.getPersister().guessEntityMode(event.getEntity()))
                    .toString() : "";
            final String entityName = event.getEntity().getClass().toString();
            final Date transTime = new Date(); // new Date(event.getSource().getTimestamp());
            final EntityMode entityMode = event.getPersister().guessEntityMode(event.getEntity());
            Object newPropValue = null;

            // need to have a separate session for audit save
            StatelessSession session = event.getPersister().getFactory().openStatelessSession();
            session.beginTransaction();

            for (String propertyName : event.getPersister().getPropertyNames()) {
                newPropValue = event.getPersister().getPropertyValue(event.getEntity(), propertyName, entityMode);
                // because we are performing an insert we only need to be concerned will non-null values
                if (newPropValue != null) {
                    // collections will fire their own events
                    if (!(newPropValue instanceof Collection)) {
                        if (LOG.isDebugEnabled()) {
                            LOG.debug("{} for: {}, ID: {}, property: {}, value: {}, actor: {}, date: {}", new Object[] { OPERATION_TYPE_INSERT, entityName, entityId, propertyName, newPropValue, actorId, transTime });
                        }
                        session.insert(new AuditTrail(entityId, entityName, propertyName, null, newPropValue != null ? newPropValue.toString() : null, OPERATION_TYPE_INSERT, actorId, transTime));
                    }
                }
            }

            session.getTransaction().commit();
            session.close();
        } catch (HibernateException e) {
            LOG.error("Unable to process audit log for INSERT operation", e);
        }
        return false;
    }

    /**
     * Log updates made to the current model in the the Audit Trail.
     * 
     * @param event
     *            the pre-update event
     */
    @Override
    public final boolean onPreUpdate(PreUpdateEvent event) {
        try {
            // TODO : need to get the actor ID somehow
            final Long actorId = 0L;
            final Serializable entityId = event.getPersister().hasIdentifierProperty() ? event.getPersister().getIdentifier(event.getEntity(), event.getPersister().guessEntityMode(event.getEntity()))
                    : null;
            final String entityName = event.getEntity().getClass().toString();
            final Date transTime = new Date(); // new Date(event.getSource().getTimestamp());
            final EntityMode entityMode = event.getPersister().guessEntityMode(event.getEntity());
            Object oldPropValue = null;
            Object newPropValue = null;

            // need to have a separate session for audit save
            StatelessSession session = event.getPersister().getFactory().openStatelessSession();
            session.beginTransaction();

            // get the existing entity from session so that we can extract existing property values
            Object existingEntity = session.get(event.getEntity().getClass(), entityId);

            // cycle through property names, extract corresponding property values and insert new entry in audit trail
            for (String propertyName : event.getPersister().getPropertyNames()) {
                newPropValue = event.getPersister().getPropertyValue(event.getEntity(), propertyName, entityMode);
                // because we are performing an insert we only need to be concerned will non-null values
                if (newPropValue != null) {
                    // collections will fire their own events
                    if (!(newPropValue instanceof Collection)) {
                        oldPropValue = event.getPersister().getPropertyValue(existingEntity, propertyName, entityMode);
                        if (LOG.isDebugEnabled()) {
                            LOG.debug("{} for: {}, ID: {}, property: {}, old value: {}, new value: {}, actor: {}, date: {}", new Object[] { OPERATION_TYPE_UPDATE, entityName, entityId, propertyName, oldPropValue, newPropValue, actorId, transTime });
                        }
                        session.insert(new AuditTrail(entityId.toString(), entityName, propertyName, oldPropValue != null ? oldPropValue.toString() : null, newPropValue != null ? newPropValue
                                .toString() : null, OPERATION_TYPE_UPDATE, actorId, transTime));
                    }
                }
            }

            session.getTransaction().commit();
            session.close();
        } catch (HibernateException e) {
            LOG.error("Unable to process audit log for UPDATE operation", e);
        }
        return false;
    }

    /**
     * Log the loading of the current model in the the Audit Trail.
     * 
     * @param event
     *            the pre-load event
     */
    @Override
    public final void onPreLoad(final PreLoadEvent event) {
        // TODO : complete load audit log if desired
    }
}

We can add the following listener definitions in our hibernate.cfg.xml to tell Hiberante that we are listening:

<listener type="pre-delete" class="org.mycompany.dao.listener.impl.HibernateAuditLogListener"/>
<listener type="pre-update" class="org.mycompany.dao.listener.impl.HibernateAuditLogListener"/>
<listener type="pre-insert" class="org.mycompany.dao.listener.impl.HibernateAuditLogListener"/>
<listener type="pre-load" class="org.mycompany.dao.listener.impl.HibernateAuditLogListener"/>

Addendum : we can change above to a Post[X]Event, this enables us to get Id and other meta data in a more reliable way than pre[x] events. -- Vyas.


  NEW COMMENT

Audit logging Wiki impl does not support arrays(int[],String[],. 10 Oct 2005, 06:37 jc7442
Interceptor.onFlushDirty is not invoked when only an arrays of 
primitive type is updated. Consequently some AuditLogRecord are 
missing.
 
Passing the name of the interceptor class to opensession() 01 Dec 2005, 09:58 manhar_puri
We need to make some more changes to the code. In the call to the 
opensession() method of the SessionFactory class we have to pass the 
name of the interceptor class.

If there is another way of doing this please let me know.
 
Audit Log: Question on Rollback in JTA... 05 Jan 2006, 04:50 joshi
Since we are opening a new session instead of 
sessionFactory.getCurrentSession(), will it work with JTA?

I understand that we are not supposed to use same session as the one 
that triggers the event/interceptor method.

In a different context, I saw that the following provides a new 
session with the same connection.

Session newSession = sessionFactory.openSession(oldSession.connection
()); 

If we use the above, will it work with JTA?
 
small code comment 22 Mar 2006, 07:35 stewart.cambridge
Where you do this:

            Class objectClass = obj.getClass();
            String className = objectClass.getName();

            // Just get the class name without the package structure 
            String[] tokens = className.split("\\.");
            int lastToken = tokens.length - 1;
            className = tokens[lastToken];

You just do this:

            Class objectClass = obj.getClass();
            String className = objectClass.getSimpleName();
 
NHibernate version 12 Apr 2006, 02:28 shahzad_syed
Hi,

Do you have an NHibernate version of the implementation??
 
Re: small code comment 29 May 2006, 07:05 raghavender
POST QUESTIONS ON THE FORUM! COMMENTS HERE SHOULD ADD VALUE TO THE PAGE!
On 22 Mar 2006 07:35, stewart.cambridge wrote:

>Where you do this:

>            Class objectClass = obj.getClass();
>            String className = objectClass.getName();

>            // Just get the class name without the package structure
>            String[] tokens = className.split("\\.");
>            int lastToken = tokens.length - 1;
>            className = tokens[lastToken];

>You just do this:

>            Class objectClass = obj.getClass();
>            String className = objectClass.getSimpleName();

Hi Can you please tell me how can we create our own components and set 
them to the properties and check them while auditing 

just like in the above example code 

if(interfaces[i].getName().equals
("com.mycompany.model.audit.Component"))

How can we write "com.mycompany.model.audit.Component" class and set it 
to the property.

My requirement is to audit  to audit only the selective fields.

Thanks in advance
Raghavender
 
Annotated extension (JDK5) 17 Oct 2006, 04:13 RBramley
The audit interceptor was a great start (thanks Rob) - but it didn't
quite meet all of my needs. Some existing entities have an updated_date
- I don't want an audit record for those!

I defined an annotation DoNotAudit:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface DoNotAudit { }

and then modified the logChanges method to add (after
transient/static/final Field check): 

	          // ignore annotated fields
	          if(fields[ii].isAnnotationPresent(DoNotAudit.class)) 
	        	  continue fieldIteration;


Then classes that implement Auditable can use the annotation to indicate
fields that should not be audited, e.g.
	@DoNotAudit private Date updatedDate;


Hope this helps!

Robin
 
Thread safety of the audit interceptor 03 Nov 2006, 12:17 RBramley
I was using the sample code (plus some annotation extensions - see
above) and started getting java.util.ConcurrentModificationException 
thrown by the postFlush method when it's iterating the set of updates.

So I've added synchonisation around the Set usage - e.g.

Set<AuditLogRecord> sUpdates = Collections.synchronizedSet(updates);
synchronized(sUpdates) {
	sUpdates.add(logRecord);
}


Hopefully this will help someone else!

Robin
 
Re: NHibernate version 08 Nov 2006, 11:04 antonyclark99
POST QUESTIONS ON THE FORUM! COMMENTS HERE SHOULD ADD VALUE TO THE 
PAGE!On 12 Apr 2006 02:28, shahzad_syed wrote:

>Hi,

>Do you have an NHibernate version of the implementation??

Did you ever get an NHibernate version of this implementation?
If you did, could you email it to me at antonyclark99 (at) hotmail (dot) com

Thanks

Antony
 
Re: Thread safety of the audit interceptor 17 May 2007, 09:22 s.rakesh
POST QUESTIONS ON THE FORUM! COMMENTS HERE SHOULD ADD VALUE TO THE PAGE!
On 03 Nov 2006 12:17, RBramley wrote:

>I was using the sample code (plus some annotation extensions - see
>above) and started getting java.util.ConcurrentModificationException
>thrown by the postFlush method when it's iterating the set of updates.

>So I've added synchonisation around the Set usage - e.g.

>Set<AuditLogRecord> sUpdates = Collections.synchronizedSet(updates);
>synchronized(sUpdates) {
>	sUpdates.add(logRecord);
>}


>Hopefully this will help someone else!

>Robin

I'm facing same problem.I made what you suggested but it is not working.
As AuditLogInterceptor bean is not singleton,it is instantiating per 
request(means it is threadsafe).In such conditions 
ConcurrentModificationException is very odd.

If you have any idea please reply.

Thanks,
s.rakesh
 
Logger using SessinFactory and DB 28 Mar 2008, 11:10 golly97
Hey guys,
I used the following article as a base line to create my own logger.
Have a look and let me know what you think

http://javarobski.blogspot.com/2008/03/hibernate-audit-logging-right-way.html

I hack into the session factory to get the DB table and column names.
This works better for me as i don't depend on the names of my java objects

cheers.


Robert
 
Another way 17 Jul 2008, 07:31 adamw
A project which also enables audit logging is Envers:

http://www.jboss.org/envers/

The technique used there is a bit similar, but much more expanded.

-- 
Adam
 
Re: Thread safety of the audit interceptor 28 Sep 2008, 11:02 singhspk
POST QUESTIONS ON THE FORUM! COMMENTS HERE SHOULD ADD VALUE TO THE 
PAGE!On 17 May 2007 09:22, s.rakesh wrote:

>POST QUESTIONS ON THE FORUM! COMMENTS HERE SHOULD ADD VALUE TO THE
PAGE!
>On 03 Nov 2006 12:17, RBramley wrote:

>>I was using the sample code (plus some annotation extensions - see
>>above) and started getting java.util.ConcurrentModificationException
>>thrown by the postFlush method when it's iterating the set of
updates.

>>So I've added synchonisation around the Set usage - e.g.

>>Set<AuditLogRecord> sUpdates = Collections.synchronizedSet(updates);
>>synchronized(sUpdates) {
>>	sUpdates.add(logRecord);
>>}


>>Hopefully this will help someone else!

>>Robin

>I'm facing same problem.I made what you suggested but it is not
working.
>As AuditLogInterceptor bean is not singleton,it is instantiating per
>request(means it is threadsafe).In such conditions
>ConcurrentModificationException is very odd.

>If you have any idea please reply.

>Thanks,
>s.rakesh

s.rakesh,

did u find a solution the problem? I face the same issue. Wanted to 
check with you before I delve any further.
 
© Copyright 2006, Red Hat Middleware, LLC. All rights reserved. JBoss and Hibernate are registered trademarks and servicemarks of Red Hat, Inc. [Privacy Policy]