HHH-6615 Throw AuditException when generated revision number is negative.

This commit is contained in:
Chris Cranford 2019-12-06 12:24:18 -05:00 committed by Andrea Boriero
parent 8c52eb2eae
commit f4abc09854
4 changed files with 181 additions and 1 deletions

View File

@ -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,

View File

@ -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 );
}

View File

@ -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();

View File

@ -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 <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
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 );
}
}
}