HHH-6615 Throw AuditException when generated revision number is negative.
This commit is contained in:
parent
8c52eb2eae
commit
f4abc09854
|
@ -422,6 +422,14 @@ public class RevisionInfoConfiguration {
|
||||||
revisionInfoXmlMapping = generateDefaultRevisionInfoXmlMapping();
|
revisionInfoXmlMapping = generateDefaultRevisionInfoXmlMapping();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final RevisionInfoNumberReader revisionInfoNumberReader = new RevisionInfoNumberReader(
|
||||||
|
revisionInfoClass,
|
||||||
|
revisionInfoIdData,
|
||||||
|
metadata.getMetadataBuildingOptions().getServiceRegistry()
|
||||||
|
);
|
||||||
|
|
||||||
|
revisionInfoGenerator.setRevisionInfoNumberReader( revisionInfoNumberReader );
|
||||||
|
|
||||||
return new RevisionInfoConfigurationResult(
|
return new RevisionInfoConfigurationResult(
|
||||||
revisionInfoGenerator, revisionInfoXmlMapping,
|
revisionInfoGenerator, revisionInfoXmlMapping,
|
||||||
new RevisionInfoQueryCreator(
|
new RevisionInfoQueryCreator(
|
||||||
|
@ -429,7 +437,7 @@ public class RevisionInfoConfiguration {
|
||||||
revisionInfoTimestampData.getName(), isTimestampAsDate()
|
revisionInfoTimestampData.getName(), isTimestampAsDate()
|
||||||
),
|
),
|
||||||
generateRevisionInfoRelationMapping(),
|
generateRevisionInfoRelationMapping(),
|
||||||
new RevisionInfoNumberReader( revisionInfoClass, revisionInfoIdData, metadata.getMetadataBuildingOptions().getServiceRegistry() ),
|
revisionInfoNumberReader,
|
||||||
globalCfg.isTrackEntitiesChangedInRevision()
|
globalCfg.isTrackEntitiesChangedInRevision()
|
||||||
? new ModifiedEntityNamesReader( revisionInfoClass, modifiedEntityNamesData, metadata.getMetadataBuildingOptions().getServiceRegistry() )
|
? new ModifiedEntityNamesReader( revisionInfoClass, modifiedEntityNamesData, metadata.getMetadataBuildingOptions().getServiceRegistry() )
|
||||||
: null,
|
: null,
|
||||||
|
|
|
@ -14,6 +14,7 @@ import org.hibernate.Session;
|
||||||
import org.hibernate.envers.EntityTrackingRevisionListener;
|
import org.hibernate.envers.EntityTrackingRevisionListener;
|
||||||
import org.hibernate.envers.RevisionListener;
|
import org.hibernate.envers.RevisionListener;
|
||||||
import org.hibernate.envers.RevisionType;
|
import org.hibernate.envers.RevisionType;
|
||||||
|
import org.hibernate.envers.exception.AuditException;
|
||||||
import org.hibernate.envers.internal.entities.PropertyData;
|
import org.hibernate.envers.internal.entities.PropertyData;
|
||||||
import org.hibernate.envers.internal.synchronization.SessionCacheCleaner;
|
import org.hibernate.envers.internal.synchronization.SessionCacheCleaner;
|
||||||
import org.hibernate.envers.internal.tools.ReflectionTools;
|
import org.hibernate.envers.internal.tools.ReflectionTools;
|
||||||
|
@ -36,6 +37,8 @@ public class DefaultRevisionInfoGenerator implements RevisionInfoGenerator {
|
||||||
private final Constructor<?> revisionInfoClassConstructor;
|
private final Constructor<?> revisionInfoClassConstructor;
|
||||||
private final SessionCacheCleaner sessionCacheCleaner;
|
private final SessionCacheCleaner sessionCacheCleaner;
|
||||||
|
|
||||||
|
private RevisionInfoNumberReader revisionInfoNumberReader;
|
||||||
|
|
||||||
public DefaultRevisionInfoGenerator(
|
public DefaultRevisionInfoGenerator(
|
||||||
String revisionInfoEntityName,
|
String revisionInfoEntityName,
|
||||||
Class<?> revisionInfoClass,
|
Class<?> revisionInfoClass,
|
||||||
|
@ -54,9 +57,19 @@ public class DefaultRevisionInfoGenerator implements RevisionInfoGenerator {
|
||||||
this.sessionCacheCleaner = new SessionCacheCleaner();
|
this.sessionCacheCleaner = new SessionCacheCleaner();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setRevisionInfoNumberReader(RevisionInfoNumberReader revisionInfoNumberReader) {
|
||||||
|
this.revisionInfoNumberReader = revisionInfoNumberReader;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void saveRevisionData(Session session, Object revisionData) {
|
public void saveRevisionData(Session session, Object revisionData) {
|
||||||
session.save( revisionInfoEntityName, 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 );
|
sessionCacheCleaner.scheduleAuditDataRemoval( session, revisionData );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,11 @@ import org.hibernate.envers.RevisionType;
|
||||||
* @author Adam Warski (adam at warski dot org)
|
* @author Adam Warski (adam at warski dot org)
|
||||||
*/
|
*/
|
||||||
public interface RevisionInfoGenerator {
|
public interface RevisionInfoGenerator {
|
||||||
|
/**
|
||||||
|
* Set the revision entity number reader instance.
|
||||||
|
*/
|
||||||
|
void setRevisionInfoNumberReader(RevisionInfoNumberReader revisionInfoNumberReader);
|
||||||
|
|
||||||
void saveRevisionData(Session session, Object revisionData);
|
void saveRevisionData(Session session, Object revisionData);
|
||||||
|
|
||||||
Object generate();
|
Object generate();
|
||||||
|
|
|
@ -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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue