Sunday, 13 October 2013

Audit Logging using Hibernate Interceptor in JPA 2.0

If you are using JPA 2.0 with Hibernate and you want to do audit logging from middle-ware itself, I believe you landed up on the exact place where you should be. You can try audit logging in your local environment by following this post.

Required JPA/Hibernate Maven Dependencies

<dependency>
         <groupId>org.hibernate.java-persistence</groupId>
         <artifactId>jpa-api</artifactId>
         <version>2.0-cr-1</version>
</dependency>
<dependency>
         <groupId>org.hibernate</groupId>
         <artifactId>hibernate-entitymanager</artifactId>
         <version>3.6.4.Final</version>   
</dependency>
<dependency>
         <groupId>org.springframework</groupId>
         <artifactId>spring-orm</artifactId>
         <version>3.0.5.RELEASE</version>
</dependency>

JPA Configuration in Spring Application Context File

For JPA/Hibernate, you need to configure entity manager, transaction, data source and JPA vendor in your ‘applicationContext.xml’ file.

<bean id="dataSource"
         class="org.springframework.jdbc.datasource.DriverManagerDataSource"
         p:driverClassName="oracle.jdbc.driver.OracleDriver"  
        p:url="jdbc:oracle:thin:@127.0.0.1:1521/mydbservice "
         p:username="nvuser" p:password="nvpass" />

<bean id="entityManagerFactory"
         class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"
         p:persistenceUnitName="MyPersistentUnit"
         p:persistenceXmlLocation="classpath*:persistence.xml"
         p:dataSource-ref="dataSource" p:jpaVendorAdapter-ref="jpaAdapter">
         <property name="loadTimeWeaver">
            <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver"/>
         </property>              
</bean>

<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"
                 p:entityManagerFactory-ref="entityManagerFactory" />

<bean id="jpaAdapter"
                 class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"
                 p:database="ORACLE" p:showSql="true" />

Persistence.xml Configuration

To enable the hibernate audit logging you need to configure a property called ‘hibernate.ejb.interceptor’ in persistence.xml file and the value of this property should be the class that extends the Hibernate ‘EmptyInterceptor’.

<?xml version=”1.0” encoding=”UTF-8”?>
<persistence version="2.0"
         xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
         <persistence-unit name=" MyPersistentUnit ">
               
                 //JPA Entity classes are configured here
                 <class>com.mycom.entities.User </class>           

                 <shared-cache-mode>ENABLE_SELECTIVE</shared-cache-mode>

                 <properties>
                 <property name="hibernate.ejb.interceptor"
                          value=" com.xxx.entities.baseentity.AuditLogInterceptor" />
                 </properties>
         </persistence-unit>
</persistence>

Audit Logging Interceptor Implementation

Hibernate provides an interceptor called ‘EmptyInterceptor’ with following methods.
  • onSave() – This method is called when an entity is saved, but the entity is not saved into database yet.
  • onFlushDirty() – This method is called when an entity is updated, the entity is not update into database yet.
  • onDelete () – This method is called when an entity is deleted , the entity is not deleted into database yet.
  • preFlush() – This method is called before the saved, updated or deleted entities are not committed to the database.
  • postFlush() – This method is called after the saved, updated or deleted entities are committed to database. This is called after preFlush() method.

To perform audit logging follow below steps:
  • Create data base table to capture audit data:
          Audit [ Row_ID, Old_Value, New_Value, Column_Changed, Table_Changed,
                    Transaction_Id, User_ID]
  • Create JPA entity say ‘AuditTrail’ for above table
  • Create class ‘AuditLogInterceptor’ by extending Hibernate ‘EmptyInterceptor’.
  • Override interceptor methods in ‘AuditLogInterceptor’
  • Capture required information into AuditTrail entity and save entity

public class AuditLogInterceptor extends EmptyInterceptor {  
    
  private List<AuditTrail> auditTrailList = new ArrayList<AuditTrail>();    
   . . . . . .
 
   // First Parameter: The entity which is being updated     
   // Second Parameter: The primary key of updated row 
   // Third Parameter:  Current states of updated entity    
   // Fourth Parameter: Previous states of updated entity  
   // Fifth Parameter: The variable names of updated entity
   // Sixth Parameter: The type of variables of updated entity
   @Override
   public boolean onFlushDirty(Object entity, Serializable id,
            Object[] currentState, Object[] previousState,
            String[] propertyNames, Type[] types) {                     
           // Get the table name from entity
           Class<?> entityClass = entity.getClass();
            Table tableAnnotation = entityClass.getAnnotation(Table.class);            
          
           for (int i = 0; i < currentState.length; i++) {              
                // Track changes only for the states which are String or Number types           
                 if(!(previousState[i] instanceof String
                     || previousState[i] instanceof Long
                     || previousState[i] instanceof BigDecimal
                      || previousState[i] instanceof Integer
                     || previousState[i] instanceof Timestamp))
                 {
                          continue;
                 }                
                // Check whether column value is updated
                 if(previousState[i] != null && currentState[i] != null
                   &&  !(previousState[i].equals(currentState[i]))){
                         
                  AuditTrail auditTrail = new AuditTrail();               
                 // Set updated table name
                 auditTrail.setTableName(tableAnnotation.name());
                 // Set updated column name
                 Column col = null;
                 try {                                                  
                        String filedName = propertyNames[i];
                        if(filedName.contains("serialVersionUID")){
                                  continue;
                        }
                        Character firstChar =  filedName.charAt(0);
                        firstChar = Character.toUpperCase(firstChar);
                        String filedNameSubStr = filedName.substring(1);
                        String methodName = "get"+ firstChar+filedNameSubStr;                       
                        Class[] parameterTypes = new Class[]{};                           
                        Method ueMethod = entityClass.getDeclaredMethod(methodName, parameterTypes);
                        col = ueMethod.getAnnotation(Column.class);                         
                    } catch (Exception e) {
                                e.printStackTrace();
                    }
                   if(col != null){
                          auditTrail.setColumnIdentifier(col.name());       
                   }               
                // Set old value
                if(previousState[i] != null){
                          auditTrail.setOldValue(previousState[i] == null  ? null :
                         previousState[i].toString());    
                }               
                // Set new value
                if(currentState[i] != null){
                          auditTrail.setNewValue(currentState[i] == null ? null:
                         currentState[i].toString());
                }               
                // Set database operation
                auditTrail.setOperation("Update");               
                // Set row primary key value
                auditTrail.setRowIdentifier(id.toString());  
                auditTrailList.add(auditTrail);                         
              }           
            }                        
        return false;
    }        
    @Override  
    public void postFlush(Iterator iterator) throws CallbackException {            
           // Set unique transaction id in all AuditTrail Entities
           String transId = UUID.randomUUID().toString();     
           for (AuditTrail auditTrailauditTrailList) {
               auditTrail.setTransactionId(transId);
           }   
          // Save ‘AuditTrail’ entity list into database using AuditDAO 
    }  
 } 

Audit logging Sequence Flow




Issue Faced during Audit Logging Implementation

I faced an interesting issue during audit logging implementation in my application. Let me share that issue detail.

I had ‘AbstractDAO’ and 'AuditDAO' class extends this ‘AbstractDAO’. 'AuditDAO' class is configured as ‘@Repository’. AbstractDAO’ has entity manager property which is initialized by Spring Container. I wanted to auto wire ‘AuditDAO’ in ‘AuditLogInterceptor’ to save ‘AuditTrail’ entity. But, this couldn’t happen. The reason was ‘AuditLogInterceptor’ was not defined as spring resource (like @Component or @Resources or @Repository). Hence's its property can’t be auto wired. Since, this interceptor is initialized by hibernate, I couldn't configure this as spring resource. Finally, I had to take the help of StackOverflow and got the idea to solve this problem. You can refer this issue and solution from here.