8

Long story short: We develop and maintain a library that can be used in other projects using JavaEE7/CDI/JPA. Applications will run under Glassfish-4.0 and use Hibernate's JPA implementation for an underlying PostgreSQL persistence. This is part of a long term migration effort to rewrite old applications that were written in Spring/Struts/Hibernate into the new world of JavaEE7/CDI/JTA.

The problem: For audit purposes, our library needs to intercept all database transactions and include custom SQL statements before the user statements are executed. At this point, the current username and IP address need to be inserted into a temporary database variable (vendor specific feature) so that a database trigger can read them to create the audit trail for any row modification. This particular post was very helpful providing alternatives, and our team went down the trigger road due to a previously established legacy.

HOWEVER: We are deeply disappointed at how JTA handles transaction events. There are numerous ways to intercept transactions, but this particular case seems to be down right impossible. In the old architecture, using Spring's transaction manager, we simply used a Hibernate Interceptor implementing Interceptor.afterTransactionBegin(...). Reading up on the official JTA-1.2 spec, we found that it does have support for Synchronization.beforeCompletion and Synchronization.afterCompletion. After several hours of debugging sessions we clearly noted that Hibernate's implementation of JTA is using these facilities. But JTA seems to be lacking events like beforeBegin and afterBegin (which IMHO seems to be a lack of common sense). And since there are no facilities to intercept those, Hibernate complies fully with JTA and it simply won't. Period.

No matter what we do, we can't find a way. We tried, for instance, to intercept @Transactional annotations and run our code just after the container's JTA impl does its job to open the transaction. But we lack the ability to dynamically acquire the EntityManager associated with that particular transaction. Remember: this is a library, not the web application itself. It cannot make any assumptions about which Persistence Units are declared and used by the application. And, as far as we can tell, we need to know which specific Persistent Unit name to inject it into our code. We are trying to provide an audit facility to other temas that is as transparent as possible.

So we humbly ask for help. If anyone out there has a solution, workaround, whatever opinion, we'll be glad to hear it.

Community
  • 1
  • 1
JulioHM
  • 1,229
  • 1
  • 10
  • 17
  • 1
    Bypass JPA entirely and add an interceptor on the actual database connection pool? I only know how to do this in Tomcat jdbc.pool, but one would hope Glassfish has a way. – Affe Aug 28 '14 at 15:56
  • That could be the case, but at this level I don't think we have access to the Http session to acquire any logged in user or their client IP address. – JulioHM Aug 28 '14 at 18:40

1 Answers1

6

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.

JulioHM
  • 1,229
  • 1
  • 10
  • 17
  • Were you able to make this work? I've followed your pattern and am have exceptions coming out of the proxied EntityManagerFactory. Thanks in advance. – Shawn Eion Smith Aug 31 '15 at 20:07
  • @JulioHM in the persistence.xml is that a typo? shouldn't be: my.package.MyHibernatePersistenceProvider instead of HibernatePersistenceProvider – Alboz May 30 '19 at 15:30