Audit Logging via Hibernate Interceptor (1/2)

Posted on Posted in Java, Technology Center

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 for 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();
        }
    }
}

 

Launching an enterprise application is so much more than just having an idea and a guy to implement it. You have ideas, we have solutions! Contact us today.

Send us a message

Continuation: Audit Logging via Hibernate Interceptor (2/2)

One thought on “Audit Logging via Hibernate Interceptor (1/2)

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.