As most other repetitive tasks, the audit logging can be automated and the great thing is: I'll show you how!
We get started by somehow marking the entities you want to be tracable and there are two ways of doing this. Either, by creating a @MappedSuperclass or by implementing an interface.
Let's start with the MappedSuperclass-way of doing it.
Create a class called TraceableEntity:
@MappedSuperclass
public abstract class TraceableEntity implements Serializable {
@Column(name = "created")
@Temporal(TemporalType.TIMESTAMP)
private Date created;
@Column(name = "created_by")
private String createdBy;
@Column(name = "changed")
@Temporal(TemporalType.TIMESTAMP)
private Date changed;
@Column(name = "changed_by")
private String changedBy;
// ... getters/setters
}
Of course, this super class has to be implemented somewhere and for the sake of this example, we have an entity called Invoice (beware, it's grossly simplified):
@Entity
@Table(name = "invoice")
public class Invoice extends TraceableEntity {
@Id
@GeneratedValue
@Column(name = "id")
private Integer id;
@Column(name = "product");
private String product;
@Column(name = "receiver");
private String receiver;
@Column(name = "amount")
private Double amount;
// ... getters/setters
}
The observant reader will instantly think: "Hmmm, what if my entity already extends another class"? Well, this is a problem, obviously (if only we had multiple inheritance in Java :D)... You could solve this by making the Entity's superclass extend the TraceableEntity.
Another option would be the previously mentioned Interface-way of doing it, so let's create an interface called Traceable:
public interface Traceable {
void setCreated(Date created);
void setCreatedBy(String createdBy);
void setChanged(Date changed);
void setChangedBy(String changedBy);
}
Let's reuse the Invoice-entity but make it implement the Traceable interface instead:
@Entity
@Table(name = "invoice")
public class Invoice implements Traceable {
@Id
@GeneratedValue
@Column(name = "id")
private Integer id;
@Column(name = "product");
private String product;
@Column(name = "receiver");
private String receiver;
@Column(name = "amount")
private Double amount;
@Column(name = "created")
@Temporal(TemporalType.TIMESTAMP)
private Date created;
@Column(name = "created_by")
private String createdBy;
@Column(name = "changed")
@Temporal(TemporalType.TIMESTAMP)
private Date changed;
@Column(name = "changed_by")
private String changedBy;
// ... getters/setters
}
This works just as well, however, you will end up copying and pasting all the created/changed-stuff. Decide for yourself which method you like to use.
Time for the magic, let's implement an Entity Listener (which basically is just a POJO) and let's call it AuditLogger:
public class AuditLogger {
private static InitialContext ctx;
public AuditLogger() {
try {
if (ctx == null) ctx = new InitialContext();
} catch (NamingException e) {
System.out.println(e.getMessage());
}
}
public void prePersist(Object entity) {
if (entity instanceof Traceable) { // ... or TraceableEntity ;)
Traceable traceable = (Traceable) entity;
String callerId = getCallerIdentity();
traceable.setCreated(new Date());
traceable.setCreatedBy(callerId);
}
}
public void preUpdate(Object entity) {
if (entity instanceof Traceable) {
Traceable traceable = (Traceable) entity;
traceable.setChanged(new Date());
traceable.setChangedBy(getCallerIdentity());
}
}
private String getCallerIdentity() {
try {
EJBContext ejbCtx = (EJBContext) ctx.lookup("java:comp/EJBContext");
return ejbCtx.getCallerPrincipal().getName(); // get identity with JAAS
} catch (Exception e) {
System.err.println(e.getMessage());
return "unknown";
}
}
}
Since I am writing this without the aid of an IDE, this class is very simplified. The InitialContext-creation should be improved in a production-environment as well as the error logging. Beware of the fact that Entity Listener life cycle methods can not throw checked exceptions so in reality you just have two options: either throwing an EJBException (or any kind of RuntimeException) or by swalloing the errors... however, even if you ignore an error, you should do proper logging. Otherwise you will have a really hard time to debug possible errors in your Listener.
You may also notice that I am expecting you to use JAAS to get the identity of the person logged into your system. If you don't use JAAS, you have to alter the code to fit your needs.
Lastly, since JBoss AS only created one instance of a listener (to my knowledge), it's unnecessary to check the ctx-variable for null-values or declaring it to be static. Unfortunately, I don't know whether other Containers behave the same way so I added it anyway just to be on the safe side.
Anyway, now that we creates the listener, we have to get the Persistence Provider to invoke it whenever an entity is inserted or updated. This is done with the orm.xml-file.
Add following to your orm.xml:
<persistence-unit-metadata>
<persistence-unit-defaults>
<entity-listeners>
<entity-listener class="org.mindbug.TraceLogger">
<pre-persist method-name="prePersist"/>
<pre-update method-name="preUpdate"/>
</entity-listener>
</entity-listeners>
</persistence-unit-defaults>
</persistence-unit-metadata>
So far so good, that's all we need, but... after telling you all this I should also bring to your attentention that you could use an EJB Interceptor instead. However, even though it's possible to differentiate whether you do a create or an update-operation (by checking if the created-property has been set before), it still sets the timestamps when the method is invoked and not just before JPA is committing your changes to the database. Whether this is desirable or not is up to you, I just want to mention it for the sake of completeness.
Phew, now that was something and a half and believe it or not, we are actually finished. I hope you find it useful when creating your next enterprise application!