This was quickly answered here in this post by myself, but hiding the fact that we spent over two weeks trying different strategies to overcome this. So, here goes our final implementation we decided to use.
Basic idea: Create your own implementation of javax.persistence.spi.PersistenceProvider by extending the one given by Hibernate. For all effects, this is the only point where your code will be tied to Hibernate or any other vendor specific implementation.
public class MyHibernatePersistenceProvider extends org.hibernate.jpa.HibernatePersistenceProvider {
@Override
public EntityManagerFactory createContainerEntityManagerFactory(PersistenceUnitInfo info, Map properties) {
return new EntityManagerFactoryWrapper(super.createContainerEntityManagerFactory(info, properties));
}
}
The idea is to wrap hibernate's versions of EntityManagerFactory and EntityManager with your own implementation. So you need to create classes that implement these interfaces and keep the vendor specific implementation inside.
This is the EntityManagerFactoryWrapper
public class EntityManagerFactoryWrapper implements EntityManagerFactory {
private EntityManagerFactory emf;
public EntityManagerFactoryWrapper(EntityManagerFactory originalEMF) {
emf = originalEMF;
}
public EntityManager createEntityManager() {
return new EntityManagerWrapper(emf.createEntityManager());
}
// Implement all other methods for the interface
// providing a callback to the original emf.
The EntityManagerWrapper is our interception point. You will need to implement all methods from the interface. At every method where an entity can be modified, we include a call to a custom query to set local variables at the database.
public class EntityManagerWrapper implements EntityManager {
private EntityManager em;
private Principal principal;
public EntityManagerWrapper(EntityManager originalEM) {
em = originalEM;
}
public void setAuditVariables() {
String userid = getUserId();
String ipaddr = getUserAddr();
String sql = "SET LOCAL application.userid='"+userid+"'; SET LOCAL application.ipaddr='"+ipaddr+"'";
em.createNativeQuery(sql).executeUpdate();
}
protected String getUserAddr() {
HttpServletRequest httprequest = CDIBeanUtils.getBean(HttpServletRequest.class);
String ipaddr = "";
if ( httprequest != null ) {
ipaddr = httprequest.getRemoteAddr();
}
return ipaddr;
}
protected String getUserId() {
String userid = "";
// Try to look up a contextual reference
if ( principal == null ) {
principal = CDIBeanUtils.getBean(Principal.class);
}
// Try to assert it from CAS authentication
if (principal == null || "anonymous".equalsIgnoreCase(principal.getName())) {
if (AssertionHolder.getAssertion() != null) {
principal = AssertionHolder.getAssertion().getPrincipal();
}
}
if ( principal != null ) {
userid = principal.getName();
}
return userid;
}
@Override
public void persist(Object entity) {
if ( em.isJoinedToTransaction() ) {
setAuditVariables();
}
em.persist(entity);
}
@Override
public <T> T merge(T entity) {
if ( em.isJoinedToTransaction() ) {
setAuditVariables();
}
return em.merge(entity);
}
@Override
public void remove(Object entity) {
if ( em.isJoinedToTransaction() ) {
setAuditVariables();
}
em.remove(entity);
}
// Keep implementing all methods that can change
// entities so you can setAuditVariables() before
// the changes are applied.
@Override
public void createNamedQuery(.....
Downside: Interception queries (SET LOCAL) will likely run several times inside a single transaction, specially if there are several statements made on a single service call. Given the circumstances, we decided to keep it this way due to the fact that it's a simple SET LOCAL in memory call to PostgreSQL. Since there are no tables involved, we can live with the performance hit.
Now just replace Hibernate's persistence provider inside persistence.xml:
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"
version="2.1">
<persistence-unit name="petstore" transaction-type="JTA">
<provider>my.package.HibernatePersistenceProvider</provider>
<jta-data-source>java:app/jdbc/exemplo</jta-data-source>
<properties>
<property name="hibernate.transaction.jta.platform" value="org.hibernate.service.jta.platform.internal.SunOneJtaPlatform" />
<property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect"/>
</properties>
</persistence-unit>
As a side note, this is the CDIBeanUtils we have to help with the bean manager on some special occasions. In this case, we are using it to look up a reference to HttpServletRequest and Principal.
public class CDIBeanUtils {
public static <T> T getBean(Class<T> beanClass) {
BeanManager bm = CDI.current().getBeanManager();
Iterator<Bean<?>> ite = bm.getBeans(beanClass).iterator();
if (!ite.hasNext()) {
return null;
}
final Bean<T> bean = (Bean<T>) ite.next();
final CreationalContext<T> ctx = bm.createCreationalContext(bean);
final T t = (T) bm.getReference(bean, beanClass, ctx);
return t;
}
}
To be fair, this is not exactly intercepting Transactions events. But we are able to include the custom queries we need inside the transaction.
Hopefully this can help others avoid the pain we went through.