Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ways to pass additional data to Custom RevisionEntity in Hibernate Envers?

It's RESTful web app. I am using Hibernate Envers to store historical data. Along with revision number and timestamp, I also need to store other details (for example: IP address and authenticated user). Envers provides multiple ways to have a custom revision entity which is awesome. I am facing problem in setting the custom data on the revision entity.

@RevisionEntity( MyCustomRevisionListener.class )
public class MyCustomRevisionEntity extends DefaultRevisionEntity {
    private String userName;

    private String ip;

    //Accessors
}

public class MyCustomRevisionListener implements RevisionListener {
    public void newRevision( Object revisionEntity ) {
        MyCustomRevisionEntity customRevisionEntity = ( MyCustomRevisionEntity ) revisionEntity;
        //Here I need userName and Ip address passed as arguments somehow, so that I can set them on the revision entity. 
    }
}

Since newRevision() method does not allow any additional arguments, I can not pass my custom data (like username and ip) to it. How can I do that?

Envers also provides another approach as:

An alternative method to using the org.hibernate.envers.RevisionListener is to instead call the getCurrentRevision( Class revisionEntityClass, boolean persist ) method of the org.hibernate.envers.AuditReader interface to obtain the current revision, and fill it with desired information.

So using the above approach, I'll have to do something like this:

Change my current dao method like:

public void persist(SomeEntity entity) {
     ...
     entityManager.persist(entity);
     ...
}

to

public void persist(SomeEntity entity, String userName, String ip) {
     ...
     //Do the intended work
     entityManager.persist(entity);
     //Do the additional work
     AuditReader reader = AuditReaderFactory.get(entityManager)
     MyCustomRevisionEntity revision = reader.getCurrentRevision(MyCustomRevisionEntity, false);
     revision.setUserName(userName);
     revision.setIp(ip);

}

I don't feel very comfortable with this approach as keeping audit data seems a cross cutting concern to me. And I obtain the userName and Ip and other data through HTTP request object. So all that data will need to flow down right from entry point of application (controller) to the lowest layer (dao layer).

Is there any other way in which I can achieve this? I am using Spring.

I am imagining something like Spring keeping information about the 'stack' to which a particular method invocation belongs. So that when newRevision() in invoked, I know which particular invocation at the entry point lead to this invocation. And also, I can somehow obtain the arguments passed to first method of the call stack.

like image 967
Anmol Gupta Avatar asked Jun 14 '16 14:06

Anmol Gupta


1 Answers

One good way to do this would be to leverage a ThreadLocal variable.

As an example, Spring Security has a filter that initializes a thread local variable stored in SecurityContextHolder and then you can access this data from that specific thread simply by doing something like:

SecurityContext ctx = SecurityContextHolder.getSecurityContext();
Authorization authorization = ctx.getAuthorization();

So imagine an additional interceptor that your web framework calls that either adds additional information to the spring security context, perhaps in a custom user details object if using spring security or create your own holder & context object to hold the information the listener needs.

Then it becomes a simple:

public class MyRevisionEntityListener implements RevisionListener {
  @Override
  public void newRevision(Object revisionEntity) {
    // If you use spring security, you could use SpringSecurityContextHolder.
    final UserContext userContext = UserContextHolder.getUserContext();
    MyRevisionEntity mre = MyRevisionEntity.class.cast( revisionEntity );
    mre.setIpAddress( userContext.getIpAddress() );
    mre.setUserName( userContext.getUserName() );
  } 
}

This feels like the cleanest approach to me.

It is worth noting that the other API getCurrentRevision(Session,boolean) was deprecated as of Hibernate 5.2 and is scheduled for removal in 6.0. While an alternative means may be introduced, the intended way to perform this type of logic is using a RevisionListener.

like image 106
Naros Avatar answered Oct 03 '22 07:10

Naros