diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/RevisionInfoConfiguration.java b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/RevisionInfoConfiguration.java index e064cc46a9..e8b089ec29 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/RevisionInfoConfiguration.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/RevisionInfoConfiguration.java @@ -422,6 +422,14 @@ public class RevisionInfoConfiguration { revisionInfoXmlMapping = generateDefaultRevisionInfoXmlMapping(); } + final RevisionInfoNumberReader revisionInfoNumberReader = new RevisionInfoNumberReader( + revisionInfoClass, + revisionInfoIdData, + metadata.getMetadataBuildingOptions().getServiceRegistry() + ); + + revisionInfoGenerator.setRevisionInfoNumberReader( revisionInfoNumberReader ); + return new RevisionInfoConfigurationResult( revisionInfoGenerator, revisionInfoXmlMapping, new RevisionInfoQueryCreator( @@ -429,7 +437,7 @@ public class RevisionInfoConfiguration { revisionInfoTimestampData.getName(), isTimestampAsDate() ), generateRevisionInfoRelationMapping(), - new RevisionInfoNumberReader( revisionInfoClass, revisionInfoIdData, metadata.getMetadataBuildingOptions().getServiceRegistry() ), + revisionInfoNumberReader, globalCfg.isTrackEntitiesChangedInRevision() ? new ModifiedEntityNamesReader( revisionInfoClass, modifiedEntityNamesData, metadata.getMetadataBuildingOptions().getServiceRegistry() ) : null, diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/revisioninfo/DefaultRevisionInfoGenerator.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/revisioninfo/DefaultRevisionInfoGenerator.java index 863deb704a..36a04bc9a8 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/internal/revisioninfo/DefaultRevisionInfoGenerator.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/revisioninfo/DefaultRevisionInfoGenerator.java @@ -14,6 +14,7 @@ import org.hibernate.Session; import org.hibernate.envers.EntityTrackingRevisionListener; import org.hibernate.envers.RevisionListener; import org.hibernate.envers.RevisionType; +import org.hibernate.envers.exception.AuditException; import org.hibernate.envers.internal.entities.PropertyData; import org.hibernate.envers.internal.synchronization.SessionCacheCleaner; import org.hibernate.envers.internal.tools.ReflectionTools; @@ -36,6 +37,8 @@ public class DefaultRevisionInfoGenerator implements RevisionInfoGenerator { private final Constructor revisionInfoClassConstructor; private final SessionCacheCleaner sessionCacheCleaner; + private RevisionInfoNumberReader revisionInfoNumberReader; + public DefaultRevisionInfoGenerator( String revisionInfoEntityName, Class revisionInfoClass, @@ -54,9 +57,19 @@ public class DefaultRevisionInfoGenerator implements RevisionInfoGenerator { this.sessionCacheCleaner = new SessionCacheCleaner(); } + @Override + public void setRevisionInfoNumberReader(RevisionInfoNumberReader revisionInfoNumberReader) { + this.revisionInfoNumberReader = revisionInfoNumberReader; + } + @Override public void saveRevisionData(Session session, Object revisionData) { session.save( revisionInfoEntityName, revisionData ); + if ( revisionInfoNumberReader != null ) { + if ( revisionInfoNumberReader.getRevisionNumber( revisionData ).longValue() < 0 ) { + throw new AuditException( "Negative revision numbers are not allowed" ); + } + } sessionCacheCleaner.scheduleAuditDataRemoval( session, revisionData ); } diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/revisioninfo/RevisionInfoGenerator.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/revisioninfo/RevisionInfoGenerator.java index 036d2b05cb..e7e52e20e1 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/internal/revisioninfo/RevisionInfoGenerator.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/revisioninfo/RevisionInfoGenerator.java @@ -15,6 +15,11 @@ import org.hibernate.envers.RevisionType; * @author Adam Warski (adam at warski dot org) */ public interface RevisionInfoGenerator { + /** + * Set the revision entity number reader instance. + */ + void setRevisionInfoNumberReader(RevisionInfoNumberReader revisionInfoNumberReader); + void saveRevisionData(Session session, Object revisionData); Object generate(); diff --git a/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/reventity/RevisionNumberOverflowTest.java b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/reventity/RevisionNumberOverflowTest.java new file mode 100644 index 0000000000..462afd6afd --- /dev/null +++ b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/reventity/RevisionNumberOverflowTest.java @@ -0,0 +1,154 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.envers.test.integration.reventity; + +import java.util.List; +import java.util.Objects; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; + +import org.hibernate.annotations.GenericGenerator; +import org.hibernate.annotations.Parameter; +import org.hibernate.envers.RevisionEntity; +import org.hibernate.envers.RevisionNumber; +import org.hibernate.envers.RevisionTimestamp; +import org.hibernate.envers.exception.AuditException; +import org.hibernate.envers.test.BaseEnversJPAFunctionalTestCase; +import org.hibernate.envers.test.Priority; +import org.hibernate.envers.test.entities.StrTestEntity; +import org.hibernate.id.enhanced.TableGenerator; +import org.junit.Test; + +import org.hibernate.testing.TestForIssue; + +import static org.hibernate.testing.junit4.ExtraAssertions.assertTyping; +import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; +import static org.junit.Assert.assertEquals; + +/** + * This test checks that when revision number overflow occurs an {@link AuditException} is thrown. + * + * In order to test this use case, the {@code REVISION_GENERATOR} is explicitly initialized at + * {@link Integer.MAX_VALUE} and we attempt to persist two entities that are audited. The + * expectation is that the test should persist the first entity but the second should throw the + * desired exception. + * + * Revision numbers should always be positive values and always increasing, this is due to the + * nature of how the {@link org.hibernate.envers.AuditReader} builds audit queries. + * + * @author Chris Cranford + */ +@TestForIssue(jiraKey = "HHH-6615") +public class RevisionNumberOverflowTest extends BaseEnversJPAFunctionalTestCase { + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { StrTestEntity.class, CustomCappedRevEntity.class }; + } + + @Priority(10) + @Test + public void initData() { + // Save entity with maximum possible revision number + doInJPA( this::entityManagerFactory, entityManager -> { + final StrTestEntity entity = new StrTestEntity( "test1" ); + entityManager.persist( entity ); + } ); + + // Save entity with overflow revision number + try { + doInJPA( this::entityManagerFactory, entityManager -> { + final StrTestEntity entity = new StrTestEntity( "test2" ); + entityManager.persist( entity ); + } ); + } catch ( Exception e ) { + assertRootCause( e, AuditException.class, "Negative revision numbers are not allowed" ); + } + } + + @Test + public void testRevisionExpectations() { + final StrTestEntity expected = new StrTestEntity( "test1", 1 ); + + // Verify there was only one entity instance saved + List results = getAuditReader().createQuery().forRevisionsOfEntity( StrTestEntity.class, true, true ).getResultList(); + assertEquals( 1, results.size() ); + assertEquals( expected, results.get( 0 ) ); + + // Verify entity instance saved has revision Integer.MAX_VALUE + assertEquals( expected, getAuditReader().find( StrTestEntity.class, 1, Integer.MAX_VALUE ) ); + } + + private static void assertRootCause(Exception exception, Class type, String message) { + Throwable root = exception; + while ( root.getCause() != null ) { + root = root.getCause(); + } + assertTyping( type, root ); + assertEquals( root.getMessage(), message ); + } + + // We create a custom revision entity here with an explicit configuration for the revision + // number generation that is explicitly initialized at Integer.MAX_VALUE. This allows the + // test to attempt to persist two entities where the first will not trigger a revision + // number overflow; however the second attempt to persist an entity will. + + @Entity(name = "CustomCappedRevEntity") + @GenericGenerator(name = "EnversCappedRevisionNumberGenerator", + strategy = "org.hibernate.id.enhanced.TableGenerator", + parameters = { + @Parameter(name = TableGenerator.TABLE_PARAM, value = "REVISION_GENERATOR"), + @Parameter(name = TableGenerator.INITIAL_PARAM, value = "2147483647"), + @Parameter(name = TableGenerator.INCREMENT_PARAM, value = "1"), + @Parameter(name = TableGenerator.CONFIG_PREFER_SEGMENT_PER_ENTITY, value = "true") + }) + @RevisionEntity + public static class CustomCappedRevEntity { + @Id + @GeneratedValue(generator = "EnversCappedRevisionNumberGenerator") + @RevisionNumber + private int rev; + + @RevisionTimestamp + private long timestamp; + + public int getRev() { + return rev; + } + + public void setRev(int rev) { + this.rev = rev; + } + + public long getTimestamp() { + return timestamp; + } + + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + CustomCappedRevEntity that = (CustomCappedRevEntity) o; + return rev == that.rev && + timestamp == that.timestamp; + } + + @Override + public int hashCode() { + return Objects.hash( rev, timestamp ); + } + } +}