Audit Framework with Spring AOP and JPA

Every now and then we come across a common requirement of building an Audit framework. In simple words it means whenever a record is added, edited or removed there should be an entry made in a separate table so that different actions of users can be monitored. Today in this tutorial we will build an audit framework in Java using Spring AOP and JPA. Records will be saved in xml form. We will reuse code from JPA OneToMany Mapping Using Hibernate and MySQL tutorial.

Our database schema consists of BookShelf, Book and Audit.

Database Schema

First we will setup our maven dependency (Need help creating Maven Java Project in Eclipse ?)

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>AuditFrameWork</groupId>
	<artifactId>AuditFrameWork</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<build>
		<plugins>
			<plugin>
				<artifactId>maven-compiler-plugin</artifactId>
				<configuration>
					<source>1.7</source>
					<target>1.7</target>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-clean-plugin</artifactId>
				<version>2.5</version>
			</plugin>
		</plugins>
	</build>
	<dependencies>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-core</artifactId>
			<version>${spring.version}</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-context</artifactId>
			<version>${spring.version}</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-aop</artifactId>
			<version>${spring.version}</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-tx</artifactId>
			<version>${spring.version}</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-orm</artifactId>
			<version>${spring.version}</version>
		</dependency>
		<dependency>
			<groupId>cglib</groupId>
			<artifactId>cglib</artifactId>
			<version>2.2.2</version>
		</dependency>
		<dependency>
			<groupId>org.aspectj</groupId>
			<artifactId>aspectjrt</artifactId>
			<version>1.6.11</version>
		</dependency>
		<dependency>
			<groupId>org.aspectj</groupId>
			<artifactId>aspectjweaver</artifactId>
			<version>1.6.11</version>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>5.1.21</version>
		</dependency>
		<dependency>
			<groupId>org.hibernate.javax.persistence</groupId>
			<artifactId>hibernate-jpa-2.0-api</artifactId>
			<version>1.0.1.Final</version>
		</dependency>
		<dependency>
			<groupId>com.thoughtworks.xstream</groupId>
			<artifactId>xstream</artifactId>
			<version>1.4.2</version>
		</dependency>
		<dependency>
			<groupId>commons-lang</groupId>
			<artifactId>commons-lang</artifactId>
			<version>2.6</version>
		</dependency>
		<dependency>
			<groupId>commons-collections</groupId>
			<artifactId>commons-collections</artifactId>
			<version>3.2</version>
		</dependency>
		<dependency>
			<groupId>commons-beanutils</groupId>
			<artifactId>commons-beanutils</artifactId>
			<version>1.8.3</version>
		</dependency>
		<dependency>
			<groupId>javax.enterprise</groupId>
			<artifactId>cdi-api</artifactId>
			<version>1.0</version>
		</dependency>
		<dependency>
			<groupId>org.hibernate</groupId>
			<artifactId>hibernate-core</artifactId>
			<version>${hibernate.version}</version>
		</dependency>
		<dependency>
			<groupId>org.hibernate</groupId>
			<artifactId>hibernate-entitymanager</artifactId>
			<version>${hibernate.version}</version>
			<exclusions>
				<exclusion>
					<groupId>cglib</groupId>
					<artifactId>cglib</artifactId>
				</exclusion>
				<exclusion>
					<groupId>dom4j</groupId>
					<artifactId>dom4j</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
	</dependencies>
	<properties>
		<hibernate.version>3.6.7.Final</hibernate.version>
		<spring.version>3.1.2.RELEASE</spring.version>
	</properties>
</project>

Second we will start with our JPA mapping code. There are few important classes and interfaces involved.

  1. IEntity – A marker interface to depict an database entity
  2. IXMLConvertable – As name suggests this interface let’s conversion from Java to XML generic
  3. IAuditable – A marker interface to depict an entity which can be auditable

Let’s start building our framework.

IAuditable


package com.mumz.test.audit.interfaces;

import java.io.Serializable;

/**
 * The Interface IAuditable.
 * 
 * @author prabhat.jha
 */
public interface IAuditable extends Serializable{

	/** The Constant OPERATION_INSERT. */
	public static final String OPERATION_INSERT = "Insert";
	
	/** The Constant OPERATION_UPDATE. */
	public static final String OPERATION_UPDATE = "Update";
	
	/** The Constant OPERATION_DELETE. */
	public static final String OPERATION_DELETE = "Delete";
}

IEntity


package com.mumz.test.audit.interfaces;

import java.io.Serializable;

/**
 * The Interface IEntity.
 * @author prabhat.jha
 */
public interface IEntity extends Serializable{
}

IXMLConvertable


package com.mumz.test.audit.interfaces;

import java.io.Serializable;
import java.util.List;
import java.util.Map;

/**
 * The Interface IXMLConvertable.
 * @author prabhat.jha
 */
public interface IXMLConvertable extends Serializable {
	
	/**
	 * Gets the class alias.
	 * 
	 * @return the class alias
	 */
	public String getClassAlias();

	/**
	 * Gets the field to be omitted.
	 * 
	 * @return the field to be omitted
	 */
	public List<String> getFieldsToBeOmitted();

	/**
	 * Gets the field aliases.
	 * 
	 * @return the field aliases
	 */
	public Map<String, String> getFieldAliases();
}

With the basic structure of our code base setup we will reuse the code from this post. Explanation on OneToMany can be obtained from the link.

Book.java

package com.mumz.test.audit.beans;

import java.util.List;
import java.util.Map;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.Table;
import javax.persistence.Transient;

import com.mumz.test.audit.interfaces.IAuditable;
import com.mumz.test.audit.interfaces.IEntity;
import com.mumz.test.audit.interfaces.IXMLConvertable;

/**
 * The Class Book.
 * 
 * @author prabhat.jha
 */
@Entity
@Table(name = "BOOK")
@NamedQueries({ @NamedQuery(name = "fetchAllBooks", query = "SELECT ALLBOOKS FROM Book ALLBOOKS") })
public class Book implements IXMLConvertable, IEntity, IAuditable {

	/** The Constant serialVersionUID. */
	private static final long serialVersionUID = -4788522141255171404L;

	/** The id. */
	private Long id = null;

	/** The name. */
	private String name = null;

	/** The author. */
	private String author = null;

	/** The book shelf. */
	private BookShelf bookShelf = null;

	/**
	 * Gets the id.
	 * 
	 * @return the id
	 */
	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	@Column(name = "BOOK_ID")
	public Long getId() {
		return id;
	}

	/**
	 * Sets the id.
	 * 
	 * @param id
	 *            the id to set
	 */
	public void setId(Long id) {
		this.id = id;
	}

	/**
	 * Gets the name.
	 * 
	 * @return the name
	 */
	@Column(name = "BOOK_NAME")
	public String getName() {
		return name;
	}

	/**
	 * Sets the name.
	 * 
	 * @param name
	 *            the name to set
	 */
	public void setName(String name) {
		this.name = name;
	}

	/**
	 * Gets the author.
	 * 
	 * @return the author
	 */
	@Column(name = "BOOK_AUTHOR")
	public String getAuthor() {
		return author;
	}

	/**
	 * Sets the author.
	 * 
	 * @param author
	 *            the author to set
	 */
	public void setAuthor(String author) {
		this.author = author;
	}

	/**
	 * Gets the book shelf.
	 * 
	 * @return the bookShelf
	 */
	@ManyToOne
	@JoinColumn(name = "BOOK_SHELF_ID")
	public BookShelf getBookShelf() {
		return bookShelf;
	}

	/**
	 * Sets the book shelf.
	 * 
	 * @param bookShelf
	 *            the bookShelf to set
	 */
	public void setBookShelf(BookShelf bookShelf) {
		this.bookShelf = bookShelf;
	}

	/** (non-Javadoc)
	 * @see com.mumz.test.audit.interfaces.IXMLConvertable#getClassAlias()
	 */
	@Override
	@Transient
	public String getClassAlias() {
		return "Book";
	}

	/** (non-Javadoc)
	 * @see com.mumz.test.audit.interfaces.IXMLConvertable#getFieldsToBeOmitted()
	 */
	@Override
	@Transient
	public List<String> getFieldsToBeOmitted() {
		return null;
	}

	/** (non-Javadoc)
	 * @see com.mumz.test.audit.interfaces.IXMLConvertable#getFieldAliases()
	 */
	@Override
	@Transient
	public Map<String, String> getFieldAliases() {
		return null;
	}

	/**
	 * (non-Javadoc).
	 * 
	 * @return the int
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((author == null) ? 0 : author.hashCode());
		result = prime * result + ((id == null) ? 0 : id.hashCode());
		result = prime * result + ((name == null) ? 0 : name.hashCode());
		return result;
	}

	/**
	 * (non-Javadoc).
	 * 
	 * @param obj
	 *            the obj
	 * @return true, if successful
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj == null) {
			return false;
		}
		if (!(obj instanceof Book)) {
			return false;
		}
		Book other = (Book) obj;
		if (author == null) {
			if (other.author != null) {
				return false;
			}
		} else if (!author.equals(other.author)) {
			return false;
		}
		if (id == null) {
			if (other.id != null) {
				return false;
			}
		} else if (!id.equals(other.id)) {
			return false;
		}
		if (name == null) {
			if (other.name != null) {
				return false;
			}
		} else if (!name.equals(other.name)) {
			return false;
		}
		return true;
	}

	/**
	 * (non-Javadoc).
	 * 
	 * @return the string
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		return String.format("Book [id=%s, name=%s, author=%s]", id, name,
				author);
	}
}

BookShelf.java


package com.mumz.test.audit.beans;

import java.util.List;
import java.util.Map;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.Transient;

import com.mumz.test.audit.interfaces.IEntity;
import com.mumz.test.audit.interfaces.IXMLConvertable;

/**
 * The Class BookShelf.
 * @author prabhat.jha
 */
@Entity
@Table(name = "BOOK_SHELF")
@NamedQueries({
	@NamedQuery(name="fetchAllBookShelves", query="SELECT ALLBOOKSHELVES FROM BookShelf ALLBOOKSHELVES")
})
public class BookShelf implements IEntity, IXMLConvertable{
	
	/** The Constant serialVersionUID. */
	private static final long serialVersionUID = -7867320637075813912L;

	/** The id. */
	private Long	id		= null;

	/** The name. */
	private String	name	= null;
	
	/** The books. */
	private List<Book> books = null;

	/**
	 * Gets the id.
	 * 
	 * @return the id
	 */
	@Id
	@GeneratedValue(strategy=GenerationType.AUTO)
	@Column(name="BOOK_SHELF_ID")
	public Long getId() {
		return id;
	}

	/**
	 * Sets the id.
	 * 
	 * @param id
	 *            the id to set
	 */
	public void setId(Long id) {
		this.id = id;
	}

	/**
	 * Gets the name.
	 * 
	 * @return the name
	 */
	@Column(name="BOOK_SHELF_NAME")
	public String getName() {
		return name;
	}

	/**
	 * Sets the name.
	 * 
	 * @param name
	 *            the name to set
	 */
	public void setName(String name) {
		this.name = name;
	}

	/**
	 * Gets the books.
	 * 
	 * @return the books
	 */
	@OneToMany(cascade=CascadeType.ALL, mappedBy="bookShelf")
	public List<Book> getBooks() {
		return books;
	}

	/**
	 * Sets the books.
	 * 
	 * @param books
	 *            the books to set
	 */
	public void setBooks(List<Book> books) {
		this.books = books;
	}

	/** (non-Javadoc)
	 * @see com.mumz.test.audit.interfaces.IXMLConvertable#getClassAlias()
	 */
	@Override
	@Transient
	public String getClassAlias() {
		return "BookShelf";
	}

	/** (non-Javadoc)
	 * @see com.mumz.test.audit.interfaces.IXMLConvertable#getFieldsToBeOmitted()
	 */
	@Override
	@Transient
	public List<String> getFieldsToBeOmitted() {
		return null;
	}

	/** (non-Javadoc)
	 * @see com.mumz.test.audit.interfaces.IXMLConvertable#getFieldAliases()
	 */
	@Override
	@Transient
	public Map<String, String> getFieldAliases() {
		return null;
	}
	/**
	 * (non-Javadoc).
	 * 
	 * @return the int
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((books == null) ? 0 : books.hashCode());
		result = prime * result + ((id == null) ? 0 : id.hashCode());
		result = prime * result + ((name == null) ? 0 : name.hashCode());
		return result;
	}

	/**
	 * (non-Javadoc).
	 * 
	 * @param obj
	 *            the obj
	 * @return true, if successful
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj == null) {
			return false;
		}
		if (!(obj instanceof BookShelf)) {
			return false;
		}
		BookShelf other = (BookShelf) obj;
		if (books == null) {
			if (other.books != null) {
				return false;
			}
		} else if (!books.equals(other.books)) {
			return false;
		}
		if (id == null) {
			if (other.id != null) {
				return false;
			}
		} else if (!id.equals(other.id)) {
			return false;
		}
		if (name == null) {
			if (other.name != null) {
				return false;
			}
		} else if (!name.equals(other.name)) {
			return false;
		}
		return true;
	}

	/**
	 * (non-Javadoc).
	 * 
	 * @return the string
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		return String.format("BookShelf [id=%s, name=%s, books=%s]", id, name, books);
	}
}

And our Audit bean which will hold pre and post image of record being inserted, updated.

Audit.java


package com.mumz.test.audit.beans;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

import com.mumz.test.audit.interfaces.IEntity;

/**
 * The Class Auditable.
 * 
 * @author prabhat.jha
 */
@Entity
@Table(name="AUDIT")
public class Audit implements IEntity {

	/** The Constant serialVersionUID. */
	private static final long serialVersionUID = 6161413362358931496L;

	/** The id. */
	private Long id = null;
	
	/** The pre image. */
	private String preImage = null;

	/** The post image. */
	private String postImage = null;
	
	/** The operation. */
	private String operation = null;

	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	@Column(name="AUDIT_ID")
	public Long getId() {
		return id;
	}

	/**
	 * Sets the id.
	 * 
	 * @param id
	 *            the new id
	 */
	public void setId(Long id) {
		this.id = id;
	}

	/**
	 * Sets the pre image.
	 * 
	 * @param preImage
	 *            the new pre image
	 */
	public void setPreImage(String preImage) {
		this.preImage = preImage;
	}
	
	/**
	 * Gets the pre image.
	 * 
	 * @return the pre image
	 */
	@Column(name="AUDIT_PRE_IMAGE")
	public String getPreImage() {
		return this.preImage;
	}

	/**
	 * Gets the post image.
	 * 
	 * @return the post image
	 */
	@Column(name="AUDIT_POST_IMAGE")
	public String getPostImage() {
		return this.postImage;
	}

	/**
	 * Sets the post image.
	 * 
	 * @param postImage
	 *            the new post image
	 */
	public void setPostImage(String postImage) {
		this.postImage = postImage;
	}
	
	/**
	 * Sets the operation.
	 * 
	 * @param operation
	 *            the new operation
	 */
	public void setOperation(String operation) {
		this.operation = operation;
	}

	/**
	 * Gets the operation.
	 * 
	 * @return the operation
	 */
	@Column(name="AUDIT_OPERATION")
	public String getOperation() {
		return this.operation;
	}

	/** (non-Javadoc)
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result
				+ ((operation == null) ? 0 : operation.hashCode());
		result = prime * result
				+ ((postImage == null) ? 0 : postImage.hashCode());
		result = prime * result
				+ ((preImage == null) ? 0 : preImage.hashCode());
		return result;
	}

	/** (non-Javadoc)
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj == null) {
			return false;
		}
		if (!(obj instanceof Audit)) {
			return false;
		}
		Audit other = (Audit) obj;
		if (operation == null) {
			if (other.operation != null) {
				return false;
			}
		} else if (!operation.equals(other.operation)) {
			return false;
		}
		if (postImage == null) {
			if (other.postImage != null) {
				return false;
			}
		} else if (!postImage.equals(other.postImage)) {
			return false;
		}
		if (preImage == null) {
			if (other.preImage != null) {
				return false;
			}
		} else if (!preImage.equals(other.preImage)) {
			return false;
		}
		return true;
	}

	/** (non-Javadoc)
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		return "Auditable [preImage=" + preImage + ", postImage=" + postImage
				+ ", operation=" + operation + "]";
	}
}

Since we are trying to build a framework, all actions should be routed through a controller, hence we will build an interface IAuditController and implementation class AuditControllerImpl.

IAuditController


package com.mumz.test.audit.controller;

import java.util.List;

import com.mumz.test.audit.beans.Book;
import com.mumz.test.audit.beans.BookShelf;

/**
 * The Interface IAuditController.
 * @author prabhat.jha
 */
public interface IAuditController {
	
	/**
	 * Adds the book.
	 * 
	 * @param book
	 *            the book
	 * @return the book
	 */
	public Book addBook(Book book);
	
	/**
	 * Update book.
	 * 
	 * @param book
	 *            the book
	 * @return the book
	 */
	public Book updateBook(Book book);
	
	/**
	 * Removes the book.
	 * 
	 * @param book
	 *            the book
	 * @return the book
	 */
	public Book removeBook(Book book);
	
	/**
	 * Fetch all books.
	 * 
	 * @return the list
	 */
	public List<Book> fetchAllBooks();
	
	/**
	 * Adds the book shelf.
	 * 
	 * @param bookShelf
	 *            the book shelf
	 * @return the book shelf
	 */
	public BookShelf addBookShelf(BookShelf bookShelf);
	
	/**
	 * Update book shelf.
	 * 
	 * @param bookShelf
	 *            the book shelf
	 * @return the book shelf
	 */
	public BookShelf updateBookShelf(BookShelf bookShelf);
	
	/**
	 * Removes the book shelf.
	 * 
	 * @param bookShelf
	 *            the book shelf
	 * @return the book shelf
	 */
	public BookShelf removeBookShelf(BookShelf bookShelf);
	
	/**
	 * Fetch all book shelves.
	 * 
	 * @return the list
	 */
	public List<BookShelf> fetchAllBookShelves();
}

AuditControllerImpl


package com.mumz.test.audit.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Controller;

import com.mumz.test.audit.beans.Book;
import com.mumz.test.audit.beans.BookShelf;
import com.mumz.test.audit.dao.IAuditPersistableService;
import com.mumz.test.audit.dao.IAuditQueryService;

/**
 * The Class AuditController.
 * 
 * @author prabhat.jha
 */
@Controller(value="auditControllerImpl")
public class AuditControllerImpl implements IAuditController {

	/** The book dao service. */
	@Autowired
	private IAuditPersistableService auditPersistableService = null;

	/** The audit query service. */
	@Autowired
	private IAuditQueryService auditQueryService = null;

	/**
	 * Gets the audit persistable service.
	 * 
	 * @return the audit persistable service
	 */
	public IAuditPersistableService getAuditPersistableService() {
		return auditPersistableService;
	}

	/**
	 * Sets the audit persistable service.
	 * 
	 * @param auditPersistableService
	 *            the new audit persistable service
	 */
	public void setAuditPersistableService(
			IAuditPersistableService auditPersistableService) {
		this.auditPersistableService = auditPersistableService;
	}

	/**
	 * Gets the audit query service.
	 * 
	 * @return the audit query service
	 */
	public IAuditQueryService getAuditQueryService() {
		return auditQueryService;
	}

	/**
	 * Sets the audit query service.
	 * 
	 * @param auditQueryService
	 *            the new audit query service
	 */
	public void setAuditQueryService(IAuditQueryService auditQueryService) {
		this.auditQueryService = auditQueryService;
	}

       /**
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.mumz.test.audit.controller.IAuditController#addBook(com.mumz.test.
	 * audit.beans.Book)
	 */
	@Override
	public Book addBook(Book book) {
		return this.getAuditPersistableService().addEntity(book);
	}

       /** (non-Javadoc)
	 * @see com.mumz.test.audit.controller.IAuditController#updateBook(com.mumz.test.audit.beans.Book)
	 */
	@Override
	public Book updateBook(Book book) {
		return this.getAuditPersistableService().updateEntity(book);
	}

       /**
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.mumz.test.audit.controller.IAuditController#removeBook(com.mumz.test
	 * .audit.beans.Book)
	 */
	@Override
	public Book removeBook(Book book) {
		return this.getAuditPersistableService().removeEntity(book);
	}

       /**
	 * (non-Javadoc)
	 * 
	 * @see com.mumz.test.audit.controller.IAuditController#fetchAllBooks()
	 */
	@SuppressWarnings("unchecked")
	@Override
	public List<Book> fetchAllBooks() {
		return (List<Book>) this.getAuditQueryService().fetchAllBooks();
	}

       /**
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.mumz.test.audit.controller.IAuditController#addBookShelf(com.mumz.
	 * test.audit.beans.BookShelf)
	 */
	@Override
	public BookShelf addBookShelf(BookShelf bookShelf) {
		return this.getAuditPersistableService().addEntity(bookShelf);
	}

       /** (non-Javadoc)
	 * @see com.mumz.test.audit.controller.IAuditController#updateBookShelf(com.mumz.test.audit.beans.BookShelf)
	 */
	public BookShelf updateBookShelf(BookShelf bookShelf) {
		return this.getAuditPersistableService().updateEntity(bookShelf);
	}

       /**
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.mumz.test.audit.controller.IAuditController#removeBookShelf(com.mumz
	 * .test.audit.beans.BookShelf)
	 */
	@Override
	public BookShelf removeBookShelf(BookShelf bookShelf) {
		return this.getAuditPersistableService().removeEntity(bookShelf);
	}

       /**
	 * (non-Javadoc)
	 * 
	 * @see com.mumz.test.audit.controller.IAuditController#fetchAllBookShelves()
	 */
	@SuppressWarnings("unchecked")
	@Override
	public List<BookShelf> fetchAllBookShelves() {
		return (List<BookShelf>) this.getAuditQueryService()
				.fetchAllBookShelves();
	}
}

Next we will implement our dao layer. IAuditPersistableService will be used when we are doing any add or update where as IAuditQueryService will be used fetching records.

IAuditPersistableService.java


package com.mumz.test.audit.dao;

import com.mumz.test.audit.interfaces.IEntity;

/**
 * The Interface IAuditPersistableService.
 * 
 * @author prabhat.jha
 */
public interface IAuditPersistableService {

	/**
	 * Adds the book.
	 * 
	 * @param entityToBeAdded
	 *            the book
	 * @return the book
	 */
	public <T extends IEntity> T addEntity(T entityToBeAdded);

	/**
	 * Update book.
	 * 
	 * @param entityToBeUpdated
	 *            the book
	 * @return the book
	 */
	public <T extends IEntity> T updateEntity(T entityToBeUpdated);

	/**
	 * Removes the book.
	 * 
	 * @param entityToBeRemoved
	 *            the book
	 * @return the book
	 */
	public <T extends IEntity> T removeEntity(T entityToBeRemoved);

}

AuditPersistableServiceImpl.java


package com.mumz.test.audit.dao;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.mumz.test.audit.interfaces.IEntity;

/**
 * The Class AuditPersistableServiceImpl.
 * @author prabhat.jha
 */
@Transactional
@Service
public class AuditPersistableServiceImpl implements IAuditPersistableService {

	/** The entity manager. */
	@PersistenceContext
	private EntityManager entityManager = null;

	/**
	 * Gets the entity manager.
	 *
	 * @return the entity manager
	 */
	public EntityManager getEntityManager() {
		return entityManager;
	}

	/**
	 * Sets the entity manager.
	 *
	 * @param entityManager the new entity manager
	 */
	public void setEntityManager(EntityManager entityManager) {
		this.entityManager = entityManager;
	}
	
	/** (non-Javadoc)
	 * @see com.mumz.test.audit.dao.IAuditPersistableService#addBook(com.mumz.test.audit.beans.Book)
	 */
	@Override
	public <T extends IEntity> T addEntity(T entityToBeAdded) {
		return this.getEntityManager().merge(entityToBeAdded);
	}

	/** (non-Javadoc)
	 * @see com.mumz.test.audit.dao.IAuditPersistableService#removeBook(com.mumz.test.audit.beans.Book)
	 */
	@Override
	public <T extends IEntity> T removeEntity(T entityToBeRemoved) {
		//Un-tested code, I think JPA doesn't allow direct remove, first a fetch and then subsequent remove is required.
                this.getEntityManager().remove(entityToBeRemoved);
		return entityToBeRemoved;
	}


	/** (non-Javadoc)
	 * @see com.mumz.test.audit.dao.IAuditPersistableService#updateBook(com.mumz.test.audit.beans.Book)
	 */
	@Override
	public <T extends IEntity> T updateEntity(T entityToBeUpdated) {
		return this.getEntityManager().merge(entityToBeUpdated);
	}
}

IAuditQueryService


package com.mumz.test.audit.dao;

import java.util.List;

import com.mumz.test.audit.interfaces.IEntity;

/**
 * The Interface IAuditQueryService.
 * @author prabhat.jha
 */
public interface IAuditQueryService {
	
	/**
	 * Fetch all books.
	 * 
	 * @return the list
	 */
	public List<? extends IEntity> fetchAllBooks();
	
	/**
	 * Fetch all book shelves.
	 * 
	 * @return the list
	 */
	public List<? extends IEntity> fetchAllBookShelves();
}

AuditQueryServiceImpl.java


package com.mumz.test.audit.dao;

import java.util.Collections;
import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.mumz.test.audit.beans.Book;
import com.mumz.test.audit.beans.BookShelf;

/**
 * The Class AuditQueryServiceImpl.
 * @author prabhat.jha
 */
@Transactional(readOnly = true)
@Service
public class AuditQueryServiceImpl implements IAuditQueryService {

	/** The entity manager. */
	@PersistenceContext
	private EntityManager entityManager = null;
	
	/**
	 * Gets the entity manager.
	 * 
	 * @return the entity manager
	 */
	public EntityManager getEntityManager() {
		return entityManager;
	}

	/**
	 * Sets the entity manager.
	 * 
	 * @param entityManager
	 *            the new entity manager
	 */
	public void setEntityManager(EntityManager entityManager) {
		this.entityManager = entityManager;
	}

	/** (non-Javadoc)
	 * @see com.mumz.test.audit.dao.IAuditPersistableService#fetchAllBooks()
	 */
	@SuppressWarnings("unchecked")
	@Override
	public List<Book> fetchAllBooks() {
		try {
			Query namedQuery = this.getEntityManager().createNamedQuery("fetchAllBooks");
			return namedQuery.getResultList();
		} catch (Exception e) {
			e.printStackTrace();
		}
		return Collections.emptyList();
	}

	/** (non-Javadoc)
	 * @see com.mumz.test.audit.dao.IAuditPersistableService#fetchAllBookShelves()
	 */
	@SuppressWarnings("unchecked")
	@Override
	public List<BookShelf> fetchAllBookShelves() {
		try {
			Query namedQuery = this.getEntityManager().createNamedQuery("fetchAllBookShelves");
			return namedQuery.getResultList();
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}
}

Next is our Audit Advice which actually does the audit entry.

AuditAdvice.java


package com.mumz.test.audit.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.mumz.test.audit.beans.Audit;
import com.mumz.test.audit.dao.IAuditPersistableService;
import com.mumz.test.audit.interfaces.IAuditable;
import com.mumz.test.audit.interfaces.IXMLConvertable;
import com.mumz.test.audit.utils.XStreamUtils;

/**
 * The Class BookAdvice.
 * 
 * @author prabhat.jha
 */
@Aspect
@Component
public class AuditAdvice {

	/** The audit persistable service. */
	@Autowired
	private IAuditPersistableService auditPersistableService = null;

	/**
	 * Gets the audit persistable service.
	 * 
	 * @return the audit persistable service
	 */
	public IAuditPersistableService getAuditPersistableService() {
		return auditPersistableService;
	}

	/**
	 * Sets the audit persistable service.
	 * 
	 * @param auditPersistableService
	 *            the new audit persistable service
	 */
	public void setAuditPersistableService(
			IAuditPersistableService auditPersistableService) {
		this.auditPersistableService = auditPersistableService;
	}

	/**
	 * Around remove advice.
	 * 
	 * @param pjp
	 *            the pjp
	 */
	@Around("execution(* com.mumz.test.audit.controller.AuditControllerImpl.*(..))")
	public void aroundAddBookShelfAdvice(ProceedingJoinPoint pjp) {
		String methodName = pjp.getSignature().getName();
		String operation = null;
		if (methodName.toUpperCase().contains("ADD")) {
			operation = IAuditable.OPERATION_INSERT;
		} else if (methodName.toUpperCase().contains("UPDATE")) {
			operation = IAuditable.OPERATION_UPDATE;
		} else {
			operation = IAuditable.OPERATION_DELETE;
		}
		Object[] arguments = pjp.getArgs();
		IXMLConvertable preImage = null;
		IXMLConvertable postImage = null;
		for (Object object : arguments) {
			if (!operation.equalsIgnoreCase(IAuditable.OPERATION_INSERT)
					&& object instanceof IAuditable
					&& object instanceof IXMLConvertable) {
				preImage = (IXMLConvertable) object;
			}
		}
		try {
			Object returnValue = pjp.proceed();
			if (operation.equalsIgnoreCase(IAuditable.OPERATION_UPDATE)
					&& returnValue instanceof IAuditable
					&& returnValue instanceof IXMLConvertable) {
				postImage = (IXMLConvertable) returnValue;
			}
		} catch (Throwable e) {
			e.printStackTrace();
		}
		if (preImage != null || postImage != null) {
			Audit auditableEntity = new Audit();
			auditableEntity.setOperation(IAuditable.OPERATION_INSERT);
			auditableEntity.setPreImage(XStreamUtils.getXMLFromObject(preImage,
					preImage.getClass().getName(), preImage.getFieldAliases(),
					preImage.getFieldsToBeOmitted()));
			auditableEntity.setPostImage(XStreamUtils.getXMLFromObject(
					postImage, postImage.getClass().getName(),
					postImage.getFieldAliases(),
					postImage.getFieldsToBeOmitted()));
			this.getAuditPersistableService().addEntity(auditableEntity);
		}
	}
}

Next is our XStreamUtils which is a generic code and converts given IXMLConvertable object and returns a XML representation of it.

XStreamUtils.java


package com.mumz.test.audit.utils;

import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;

import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.xml.DomDriver;

/**
 * The Class XStreamUtils.
 * 
 * @author prabhat.jha
 */
public class XStreamUtils {

	/**
	 * Gets the xML from object.
	 * 
	 * @param toBeConverted
	 *            the to be converted
	 * @param classNameAlias
	 *            the class name alias
	 * @param fieldAlias
	 *            the field alias
	 * @param fieldsToBeOmitted
	 *            the fields to be omitted
	 * @return the xML from object
	 */
	public static String getXMLFromObject(Object toBeConverted,
			String classNameAlias, Map<String, String> fieldAlias,
			List<String> fieldsToBeOmitted) {
		StringBuilder objectAsXML = new StringBuilder();
		if (toBeConverted != null) {
			XStream xStream = new XStream(new DomDriver());
			if (StringUtils.isNotEmpty(classNameAlias)) {
				xStream.alias(classNameAlias, toBeConverted.getClass());
			}
			if (fieldAlias != null && !fieldAlias.isEmpty()) {
				for (Entry<String, String> entry : fieldAlias.entrySet()) {
					xStream.aliasField(entry.getKey(),
							toBeConverted.getClass(), entry.getValue());
				}
			}
			if (CollectionUtils.isNotEmpty(fieldsToBeOmitted)) {
				for (String fieldToBeOmitted : fieldsToBeOmitted) {
					xStream.omitField(toBeConverted.getClass(),
							fieldToBeOmitted);
				}
			}
			objectAsXML.append(xStream.toXML(toBeConverted));
		}
		return objectAsXML.toString();
	}
}

Next is our spring configuration file.

audit-bean-definition.xml


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
	xmlns:aop="http://www.springframework.org/schema/aop"
	xmlns:tx="http://www.springframework.org/schema/tx"
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation=
		"http://www.springframework.org/schema/beans 
		http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
		http://www.springframework.org/schema/tx 
		http://www.springframework.org/schema/tx/spring-tx-3.1.xsd
    	http://www.springframework.org/schema/aop 
    	http://www.springframework.org/schema/aop/spring-aop-3.1.xsd
    	http://www.springframework.org/schema/context
    	http://www.springframework.org/schema/context/spring-context-3.1.xsd">

	<bean id="propertyConfigurer"
		class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
		<property name="location">
			<value>jdbc.properties</value>
		</property>
	</bean>

	<bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />
	
	<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
		<property name="driverClassName">
			<value>${jdbc.driverClassName}</value>
		</property>
		<property name="url">
			<value>${jdbc.url}</value>
		</property>
		<property name="username">
			<value>${jdbc.username}</value>
		</property>
		<property name="password">
			<value>${jdbc.password}</value>
		</property>
	</bean>
	
	<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
		<property name="dataSource" ref="dataSource" />
		<property name="jpaVendorAdapter">
			<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
				<property name="database">
					<value>${jdbc.databaseVendor}</value>
				</property>
				<property name="showSql" value="false" />
			</bean>
		</property>
	</bean>
	
	<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
		<property name="entityManagerFactory" ref="entityManagerFactory" />
	</bean>

	<tx:annotation-driven transaction-manager="transactionManager" />
	
	<context:component-scan base-package="com.mumz.test.audit"></context:component-scan>
	
	<context:annotation-config></context:annotation-config>
	
	<aop:aspectj-autoproxy/>
</beans>

I have placed all the database properties inside jdbc.properties file

jdbc.properties


jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc\:mysql\://localhost\:3306/AuditSchema
jdbc.username=root
jdbc.password=root
hibernate.dialect=org.hibernate.dialect.MySQLDialect
jdbc.databaseVendor=MYSQL

JPA needs persistence.xml inside META-INF folder in the classpath, so we will create our persistence.xml.

persistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence" version="1.0">
	<persistence-unit name="auditPersitenceUnit">
		<class>com.mumz.test.audit.beans.Book</class>
		<class>com.mumz.test.audit.beans.BookShelf</class>
		<class>com.mumz.test.audit.beans.Audit</class>
	</persistence-unit>
</persistence>

Finally some test code to check our implementation.

AuditMainApp.java


package com.mumz.test.audit.app;

import java.util.ArrayList;
import java.util.List;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.mumz.test.audit.beans.Book;
import com.mumz.test.audit.beans.BookShelf;
import com.mumz.test.audit.interfaces.IAuditController;

public class AuditMainApp {
	public static void main(String[] args) {
		ApplicationContext applicationContext = new ClassPathXmlApplicationContext("audit-bean-definition.xml");
		IAuditController auditController = applicationContext.getBean(IAuditController.class);
		BookShelf bookShelf = new BookShelf();
		Book book  = new Book();
		book.setAuthor("Test Author 1");
		book.setName("Test Book 1");
		book.setBookShelf(bookShelf);
		List<Book> books = new ArrayList<Book>();
		books.add(book);
		bookShelf.setBooks(books);
		bookShelf.setName("Test 1");
		auditController.addBookShelf(bookShelf);
	}
}

Database schema used for this post is below.

Database Schema


SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0;
SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0;
SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='TRADITIONAL';

DROP SCHEMA IF EXISTS `auditschema` ;
CREATE SCHEMA IF NOT EXISTS `auditschema` DEFAULT CHARACTER SET utf8 ;
USE `auditschema` ;

-- -----------------------------------------------------
-- Table `auditschema`.`audit`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `auditschema`.`audit` ;

CREATE  TABLE IF NOT EXISTS `auditschema`.`audit` (
  `AUDIT_ID` INT(11) NOT NULL AUTO_INCREMENT ,
  `AUDIT_PRE_IMAGE` VARCHAR(3000) NULL DEFAULT NULL ,
  `AUDIT_POST_IMAGE` VARCHAR(3000) NULL DEFAULT NULL ,
  `AUDIT_OPERATION` VARCHAR(45) NULL DEFAULT NULL ,
  PRIMARY KEY (`AUDIT_ID`) )
ENGINE = InnoDB
AUTO_INCREMENT = 2
DEFAULT CHARACTER SET = utf8;

-- -----------------------------------------------------
-- Table `auditschema`.`book_shelf`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `auditschema`.`book_shelf` ;

CREATE  TABLE IF NOT EXISTS `auditschema`.`book_shelf` (
  `BOOK_SHELF_ID` INT(11) NOT NULL AUTO_INCREMENT ,
  `BOOK_SHELF_NAME` VARCHAR(45) NOT NULL ,
  PRIMARY KEY (`BOOK_SHELF_ID`) )
ENGINE = InnoDB
AUTO_INCREMENT = 10
DEFAULT CHARACTER SET = utf8;

-- -----------------------------------------------------
-- Table `auditschema`.`book`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `auditschema`.`book` ;

CREATE  TABLE IF NOT EXISTS `auditschema`.`book` (
  `BOOK_ID` INT(11) NOT NULL AUTO_INCREMENT ,
  `BOOK_NAME` VARCHAR(45) NOT NULL ,
  `BOOK_AUTHOR` VARCHAR(45) NOT NULL ,
  `BOOK_SHELF_ID` INT(11) NOT NULL ,
  PRIMARY KEY (`BOOK_ID`) ,
  INDEX `FK_BOOK_SHELF_ID` (`BOOK_SHELF_ID` ASC) ,
  CONSTRAINT `FK_BOOK_SHELF_ID`
    FOREIGN KEY (`BOOK_SHELF_ID` )
    REFERENCES `auditschema`.`book_shelf` (`BOOK_SHELF_ID` )
    ON DELETE NO ACTION
    ON UPDATE NO ACTION)
ENGINE = InnoDB
AUTO_INCREMENT = 9
DEFAULT CHARACTER SET = utf8;

SET SQL_MODE=@OLD_SQL_MODE;
SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS;
SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS;

If you have followed every step your code structure should look similar to:
Project Structure

That's all, It has been a very long post, I hope you find it useful.

There are 3 comments

  1. AG

    how do you know that the save to Book or BookeShelf is
    successful as you are catching the execution of the action and not
    completion of the transaction

  2. Dirk

    Excellent post- tried to run your source, and run into jpa issues in that your persistence.xml file seems to be invalid?
    Could not load properties; nested exception is java.util.InvalidPropertiesFormatException: org.xml.sax.SAXParseException; lineNumber: 4; columnNumber: 66; Document root element “persistence”, must match DOCTYPE root “null”.

Leave a comment