audit security A common requirement in developing enterprise applications is to ensure audit logs are available for data security and traceability–who made the changes, when they were made, and what files or sections were changed. This requirement is not only dictated by corporate IT policies, but also required by government laws. Considering that most enterprise applications have at least 50 domain objects, implementing audit logs on each of them can be time-consuming. So, a generic solution must be established to minimize coding when creating audit logs.

The Solution

  • Use Hibernate interceptors to trigger change events.
  • Use Java reflections to retrieve old and new data.
  • Create an interface to switch logging on or off for every data object.
In summary, here are the steps needed to be performed to accomplish this.
  1. Create an Auditable interface as a marker on the data that needs to be audited. Any data object that needs to be audited must implement this Auditable interface.
  2. Create an AuditLog to store log information. This contains the date (when), user (who), class name, object id and the audit message (what).
  3. Create an AuditLogInterceptor that will keep track of all data change events and store all theĀ  changes performed.
  4. Create a utility that can retrieve object values using Java reflections to process generic data retrieval.
  5. Create sample usage of auditing.
The Implementation
The Auditable interface has 4 method declarations.
public interface Auditable {
	/**
	 * Retrieve the primary key
	 * @return
	 */
	public Long getId();

	/**
	 * Returns the list of fields that should be audited.
	 * @return
	 */
	public List getAuditableFields();

	/**
	 * Returns the field of the primary identifier (e.g. title).
	 * This field is used to uniquely identify the record.
	 * @return
	 */
	public String getPrimaryField();

	/**
	 * Returns customized audit log message. When empty, audit logging
	 * uses standard audit message.
	 * @return
	 */
	public String getAuditMessage();
}

The AuditLog entity must be able to store who, when, what:

package com.ideyatech.core.bean;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EntityListeners;
import javax.persistence.Lob;
import javax.persistence.Table;
import javax.persistence.Transient;

import com.ideyatech.core.bean.user.BaseUser;
import com.ideyatech.core.persistence.listener.AuditLogListener;
import com.ideyatech.core.util.CrudUtil;

/**
 * This class is responsible for handling all audit functions
 * needed to be attached to the classes.
 *
 * @author allantan
 */
@Entity
@Table(name=”HISTORY_LOG”)
public class AuditLog implements Serializable {
	private static final long serialVersionUID = 269168041517643087L;
	@Column(name = “ENTITY_ID”,nullable=false,updatable=false)
	private Long entityId;
	@SuppressWarnings(”unchecked”)
	@Column(name = “ENTITY_CLASS”,nullable=false,updatable=false)
	private Class entityClass;
	@Column(name = “REFERENCE”)
	private String reference;
        @Column(name = “CREATE_DATE”)
        @Temporal(TemporalType.TIMESTAMP)
        private Date createDate;
	@Lob
	@Column(name = “MESSAGE”,nullable=false,updatable=false)
	private String message;
	@Column(name = “USER_ID”,nullable=false,updatable=false)
	private Long userId;
	@Transient
	private transient Object object;
	@Transient
	private transient BaseUser user;

	public AuditLog() {
	};
	@SuppressWarnings(”unchecked”)
	public AuditLog(String message, Long entityId, Class entityClass, Long userId) {
		this.message = message;
		this.entityId = entityId;
		this.entityClass = entityClass;
		this.userId = userId;
		this.setCreateDate(new Date());
	}

	/**
	 * @return the entityId
	 */
	public Long getEntityId() {
		return entityId;
	}

	/**
	 * @param entityId the entityId to set
	 */
	public final void setEntityId(Long entityId) {
		this.entityId = entityId;
	}

	/**
	 * @return the entityClass
	 */
	@SuppressWarnings(”unchecked”)
	public Class getEntityClass() {
		return entityClass;
	}

	/**
	 * @param entityClass the entityClass to set
	 */
	public final void setEntityClass(Class entityClass) {
		this.entityClass = entityClass;
	}

	/**
	 * @return the message
	 */
	public String getMessage() {
		return message;
	}

	/**
	 * @return the userId
	 */
	public Long getUserId() {
		return userId;
	}

	/**
	 * @param userId the userId to set
	 */
	public void setUserId(Long userId) {
		this.userId = userId;
	}

	/**
	 * @return the object
	 */
	public Object getObject() {
		return object;
	}

	/**
	 * @param object the object to set
	 */
	public void setObject(Object object) {
		this.object = object;
	}

	/**
	 * @return the user
	 */
	public BaseUser getUser() {
		return user;
	}

	/**
	 * @param user the user to set
	 */
	public void setUser(BaseUser user) {
		this.user = user;
	}
}

The AuditLogInterceptor is responsible in tracking the changes and creating necessary Audit logs. Notice that session is created from a separate HibernateUtils session so that the original(unchanged) data can be retrieved for comparison.

package com.ideyatech.core.persistence.interceptor;

import java.io.Serializable;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import org.apache.log4j.Logger;
import org.hibernate.CallbackException;
import org.hibernate.EmptyInterceptor;
import org.hibernate.Session;
import org.hibernate.type.Type;

import com.ideyatech.core.bean.Auditable;
import com.ideyatech.core.persistence.impl.AuditLogDAOImpl;
import com.ideyatech.core.util.CrudUtil;
import com.ideyatech.core.util.HibernateUtil;
import com.ideyatech.core.util.StringUtil;

/**
 * This is the interceptor responsible in tracking audit trails.
 * Source is patterned after the book “Java Persistence with Hibernate” - page 546 onwards
 * and merged with http://www.hibernate.org/318.html
 *
 * @author allantan
 */
public class AuditLogInterceptor extends EmptyInterceptor {
	private static final long serialVersionUID = 582549003254963262L;

	private static Logger _log = Logger.getLogger(AuditLogInterceptor.class);

    private Set inserts    = new HashSet();
    private Set updates    = new HashSet();
    private Set deletes    = new HashSet();
    private Map oldies     = new HashMap(); 

    @Override
    public boolean onSave(Object entity,
                          Serializable id,
                          Object[] state,
                          String[] propertyNames,
                          Type[] types)
            throws CallbackException {
        if (entity instanceof Auditable)
            inserts.add((Auditable)entity);
        return false;
    } 

	/* (non-Javadoc)
	 * @see org.hibernate.EmptyInterceptor#onDelete(java.lang.Object, java.io.Serializable, java.lang.Object[], java.lang.String[], org.hibernate.type.Type[])
	 */
	@Override
	public void onDelete(Object entity, Serializable id, Object[] state,
			String[] propertyNames, Type[] types) {
        if (entity instanceof Auditable)
            deletes.add((Auditable)entity);
	}

    @Override
    public boolean onFlushDirty(Object entity,
                                Serializable id,
                                Object[] currentState,
                                Object[] previousState,
                                String[] propertyNames,
                                Type[] types)
            throws CallbackException {
        if (entity instanceof Auditable) {
        	try {
	        	// Use the id and class to get the pre-update state from the database
	        	Session tempSession =
	                HibernateUtil.getSessionFactory().openSession();
	        	Auditable old = (Auditable) tempSession.get(entity.getClass(), ((Auditable) entity).getId());
	            oldies.put(old.getId(), old);
	        	updates.add((Auditable)entity);
        	} catch (Throwable e) {
        		_log.error(e,e);
        	}
        }
        return false;
    } 

    @Override
    public void postFlush(Iterator iterator)
                    throws CallbackException {
        try {
        	for (Auditable entity:inserts) {
        		if (!entity.skipAudit()) {
        			if (StringUtil.isEmpty(entity.getAuditMessage()))
        				AuditLogDAOImpl.logEvent(
        						CrudUtil.buildCreateMessage(entity), entity);
        			else
        				AuditLogDAOImpl.logEvent(
        						entity.getAuditMessage(), entity);
        		}
        	}
        	for (Auditable entity:deletes) {
        		if (!entity.skipAudit()) {
        			if (StringUtil.isEmpty(entity.getAuditMessage()))
        				AuditLogDAOImpl.logEvent(
        						CrudUtil.buildDeleteMessage(entity), entity);
        			else
        				AuditLogDAOImpl.logEvent(
        						entity.getAuditMessage(), entity);
        		}
        	}
        	for (Auditable entity:updates) {
        		if (!entity.skipAudit()) {
        			if (StringUtil.isEmpty(entity.getAuditMessage())) {
		        		Auditable old = oldies.get(entity.getId());
		        		AuditLogDAOImpl.logEvent(CrudUtil.buildUpdateMessage(old, entity), entity);
        			} else
        				AuditLogDAOImpl.logEvent(
        						entity.getAuditMessage(), entity);
        		}
        	}
        } catch (Throwable e) {
    		_log.error(e,e);
    	} finally {
            inserts.clear();
            updates.clear();
            deletes.clear();
            oldies.clear();
        }
    }
}



This is a continuation of the post “Audit Logging via Hibernate Interceptor“.

Other helper functions are shown below:

CrudUtil is the class responsible for retrieving object values using Java reflections:

import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.log4j.Logger;

import com.ideyatech.core.InvalidImplementationException;
import com.ideyatech.core.bean.Auditable;
import com.ideyatech.core.bean.BaseCriteria;
import com.ideyatech.core.bean.BaseEntity;

/**
 * @author allanctan
 *
 */
public class CrudUtil {

    private static Logger _log = Logger.getLogger(CrudUtil.class);

	private static final String SQL_PARAM = “:([^\\s]+)”;
	private static final Pattern SQL_PARAM_PATTERN = Pattern.compile(
			SQL_PARAM, Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);

    /**
     * Creates the logging message for new audit logs
     * @param obj
     * @return
     */
    public static String buildCreateMessage(Auditable obj) {
    	StringBuffer message = new StringBuffer(”Added “);
    	message.append(obj.getClass().getSimpleName())  // class name
    		.append(” “)
    		.append(obj.getPrimaryField())
    		.append(”:”);
    	Object value = retrieveObjectValue(obj, obj.getPrimaryField());
    	if (value!=null)
    		message.append(value.toString())
    		.append(” - “);
    	// loop through the fields list
		List auditFields = obj.getAuditableFields();
		int count = 0;
		for (String property:auditFields) {
			Object ret = retrieveObjectValue(obj, property);
			if (ret!=null && ret.toString().trim().length()>0) {
				if (count > 0)
					message.append(” and “);
				message.append(property)
					.append(”=”)
					.append(ret.toString());
				count++;
			}
		}

    	return message.toString();
    }

    /**
     * Creates the logging message for update audit logs
     * @param obj
     * @return
     */
    public static String buildUpdateMessage(Auditable oldObject, Auditable newObject) {

    	StringBuffer message = new StringBuffer(”Changed “);
    	message.append(oldObject.getClass().getSimpleName())  // class name
    		.append(” “)
    		.append(oldObject.getPrimaryField())
    		.append(”:”);
    	Object value = retrieveObjectValue(oldObject, oldObject.getPrimaryField());
    	if (value!=null)
    		message.append(value.toString())
    		.append(” - “);
    	// loop through the fields list
		List auditFields = oldObject.getAuditableFields();
		int count = 0;
		for (String property:auditFields) {
			Object oldValue = retrieveObjectValue(oldObject, property);
			Object newValue = retrieveObjectValue(newObject, property);
			if (oldValue == null) oldValue = new String(”");
			if (newValue == null) newValue = new String(”");
			if (!oldValue.equals(newValue)) {
				if (count > 0)
					message.append(” and “);
				message.append(property)
					.append(” from ‘”)
					.append(oldValue.toString())
					.append(”‘ to ‘”)
					.append(newValue.toString())
					.append(”‘”);
				count++;
			}
		}

    	return message.toString();
    }

    /**
     * Creates the logging message for new audit logs
     * @param obj
     * @return
     */
    public static String buildDeleteMessage(Auditable obj) {
    	StringBuffer message = new StringBuffer(”Deleted “);
    	message.append(obj.getClass().getSimpleName())  // class name
    		.append(” “)
    		.append(obj.getPrimaryField())
    		.append(”:”);
    	Object value = retrieveObjectValue(obj, obj.getPrimaryField());
    	if (value!=null)
    		message.append(value.toString());
    	return message.toString();
    }

    /**
     * Retrieves the property name for a method name.
     * (e.g. getName will return name)
     * @param methodName
     * @return
     */
    public static String getPropertyName(String methodName) {
    	if (StringUtil.isEmpty(methodName) || methodName.length()<=3)
    		return null;
    	if (methodName.startsWith(”get”) || methodName.startsWith(”set”)) {
    		String prop = methodName.substring(4);
    		char c = Character.toLowerCase(methodName.charAt(3));
    		return c+prop;
    	} else
    		return null;
    }
    /**
     * Retrieves the getter method name for a given property.
     * (e.g. name will return getName)
     * @param propertyName
     * @return
     */
    public static String getGetterMethodName(String propertyName) {
    	if (StringUtil.isEmpty(propertyName) || propertyName.length()<=0)
    		return null;
    	char c = Character.toUpperCase(propertyName.charAt(0));
    	return “get”+c+propertyName.substring(1);
    }

	/**
	 * This method retrieves the object value that corresponds to the property specified.
	 * This method can recurse inner classes until specified property is reached.
	 *
	 * For example:
	 * obj.firstName
	 * obj.address.Zipcode
	 *
	 * @param obj
	 * @param property
	 * @return
	 */
	public static Object retrieveObjectValue(Object obj, String property) {
		if (property.contains(”.”)) {
			// we need to recurse down to final object
			String props[] = property.split(”\\.”);
			try {
				Method method = obj.getClass().getMethod(getGetterMethodName(props[0]));
				Object ivalue = method.invoke(obj);
				if (ivalue==null)
					return null;
				return retrieveObjectValue(ivalue,property.substring(props[0].length()+1));
			} catch (Exception e) {
				_log.error(”Failed to retrieve value for “+property);
				throw new InvalidImplementationException(”CrudUtil”,”retrieveObjectValue”,null,”", e);
			}
		} else {
			// let’s get the object value directly
			try {
				Method method = obj.getClass().getMethod(getGetterMethodName(property));
				return method.invoke(obj);
			} catch (Exception e) {
				_log.error(”Failed to retrieve value for “+property);
				throw new InvalidImplementationException(”CrudUtil”,”retrieveObjectValue”,null,”", e);
			}
		}
	}

	/**
	 * This method retrieves the object type that corresponds to the property specified.
	 * This method can recurse inner classes until specified property is reached.
	 *
	 * For example:
	 * obj.firstName
	 * obj.address.Zipcode
	 *
	 * @param obj
	 * @param property
	 * @return
	 */
	public static Class retrieveObjectType(Object obj, String property) {
		if (property.contains(”.”)) {
			// we need to recurse down to final object
			String props[] = property.split(”\\.”);
			try {
				Method method = obj.getClass().getMethod(getGetterMethodName(props[0]));
				Object ivalue = method.invoke(obj);
				return retrieveObjectType(ivalue,property.substring(props[0].length()+1));
			} catch (Exception e) {
				_log.error(”Failed to retrieve value for “+property);
				throw new InvalidImplementationException(”CrudUtil”,”retrieveObjectValue”,null,”", e);
			}
		} else {
			// let’s get the object value directly
			try {
				Method method = obj.getClass().getMethod(getGetterMethodName(property));
				return method.getReturnType();
			} catch (Exception e) {
				_log.error(”Failed to retrieve value for “+property);
				throw new InvalidImplementationException(”CrudUtil”,”retrieveObjectValue”,null,”", e);
			}
		}
	}
}

HibernateUtil is a utility to retrieve hibernate session separate from the usual EntityManager.

import java.net.URL;
import java.util.Set;

import org.hibernate.SessionFactory;
import org.hibernate.cfg.AnnotationConfiguration;
import org.scannotation.AnnotationDB;
import org.scannotation.ClasspathUrlFinder;

/**
 * This utility allows creation of Hibernate session directly.
 * Used for logging purposes.
 *
 * @author allantan
 */
public class HibernateUtil {
    private static final SessionFactory sessionFactory;
    static {
        try {
        	URL[] urls = ClasspathUrlFinder.findResourceBases(”META-INF/persistence.xml”);
        	AnnotationDB db = new AnnotationDB();
        	db.scanArchives(urls);
        	Set entityClasses = db.getAnnotationIndex().get(javax.persistence.Entity.class.getName());
            // Create the SessionFactory
        	AnnotationConfiguration ac =  new AnnotationConfiguration();
            ac.setProperty(”hibernate.connection.datasource”, “java:comp/env/jdbc/ideyatech”);
            ac.setProperty(”hibernate.dialect”, “org.hibernate.dialect.MySQLDialect”);
            for (String clazz:entityClasses) {
            	ac.addAnnotatedClass(Class.forName(clazz));
            }
            sessionFactory = ac.buildSessionFactory();
        } catch (Throwable ex) {
            // Make sure you log the exception, as it might be swallowed
            System.err.println(”Initial SessionFactory creation failed.” + ex);
            throw new ExceptionInInitializerError(ex);
        }
    }

    public static SessionFactory getSessionFactory() {
        return sessionFactory;
    }
}

And finally, the sample implementation of an Auditable class:

public class InboundDocument implements Auditable {
	private static final long serialVersionUID = -7019861759834380358L;

	@Column(name = “DATE_RECEIVED”)
	@Field(index = Index.UN_TOKENIZED, store = Store.YES)
	@DateBridge(resolution = Resolution.DAY)
	private Date dateReceived;

	@Column(name = “NUMBER_OF_DAYS”)
	private Integer numberOfDays;

	@Column(name = “DATE_DUE”)
	@Field(index = Index.UN_TOKENIZED, store = Store.YES)
	@DateBridge(resolution = Resolution.DAY)
	private Date dateDue;

	@ManyToOne(fetch = FetchType.EAGER)
	@JoinColumn(name = “ACTION_NEEDED”)
	@Field(index = Index.TOKENIZED)
	@FieldBridge(impl = SystemCodesBridge.class)
	private SystemCodes actionNeeded;

        …
        …

	public List getAuditableFields() {
		List props = new ArrayList();
		props.add(”documentType”);
		props.add(”actionNeeded”);
		props.add(”dateReceived”);
		props.add(”dateDue”);
		props.add(”summary”);
		return props;
	}

	public String getPrimaryField() {
		return “barcodeNumber”;
	}
}

That’s it! Just implement all your auditable classes with Auditable interface. All the codes above are extracted from open-tides - an open-source web-foundation framework that can be used to quickly setup an enterprise project.