Showing posts with label Audit Logging. Show all posts
Showing posts with label Audit Logging. Show all posts

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 auditTrail: auditTrailList) {
               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.