Package org.hibernate.envers.strategy

Source Code of org.hibernate.envers.strategy.ValidityAuditStrategy

package org.hibernate.envers.strategy;

import java.io.Serializable;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.jboss.logging.Logger;

import org.hibernate.LockOptions;
import org.hibernate.Session;
import org.hibernate.dialect.Dialect;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.envers.RevisionType;
import org.hibernate.envers.configuration.AuditConfiguration;
import org.hibernate.envers.configuration.AuditEntitiesConfiguration;
import org.hibernate.envers.configuration.GlobalConfiguration;
import org.hibernate.envers.entities.mapper.PersistentCollectionChangeData;
import org.hibernate.envers.entities.mapper.relation.MiddleComponentData;
import org.hibernate.envers.entities.mapper.relation.MiddleIdData;
import org.hibernate.envers.synchronization.SessionCacheCleaner;
import org.hibernate.envers.tools.query.Parameters;
import org.hibernate.envers.tools.query.QueryBuilder;
import org.hibernate.event.service.spi.EventListenerRegistry;
import org.hibernate.event.spi.AutoFlushEvent;
import org.hibernate.event.spi.AutoFlushEventListener;
import org.hibernate.event.spi.EventSource;
import org.hibernate.event.spi.EventType;
import org.hibernate.jdbc.ReturningWork;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.persister.entity.Queryable;
import org.hibernate.persister.entity.UnionSubclassEntityPersister;
import org.hibernate.property.Getter;
import org.hibernate.sql.Update;
import org.hibernate.type.Type;

import static org.hibernate.envers.entities.mapper.relation.query.QueryConstants.MIDDLE_ENTITY_ALIAS;
import static org.hibernate.envers.entities.mapper.relation.query.QueryConstants.REVISION_PARAMETER;

/**
*  Audit strategy which persists and retrieves audit information using a validity algorithm, based on the
*  start-revision and end-revision of a row in the audit tables.
<p>This algorithm works as follows:
<ul>
<li>For a <strong>new row</strong> that is persisted in an audit table, only the <strong>start-revision</strong> column of that row is set</li>
<li>At the same time the <strong>end-revision</strong> field of the <strong>previous</strong> audit row is set to this revision</li>
<li>Queries are retrieved using 'between start and end revision', instead of a subquery.</li>
</ul>
</p>
<p>
*  This has a few important consequences that need to be judged against against each other:
<ul>
<li>Persisting audit information is a bit slower, because an extra row is updated</li>
<li>Retrieving audit information is a lot faster</li>
</ul>
</p>
*
* @author Stephanie Pau
* @author Adam Warski (adam at warski dot org)
* @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com)
*/
public class ValidityAuditStrategy implements AuditStrategy {
  private static final Logger log = Logger.getLogger( ValidityAuditStrategy.class );

  /** getter for the revision entity field annotated with @RevisionTimestamp */
    private Getter revisionTimestampGetter = null;

    private final SessionCacheCleaner sessionCacheCleaner;

    public ValidityAuditStrategy() {
        sessionCacheCleaner = new SessionCacheCleaner();
    }

    public void perform(
      final Session session,
      String entityName,
      final AuditConfiguration auditCfg,
      final Serializable id,
      Object data,
      final Object revision) {

        final AuditEntitiesConfiguration audEntitiesCfg = auditCfg.getAuditEntCfg();
        final String auditedEntityName = audEntitiesCfg.getAuditEntityName( entityName );
    final String revisionInfoEntityName = auditCfg.getAuditEntCfg().getRevisionInfoEntityName();
    final SessionImplementor sessionImplementor = (SessionImplementor) session;
    final Dialect dialect = sessionImplementor.getFactory().getDialect();

        // Save the audit data
        session.save(auditedEntityName, data);
        sessionCacheCleaner.scheduleAuditDataRemoval(session, data);

        // Update the end date of the previous row if this operation is expected to have a previous row
        if (getRevisionType(auditCfg, data) != RevisionType.ADD) {
      final Queryable productionEntityQueryable = getQueryable( entityName, sessionImplementor );
      final Queryable rootProductionEntityQueryable = getQueryable( productionEntityQueryable.getRootEntityName(), sessionImplementor );
      final Queryable auditedEntityQueryable = getQueryable( auditedEntityName, sessionImplementor );
      final Queryable rootAuditedEntityQueryable = getQueryable( auditedEntityQueryable.getRootEntityName(), sessionImplementor );
      final Queryable revisionInfoEntityQueryable = getQueryable( revisionInfoEntityName, sessionImplementor );

      final String updateTableName;
      if ( UnionSubclassEntityPersister.class.isInstance( rootProductionEntityQueryable ) ) {
        // this is the condition causing all the problems in terms of the generated SQL UPDATE
        // the problem being that we currently try to update the in-line view made up of the union query
        //
        // this is extremely hacky means to get the root table name for the union subclass style entities.
        // hacky because it relies on internal behavior of UnionSubclassEntityPersister
        // !!!!!! NOTICE - using subclass persister, not root !!!!!!
        updateTableName = auditedEntityQueryable.getSubclassTableName( 0 );
      }
      else {
        updateTableName = rootAuditedEntityQueryable.getTableName();
      }


      // first we need to flush the session in order to have the new audit data inserted
      // todo: expose org.hibernate.internal.SessionImpl.autoFlushIfRequired via SessionImplementor
      // for now, we duplicate some of that logic here
      autoFlushIfRequired( sessionImplementor, rootAuditedEntityQueryable, revisionInfoEntityQueryable );

      final Type revisionInfoIdType = sessionImplementor.getFactory()
          .getEntityPersister( revisionInfoEntityName )
          .getIdentifierType();
      final String revEndColumnName = rootAuditedEntityQueryable.toColumns( auditCfg.getAuditEntCfg().getRevisionEndFieldName() )[0];

      final boolean isRevisionEndTimestampEnabled = auditCfg.getAuditEntCfg().isRevisionEndTimestampEnabled();

      // update audit_ent set REVEND = ? [, REVEND_TSTMP = ?] where (prod_ent_id) = ? and REV <> ? and REVEND is null
      final Update update = new Update( dialect ).setTableName( updateTableName );
      // set REVEND = ?
      update.addColumn( revEndColumnName );
      // set [, REVEND_TSTMP = ?]
      if ( isRevisionEndTimestampEnabled ) {
        update.addColumn(
            rootAuditedEntityQueryable.toColumns(
                auditCfg.getAuditEntCfg().getRevisionEndTimestampFieldName()
            )[0]
        );
      }

      // where (prod_ent_id) = ?
      update.addPrimaryKeyColumns( rootProductionEntityQueryable.getIdentifierColumnNames() );
      // where REV <> ?
      update.addWhereColumn(
          rootAuditedEntityQueryable.toColumns(
              auditCfg.getAuditEntCfg().getRevisionNumberPath()
          )[0],
          "<> ?"
      );
      // where REVEND is null
      update.addWhereColumn( revEndColumnName, " is null" );

      // Now lets execute the sql...
      final String updateSql = update.toStatementString();

      int rowCount = session.doReturningWork(
          new ReturningWork<Integer>() {
            @Override
            public Integer execute(Connection connection) throws SQLException {
              PreparedStatement preparedStatement = connection.prepareStatement( updateSql );

              try {
                int index = 1;

                // set REVEND = ?
                final Number revisionNumber = auditCfg.getRevisionInfoNumberReader().getRevisionNumber( revision );
                revisionInfoIdType.nullSafeSet( preparedStatement, revisionNumber, index, sessionImplementor );
                index += revisionInfoIdType.getColumnSpan( sessionImplementor.getFactory() );

                // set [, REVEND_TSTMP = ?]
                if ( isRevisionEndTimestampEnabled ) {
                  final Object revEndTimestampObj = revisionTimestampGetter.get( revision );
                  final Date revisionEndTimestamp = convertRevEndTimestampToDate( revEndTimestampObj );
                  final Type revEndTsType = rootAuditedEntityQueryable.getPropertyType(
                      auditCfg.getAuditEntCfg().getRevisionEndTimestampFieldName()
                  );
                  revEndTsType.nullSafeSet( preparedStatement, revisionEndTimestamp, index, sessionImplementor );
                  index += revEndTsType.getColumnSpan( sessionImplementor.getFactory() );
                }

                // where (prod_ent_id) = ?
                final Type idType = rootProductionEntityQueryable.getIdentifierType();
                idType.nullSafeSet( preparedStatement, id, index, sessionImplementor );
                index += idType.getColumnSpan( sessionImplementor.getFactory() );

                // where REV <> ?
                final Type revType = rootAuditedEntityQueryable.getPropertyType(
                    auditCfg.getAuditEntCfg().getRevisionNumberPath()
                );
                revType.nullSafeSet( preparedStatement, revisionNumber, index, sessionImplementor );

                // where REVEND is null
                //     nothing to bind....

                return preparedStatement.executeUpdate();
              }
              finally {
                try {
                  preparedStatement.close();
                }
                catch (SQLException e) {
                  log.debug( "Could not release prepared statement : " + e.getMessage() );
                }
              }
            }
          }
      );

      if ( rowCount != 1 ) {
        throw new RuntimeException(
            "Cannot update previous revision for entity " + auditedEntityName + " and id " + id
        );
      }
    }
    }

  private Queryable getQueryable(String entityName, SessionImplementor sessionImplementor) {
    return (Queryable) sessionImplementor.getFactory().getEntityPersister( entityName );
  }

  private void autoFlushIfRequired(
      SessionImplementor sessionImplementor,
      Queryable auditedEntityQueryable,
      Queryable revisionInfoEntityQueryable) {
    final Set<String> querySpaces = new HashSet<String>();
    querySpaces.add( auditedEntityQueryable.getTableName() );
    querySpaces.add( revisionInfoEntityQueryable.getTableName() );
    final AutoFlushEvent event = new AutoFlushEvent( querySpaces, (EventSource) sessionImplementor );
    final Iterable<AutoFlushEventListener> listeners = sessionImplementor.getFactory().getServiceRegistry()
        .getService( EventListenerRegistry.class )
        .getEventListenerGroup( EventType.AUTO_FLUSH )
        .listeners();
    for ( AutoFlushEventListener listener : listeners ) {
      listener.onAutoFlush( event );
    }
  }

    @SuppressWarnings({"unchecked"})
    public void performCollectionChange(Session session, AuditConfiguration auditCfg,
                                        PersistentCollectionChangeData persistentCollectionChangeData, Object revision) {
        final QueryBuilder qb = new QueryBuilder(persistentCollectionChangeData.getEntityName(), MIDDLE_ENTITY_ALIAS);

        // Adding a parameter for each id component, except the rev number
        final String originalIdPropName = auditCfg.getAuditEntCfg().getOriginalIdPropName();
        final Map<String, Object> originalId = (Map<String, Object>) persistentCollectionChangeData.getData().get(
                originalIdPropName);
        for (Map.Entry<String, Object> originalIdEntry : originalId.entrySet()) {
            if (!auditCfg.getAuditEntCfg().getRevisionFieldName().equals(originalIdEntry.getKey())) {
                qb.getRootParameters().addWhereWithParam(originalIdPropName + "." + originalIdEntry.getKey(),
                        true, "=", originalIdEntry.getValue());
            }
        }

        addEndRevisionNullRestriction(auditCfg, qb.getRootParameters());

        final List<Object> l = qb.toQuery(session).setLockOptions(LockOptions.UPGRADE).list();

        // Update the last revision if one exists.
        // HHH-5967: with collections, the same element can be added and removed multiple times. So even if it's an
        // ADD, we may need to update the last revision.
        if (l.size() > 0) {
            updateLastRevision(session, auditCfg, l, originalId, persistentCollectionChangeData.getEntityName(), revision);
        }

        // Save the audit data
        session.save(persistentCollectionChangeData.getEntityName(), persistentCollectionChangeData.getData());
        sessionCacheCleaner.scheduleAuditDataRemoval(session, persistentCollectionChangeData.getData());
    }

    private void addEndRevisionNullRestriction(AuditConfiguration auditCfg, Parameters rootParameters) {
        rootParameters.addWhere(auditCfg.getAuditEntCfg().getRevisionEndFieldName(), true, "is", "null", false);
    }

    public void addEntityAtRevisionRestriction(GlobalConfiguration globalCfg, QueryBuilder rootQueryBuilder,
      String revisionProperty,String revisionEndProperty, boolean addAlias,
            MiddleIdData idData, String revisionPropertyPath, String originalIdPropertyName,
            String alias1, String alias2) {
    Parameters rootParameters = rootQueryBuilder.getRootParameters();
    addRevisionRestriction(rootParameters, revisionProperty, revisionEndProperty, addAlias);
  }
 
  public void addAssociationAtRevisionRestriction(QueryBuilder rootQueryBuilder,  String revisionProperty,
        String revisionEndProperty, boolean addAlias, MiddleIdData referencingIdData,
        String versionsMiddleEntityName, String eeOriginalIdPropertyPath, String revisionPropertyPath,
        String originalIdPropertyName, MiddleComponentData... componentDatas) {
    Parameters rootParameters = rootQueryBuilder.getRootParameters();
    addRevisionRestriction(rootParameters, revisionProperty, revisionEndProperty, addAlias);
  }
   
  public void setRevisionTimestampGetter(Getter revisionTimestampGetter) {
    this.revisionTimestampGetter = revisionTimestampGetter;
  }

    private void addRevisionRestriction(Parameters rootParameters, 
      String revisionProperty, String revisionEndProperty, boolean addAlias) {
     
    // e.revision <= _revision and (e.endRevision > _revision or e.endRevision is null)
    Parameters subParm = rootParameters.addSubParameters("or");
    rootParameters.addWhereWithNamedParam(revisionProperty, addAlias, "<=", REVISION_PARAMETER);
    subParm.addWhereWithNamedParam(revisionEndProperty + ".id", addAlias, ">", REVISION_PARAMETER);
    subParm.addWhere(revisionEndProperty, addAlias, "is", "null", false);
  }

    @SuppressWarnings({"unchecked"})
    private RevisionType getRevisionType(AuditConfiguration auditCfg, Object data) {
        return (RevisionType) ((Map<String, Object>) data).get(auditCfg.getAuditEntCfg().getRevisionTypePropName());
    }

    @SuppressWarnings({"unchecked"})
    private void updateLastRevision(Session session, AuditConfiguration auditCfg, List<Object> l,
                                    Object id, String auditedEntityName, Object revision) {

        // There should be one entry
        if (l.size() == 1) {
            // Setting the end revision to be the current rev
            Object previousData = l.get(0);
            String revisionEndFieldName = auditCfg.getAuditEntCfg().getRevisionEndFieldName();
            ((Map<String, Object>) previousData).put(revisionEndFieldName, revision);

            if (auditCfg.getAuditEntCfg().isRevisionEndTimestampEnabled()) {
                // Determine the value of the revision property annotated with @RevisionTimestamp
              String revEndTimestampFieldName = auditCfg.getAuditEntCfg().getRevisionEndTimestampFieldName();
              Object revEndTimestampObj = this.revisionTimestampGetter.get(revision);
               Date revisionEndTimestamp = convertRevEndTimestampToDate(revEndTimestampObj);

              // Setting the end revision timestamp
              ((Map<String, Object>) previousData).put(revEndTimestampFieldName, revisionEndTimestamp);
            }
           
            // Saving the previous version
            session.save(auditedEntityName, previousData);
            sessionCacheCleaner.scheduleAuditDataRemoval(session, previousData);
        } else {
            throw new RuntimeException("Cannot find previous revision for entity " + auditedEntityName + " and id " + id);
        }
    }

    private Date convertRevEndTimestampToDate(Object revEndTimestampObj) {
        // convert to a java.util.Date
        if (revEndTimestampObj instanceof Date) {
            return (Date) revEndTimestampObj;
        }
        return new Date((Long) revEndTimestampObj);
    }
}
TOP

Related Classes of org.hibernate.envers.strategy.ValidityAuditStrategy

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.