From 1abf044f2e2d7a8038af0d8c75db14eb2997bd58 Mon Sep 17 00:00:00 2001 From: Chris Cranford Date: Thu, 25 Nov 2021 22:09:22 -0500 Subject: [PATCH] HHH-10496 Allow RevisionTimestamp to be java.time.LocalDateTime --- .../org/hibernate/envers/AuditReader.java | 18 ++++ .../internal/RevisionInfoConfiguration.java | 79 +++++++++++----- .../RevisionDoesNotExistException.java | 16 ++++ .../internal/entities/PropertyData.java | 2 +- .../entities/RevisionTimestampData.java | 65 +++++++++++++ .../internal/reader/AuditReaderImpl.java | 21 +++++ .../DefaultRevisionInfoGenerator.java | 16 +--- ...ModifiedEntitiesRevisionInfoGenerator.java | 6 +- .../RevisionInfoQueryCreator.java | 30 ++++-- .../RevisionTimestampValueResolver.java | 77 ++++++++++++++++ .../CustomLocalDateTimeRevEntity.java | 80 ++++++++++++++++ .../reventity/LocalDateTimeTest.java | 92 +++++++++++++++++++ 12 files changed, 452 insertions(+), 50 deletions(-) create mode 100644 hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/RevisionTimestampData.java create mode 100644 hibernate-envers/src/main/java/org/hibernate/envers/internal/revisioninfo/RevisionTimestampValueResolver.java create mode 100644 hibernate-envers/src/test/java/org/hibernate/orm/test/envers/entities/reventity/CustomLocalDateTimeRevEntity.java create mode 100644 hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/reventity/LocalDateTimeTest.java diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/AuditReader.java b/hibernate-envers/src/main/java/org/hibernate/envers/AuditReader.java index 387cb2a5f0..be7024d955 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/AuditReader.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/AuditReader.java @@ -6,6 +6,7 @@ */ package org.hibernate.envers; +import java.time.LocalDateTime; import java.util.Date; import java.util.List; import java.util.Map; @@ -174,6 +175,23 @@ public interface AuditReader { Number getRevisionNumberForDate(Date date) throws IllegalStateException, RevisionDoesNotExistException, IllegalArgumentException; + /** + * Gets the revision number, that corresponds to the given date. More precisely, returns + * the number of the highest revision, which was created on or before the given date. So: + * getRevisionDate(getRevisionNumberForDate(date)) <= date and + * getRevisionDate(getRevisionNumberForDate(date)+1) > date. + * + * @param date Date for which to get the revision. + * + * @return Revision number corresponding to the given date. + * + * @throws IllegalStateException If the associated entity manager is closed. + * @throws RevisionDoesNotExistException If the given date is before the first revision. + * @throws IllegalArgumentException If date is null. + */ + Number getRevisionNumberForDate(LocalDateTime date) throws IllegalStateException, + RevisionDoesNotExistException, IllegalArgumentException; + /** * A helper method; should be used only if a custom revision entity is used. See also {@link RevisionEntity}. * 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 9ecf4f7558..b887ab9acb 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 @@ -6,6 +6,7 @@ */ package org.hibernate.envers.configuration.internal; +import java.time.LocalDateTime; import java.util.Date; import java.util.Locale; import java.util.Set; @@ -36,14 +37,17 @@ import org.hibernate.envers.enhanced.OrderedSequenceGenerator; import org.hibernate.envers.enhanced.SequenceIdRevisionEntity; import org.hibernate.envers.enhanced.SequenceIdTrackingModifiedEntitiesRevisionEntity; import org.hibernate.envers.internal.entities.PropertyData; +import org.hibernate.envers.internal.entities.RevisionTimestampData; import org.hibernate.envers.internal.revisioninfo.DefaultRevisionInfoGenerator; import org.hibernate.envers.internal.revisioninfo.DefaultTrackingModifiedEntitiesRevisionInfoGenerator; import org.hibernate.envers.internal.revisioninfo.ModifiedEntityNamesReader; import org.hibernate.envers.internal.revisioninfo.RevisionInfoGenerator; import org.hibernate.envers.internal.revisioninfo.RevisionInfoNumberReader; import org.hibernate.envers.internal.revisioninfo.RevisionInfoQueryCreator; +import org.hibernate.envers.internal.revisioninfo.RevisionTimestampValueResolver; import org.hibernate.mapping.PersistentClass; import org.hibernate.mapping.Property; +import org.hibernate.service.ServiceRegistry; /** * @author Adam Warski (adam at warski dot org) @@ -103,8 +107,7 @@ public class RevisionInfoConfiguration { revisionInfoQueryCreator = new RevisionInfoQueryCreator( resolver.revisionInfoEntityName, resolver.revisionInfoIdData.getName(), - resolver.revisionInfoTimestampData.getName(), - isTimestampAsDate( revisionInfoTimestampTypeName ) + resolver.timestampValueResolver ); if ( configuration.isTrackEntitiesChanged() ) { @@ -230,14 +233,27 @@ public class RevisionInfoConfiguration { return mapping; } - private boolean isTimestampAsDate(String typeName) { - return "date".equals( typeName ) || "time".equals( typeName ) || "timestamp".equals( typeName ); - } - private org.hibernate.envers.boot.model.Column createColumn(String name, String type) { return new org.hibernate.envers.boot.model.Column( name, null, null, null, type, null, null ); } + private RevisionTimestampValueResolver createRevisionTimestampResolver( + Class revisionInfoClass, + PropertyData revisionInfoTimestampData, + String typeName, + ServiceRegistry serviceRegistry) { + return new RevisionTimestampValueResolver( + revisionInfoClass, + new RevisionTimestampData( + revisionInfoTimestampData.getName(), + revisionInfoTimestampData.getBeanName(), + revisionInfoTimestampData.getAccessType(), + typeName + ), + serviceRegistry + ); + } + private class RevisionEntityResolver { private final MetadataImplementor metadata; @@ -258,6 +274,7 @@ public class RevisionInfoConfiguration { private String revisionInfoTimestampTypeName; private String revisionPropType; private String revisionPropSqlType; + private RevisionTimestampValueResolver timestampValueResolver; public RevisionEntityResolver(MetadataImplementor metadata, ReflectionManager reflectionManager) { this.metadata = metadata; @@ -328,7 +345,12 @@ public class RevisionInfoConfiguration { final Property timestampProperty = persistentClass.getProperty( revisionInfoTimestampData.getName() ); revisionInfoTimestampTypeName = timestampProperty.getType().getName(); - boolean timestampAsDate = isTimestampAsDate( revisionInfoTimestampTypeName ); + timestampValueResolver = createRevisionTimestampResolver( + revisionInfoClass, + revisionInfoTimestampData, + revisionInfoTimestampTypeName, + metadata.getMetadataBuildingOptions().getServiceRegistry() + ); if ( useEntityTrackingRevisionEntity( revisionInfoClass ) ) { // If tracking modified entities is enabled, custom revision info entity is a subtype @@ -337,8 +359,7 @@ public class RevisionInfoConfiguration { revisionInfoEntityName, revisionInfoClass, revisionListenerClass, - revisionInfoTimestampData, - timestampAsDate, + timestampValueResolver, modifiedEntityNamesData, metadata.getMetadataBuildingOptions().getServiceRegistry() ); @@ -349,8 +370,7 @@ public class RevisionInfoConfiguration { revisionInfoEntityName, revisionInfoClass, revisionListenerClass, - revisionInfoTimestampData, - timestampAsDate, + timestampValueResolver, metadata.getMetadataBuildingOptions().getServiceRegistry() ); } @@ -359,33 +379,42 @@ public class RevisionInfoConfiguration { if ( revisionInfoGenerator == null ) { revisionListenerClass = getRevisionListenerClass( RevisionListener.class ); - boolean timestampAsDate = isTimestampAsDate( revisionInfoTimestampTypeName ); if ( configuration.isTrackEntitiesChanged() ) { revisionInfoClass = configuration.isNativeIdEnabled() ? DefaultTrackingModifiedEntitiesRevisionEntity.class : SequenceIdTrackingModifiedEntitiesRevisionEntity.class; revisionInfoEntityName = revisionInfoClass.getName(); - revisionInfoGenerator = new DefaultTrackingModifiedEntitiesRevisionInfoGenerator( - revisionInfoEntityName, - revisionInfoClass, - revisionListenerClass, - revisionInfoTimestampData, - timestampAsDate, - modifiedEntityNamesData, - metadata.getMetadataBuildingOptions().getServiceRegistry() - ); } else { revisionInfoClass = configuration.isNativeIdEnabled() ? DefaultRevisionEntity.class : SequenceIdRevisionEntity.class; + } + + timestampValueResolver = createRevisionTimestampResolver( + revisionInfoClass, + revisionInfoTimestampData, + revisionInfoTimestampTypeName, + metadata.getMetadataBuildingOptions().getServiceRegistry() + ); + + if ( configuration.isTrackEntitiesChanged() ) { + revisionInfoGenerator = new DefaultTrackingModifiedEntitiesRevisionInfoGenerator( + revisionInfoEntityName, + revisionInfoClass, + revisionListenerClass, + timestampValueResolver, + modifiedEntityNamesData, + metadata.getMetadataBuildingOptions().getServiceRegistry() + ); + } + else { revisionInfoGenerator = new DefaultRevisionInfoGenerator( revisionInfoEntityName, revisionInfoClass, revisionListenerClass, - revisionInfoTimestampData, - timestampAsDate, + timestampValueResolver, metadata.getMetadataBuildingOptions().getServiceRegistry() ); } @@ -464,12 +493,12 @@ public class RevisionInfoConfiguration { } final XClass propertyType = property.getType(); - if ( isAnyType( propertyType, Long.class, Long.TYPE, Date.class, java.sql.Date.class ) ) { + if ( isAnyType( propertyType, Long.class, Long.TYPE, Date.class, LocalDateTime.class, java.sql.Date.class ) ) { revisionInfoTimestampData = createPropertyData( property, accessType ); revisionTimestampFound = true; } else { - throwUnexpectedAnnotatedType( property, RevisionTimestamp.class, "long, Long, Date, or java.sql.Date" ); + throwUnexpectedAnnotatedType( property, RevisionTimestamp.class, "long, Long, Date, LocalDateTime, or java.sql.Date" ); } } diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/exception/RevisionDoesNotExistException.java b/hibernate-envers/src/main/java/org/hibernate/envers/exception/RevisionDoesNotExistException.java index 1039aad7ff..c6a623a820 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/exception/RevisionDoesNotExistException.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/exception/RevisionDoesNotExistException.java @@ -6,27 +6,39 @@ */ package org.hibernate.envers.exception; +import java.time.LocalDateTime; import java.util.Date; /** * @author Adam Warski (adam at warski dot org) + * @author Chris Cranford */ public class RevisionDoesNotExistException extends AuditException { private static final long serialVersionUID = -6417768274074962282L; private final Number revision; private final Date date; + private final LocalDateTime localDateTime; public RevisionDoesNotExistException(Number revision) { super( "Revision " + revision + " does not exist." ); this.revision = revision; this.date = null; + this.localDateTime = null; } public RevisionDoesNotExistException(Date date) { super( "There is no revision before or at " + date + "." ); this.date = date; this.revision = null; + this.localDateTime = null; + } + + public RevisionDoesNotExistException(LocalDateTime localDateTime) { + super( "There is no revision before or at " + localDateTime + "." ); + this.localDateTime = localDateTime; + this.revision = null; + this.date = null; } public Number getRevision() { @@ -36,4 +48,8 @@ public class RevisionDoesNotExistException extends AuditException { public Date getDate() { return date; } + + public LocalDateTime getLocalDateTime() { + return localDateTime; + } } diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/PropertyData.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/PropertyData.java index a86ccadbb1..772ea2d6c2 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/PropertyData.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/PropertyData.java @@ -60,7 +60,7 @@ public class PropertyData { this.accessType = accessType; } - private PropertyData(String name, String beanName, String accessType, Type propertyType) { + public PropertyData(String name, String beanName, String accessType, Type propertyType) { this( name, beanName, accessType ); this.propertyType = propertyType; } diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/RevisionTimestampData.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/RevisionTimestampData.java new file mode 100644 index 0000000000..785e209e78 --- /dev/null +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/RevisionTimestampData.java @@ -0,0 +1,65 @@ +/* + * 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.internal.entities; + +import java.util.Objects; + +import org.hibernate.type.Type; + +/** + * @author Chris Cranford + * @author 6.0 + */ +public class RevisionTimestampData extends PropertyData { + + private final String typeName; + + public RevisionTimestampData(String name, String beanName, String accessType, String typeName) { + super( name, beanName, accessType ); + this.typeName = typeName; + } + + public RevisionTimestampData(RevisionTimestampData old, String typeName) { + this( old.getName(), old.getBeanName(), old.getAccessType(), typeName ); + } + + public String getTypeName() { + return typeName; + } + + public boolean isTimestampDate() { + return "date".equals( typeName ) + || "time".equals( typeName ) + || "timestamp".equals( typeName ); + } + + public boolean isTimestampLocalDateTime() { + return "LocalDateTime".equals( typeName ); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + ( typeName != null ? typeName.hashCode() : 0 ); + return result; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + RevisionTimestampData that = (RevisionTimestampData) o; + return Objects.equals( typeName, that.typeName ); + } +} diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/reader/AuditReaderImpl.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/reader/AuditReaderImpl.java index d089c639e9..8e7fc2a3a5 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/internal/reader/AuditReaderImpl.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/reader/AuditReaderImpl.java @@ -6,6 +6,7 @@ */ package org.hibernate.envers.internal.reader; +import java.time.LocalDateTime; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -204,6 +205,26 @@ public class AuditReaderImpl implements AuditReaderImplementor { } } + @Override + public Number getRevisionNumberForDate(LocalDateTime date) { + checkNotNull( date, "Date of revision" ); + checkSession(); + + final Query query = enversService.getRevisionInfoQueryCreator().getRevisionNumberForDateQuery( session, date ); + + try { + final Number res = (Number) query.uniqueResult(); + if ( res == null ) { + throw new RevisionDoesNotExistException( date ); + } + + return res; + } + catch (NonUniqueResultException e) { + throw new AuditException( e ); + } + } + @Override @SuppressWarnings({"unchecked"}) public T findRevision(Class revisionEntityClass, Number revision) 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 a21bfb438a..b01753b5b9 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 @@ -7,18 +7,14 @@ package org.hibernate.envers.internal.revisioninfo; import java.lang.reflect.Constructor; -import java.util.Date; 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; import org.hibernate.internal.util.ReflectHelper; -import org.hibernate.property.access.spi.Setter; import org.hibernate.resource.beans.spi.ManagedBean; import org.hibernate.resource.beans.spi.ManagedBeanRegistry; import org.hibernate.service.ServiceRegistry; @@ -31,10 +27,9 @@ import org.hibernate.service.ServiceRegistry; public class DefaultRevisionInfoGenerator implements RevisionInfoGenerator { private final String revisionInfoEntityName; private final ManagedBean listenerManagedBean; - private final Setter revisionTimestampSetter; - private final boolean timestampAsDate; private final Constructor revisionInfoClassConstructor; private final SessionCacheCleaner sessionCacheCleaner; + private final RevisionTimestampValueResolver timestampValueResolver; private RevisionInfoNumberReader revisionInfoNumberReader; @@ -42,14 +37,12 @@ public class DefaultRevisionInfoGenerator implements RevisionInfoGenerator { String revisionInfoEntityName, Class revisionInfoClass, Class listenerClass, - PropertyData revisionInfoTimestampData, - boolean timestampAsDate, + RevisionTimestampValueResolver timestampValueResolver, ServiceRegistry serviceRegistry) { this.revisionInfoEntityName = revisionInfoEntityName; - this.timestampAsDate = timestampAsDate; + this.timestampValueResolver = timestampValueResolver; this.revisionInfoClassConstructor = ReflectHelper.getDefaultConstructor( revisionInfoClass ); - this.revisionTimestampSetter = ReflectionTools.getSetter( revisionInfoClass, revisionInfoTimestampData, serviceRegistry ); this.listenerManagedBean = resolveRevisionListenerBean( listenerClass, serviceRegistry ); @@ -80,8 +73,7 @@ public class DefaultRevisionInfoGenerator implements RevisionInfoGenerator { throw new RuntimeException( e ); } - final long timestamp = System.currentTimeMillis(); - revisionTimestampSetter.set( revisionInfo, timestampAsDate ? new Date( timestamp ) : timestamp, null ); + timestampValueResolver.resolveNow( revisionInfo ); if ( listenerManagedBean != null ) { listenerManagedBean.getBeanInstance().newRevision( revisionInfo ); diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/revisioninfo/DefaultTrackingModifiedEntitiesRevisionInfoGenerator.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/revisioninfo/DefaultTrackingModifiedEntitiesRevisionInfoGenerator.java index 069ad9ff36..4c284fc5b5 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/internal/revisioninfo/DefaultTrackingModifiedEntitiesRevisionInfoGenerator.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/revisioninfo/DefaultTrackingModifiedEntitiesRevisionInfoGenerator.java @@ -21,6 +21,7 @@ import org.hibernate.service.ServiceRegistry; * Automatically adds entity names, that have been changed during current revision, to revision entity. * * @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com) + * @author Chris Cranford * * @see org.hibernate.envers.ModifiedEntityNames * @see org.hibernate.envers.DefaultTrackingModifiedEntitiesRevisionEntity @@ -33,11 +34,10 @@ public class DefaultTrackingModifiedEntitiesRevisionInfoGenerator extends Defaul String revisionInfoEntityName, Class revisionInfoClass, Class listenerClass, - PropertyData revisionInfoTimestampData, - boolean timestampAsDate, + RevisionTimestampValueResolver timestampValueResolver, PropertyData modifiedEntityNamesData, ServiceRegistry serviceRegistry) { - super( revisionInfoEntityName, revisionInfoClass, listenerClass, revisionInfoTimestampData, timestampAsDate, serviceRegistry ); + super( revisionInfoEntityName, revisionInfoClass, listenerClass, timestampValueResolver, serviceRegistry ); modifiedEntityNamesSetter = ReflectionTools.getSetter( revisionInfoClass, modifiedEntityNamesData, serviceRegistry ); modifiedEntityNamesGetter = ReflectionTools.getGetter( revisionInfoClass, modifiedEntityNamesData, serviceRegistry ); } diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/revisioninfo/RevisionInfoQueryCreator.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/revisioninfo/RevisionInfoQueryCreator.java index 3a4d1d04f7..b0de04808a 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/internal/revisioninfo/RevisionInfoQueryCreator.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/revisioninfo/RevisionInfoQueryCreator.java @@ -6,6 +6,7 @@ */ package org.hibernate.envers.internal.revisioninfo; +import java.time.LocalDateTime; import java.util.Date; import java.util.Locale; import java.util.Set; @@ -28,16 +29,15 @@ public class RevisionInfoQueryCreator { private final String revisionInfoEntityName; private final String revisionInfoIdName; - private final String revisionInfoTimestampName; - private final boolean timestampAsDate; + private final RevisionTimestampValueResolver timestampValueResolver; public RevisionInfoQueryCreator( - String revisionInfoEntityName, String revisionInfoIdName, - String revisionInfoTimestampName, boolean timestampAsDate) { + String revisionInfoEntityName, + String revisionInfoIdName, + RevisionTimestampValueResolver timestampValueResolver) { this.revisionInfoEntityName = revisionInfoEntityName; this.revisionInfoIdName = revisionInfoIdName; - this.revisionInfoTimestampName = revisionInfoTimestampName; - this.timestampAsDate = timestampAsDate; + this.timestampValueResolver = timestampValueResolver; } public Query getRevisionDateQuery(Session session, Number revision) { @@ -45,7 +45,7 @@ public class RevisionInfoQueryCreator { String.format( Locale.ENGLISH, REVISION_DATE_QUERY, - revisionInfoTimestampName, + timestampValueResolver.getName(), revisionInfoEntityName, revisionInfoIdName ) @@ -59,9 +59,21 @@ public class RevisionInfoQueryCreator { REVISION_NUMBER_FOR_DATE_QUERY, revisionInfoIdName, revisionInfoEntityName, - revisionInfoTimestampName + timestampValueResolver.getName() ) - ).setParameter( REVISION_NUMBER_FOR_DATE_QUERY_PARAMETER, timestampAsDate ? date : date.getTime() ); + ).setParameter( REVISION_NUMBER_FOR_DATE_QUERY_PARAMETER, timestampValueResolver.resolveByValue( date ) ); + } + + public Query getRevisionNumberForDateQuery(Session session, LocalDateTime localDateTime) { + return session.createQuery( + String.format( + Locale.ENGLISH, + REVISION_NUMBER_FOR_DATE_QUERY, + revisionInfoIdName, + revisionInfoEntityName, + timestampValueResolver.getName() + ) + ).setParameter( REVISION_NUMBER_FOR_DATE_QUERY_PARAMETER, timestampValueResolver.resolveByValue( localDateTime ) ); } public Query getRevisionsQuery(Session session, Set revisions) { diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/revisioninfo/RevisionTimestampValueResolver.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/revisioninfo/RevisionTimestampValueResolver.java new file mode 100644 index 0000000000..bad7608fa9 --- /dev/null +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/revisioninfo/RevisionTimestampValueResolver.java @@ -0,0 +1,77 @@ +/* + * 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.internal.revisioninfo; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Date; + +import org.hibernate.envers.internal.entities.RevisionTimestampData; +import org.hibernate.envers.internal.tools.ReflectionTools; +import org.hibernate.property.access.spi.Setter; +import org.hibernate.service.ServiceRegistry; + +/** + * @author Chris Cranford + * @since 6.0 + */ +public class RevisionTimestampValueResolver { + + private final RevisionTimestampData timestampData; + private final Setter revisionTimestampSetter; + + public RevisionTimestampValueResolver(Class revisionInfoClass, RevisionTimestampData timestampData, ServiceRegistry serviceRegistry) { + this.timestampData = timestampData; + this.revisionTimestampSetter = ReflectionTools.getSetter( revisionInfoClass, timestampData, serviceRegistry ); + } + + public String getName() { + return timestampData.getName(); + } + + public void resolveNow(Object object) { + if ( timestampData.isTimestampDate() ) { + revisionTimestampSetter.set( object, new Date(), null ); + } + else if ( timestampData.isTimestampLocalDateTime() ) { + revisionTimestampSetter.set(object, LocalDateTime.now(), null ); + } + else { + revisionTimestampSetter.set( object, System.currentTimeMillis(), null ); + } + } + + public Object resolveByValue(Date date) { + if ( date != null ) { + if ( timestampData.isTimestampDate() ) { + return date; + } + else if ( timestampData.isTimestampLocalDateTime() ) { + return LocalDateTime.ofInstant( date.toInstant(), ZoneId.systemDefault() ); + } + else { + return date.getTime(); + } + } + return null; + } + + public Object resolveByValue(LocalDateTime localDateTime) { + if ( localDateTime != null ) { + if ( timestampData.isTimestampDate() ) { + return Date.from( localDateTime.atZone( ZoneId.systemDefault() ).toInstant() ); + } + else if ( timestampData.isTimestampLocalDateTime() ) { + return localDateTime; + } + else { + return localDateTime.atZone( ZoneId.systemDefault() ).toInstant().getEpochSecond(); + } + } + return null; + } +} diff --git a/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/entities/reventity/CustomLocalDateTimeRevEntity.java b/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/entities/reventity/CustomLocalDateTimeRevEntity.java new file mode 100644 index 0000000000..c3a9597317 --- /dev/null +++ b/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/entities/reventity/CustomLocalDateTimeRevEntity.java @@ -0,0 +1,80 @@ +/* + * 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.entities.reventity; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Objects; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.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; + +/** + * @author Chris Cranford + */ +@Entity +@GenericGenerator(name = "EnversTestingRevisionGenerator", + strategy = "org.hibernate.id.enhanced.TableGenerator", + parameters = { + @Parameter(name = "table_name", value = "REVISION_GENERATOR"), + @Parameter(name = "initial_value", value = "1"), + @Parameter(name = "increment_size", value = "1"), + @Parameter(name = "prefer_entity_table_as_segment_value", value = "true") + } +) +@RevisionEntity +public class CustomLocalDateTimeRevEntity { + @Id + @GeneratedValue(generator = "EnversTestingRevisionGenerator") + @RevisionNumber + private int id; + + @RevisionTimestamp + private LocalDateTime localDateTimestamp; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public LocalDateTime getLocalDateTimestamp() { + return localDateTimestamp; + } + + public void setLocalDateTimestamp(LocalDateTime localDateTimestamp) { + this.localDateTimestamp = localDateTimestamp; + } + + @Override + public int hashCode() { + int result = id; + result = 31 * result + ( localDateTimestamp != null ? localDateTimestamp.hashCode() : 0 ); + return result; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CustomLocalDateTimeRevEntity that = (CustomLocalDateTimeRevEntity) o; + return id == that.id && Objects.equals(localDateTimestamp, that.localDateTimestamp); + } +} diff --git a/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/reventity/LocalDateTimeTest.java b/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/reventity/LocalDateTimeTest.java new file mode 100644 index 0000000000..ab1c46d5f3 --- /dev/null +++ b/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/reventity/LocalDateTimeTest.java @@ -0,0 +1,92 @@ +/* + * 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.orm.test.envers.integration.reventity; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Date; + +import jakarta.persistence.EntityManager; + +import org.hibernate.envers.test.entities.reventity.CustomLocalDateTimeRevEntity; +import org.hibernate.testing.TestForIssue; + +import org.hibernate.orm.test.envers.BaseEnversJPAFunctionalTestCase; +import org.hibernate.orm.test.envers.Priority; +import org.hibernate.orm.test.envers.entities.StrTestEntity; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + + +/** + * @author Chris Cranford + */ +@TestForIssue( jiraKey = "HHH-10496" ) +public class LocalDateTimeTest extends BaseEnversJPAFunctionalTestCase { + private Instant timestampStart; + private Instant timestampEnd; + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { + StrTestEntity.class, + CustomLocalDateTimeRevEntity.class + }; + } + + @Test + @Priority(10) + public void initData() { + EntityManager em = getEntityManager(); + try { + timestampStart = Instant.now(); + + // some DBMs truncate time to seconds. + Thread.sleep( 1100 ); + + StrTestEntity entity = new StrTestEntity( "x" ); + + // Revision 1 + em.getTransaction().begin(); + em.persist( entity ); + em.getTransaction().commit(); + + timestampEnd = Instant.now(); + } + catch( InterruptedException x ) { + fail( "Unexpected interrupted exception" ); + } + finally { + em.close(); + } + } + + @Test + public void testTimestampsUsingDate() { + // expect just one revision prior to this timestamp. + assertEquals( 1, getAuditReader().getRevisionNumberForDate( Date.from( timestampEnd ) ) ); + } + + @Test + public void testRevisionEntityLocalDateTime() { + // get revision + CustomLocalDateTimeRevEntity revInfo = getAuditReader().findRevision( CustomLocalDateTimeRevEntity.class, 1 ); + assertNotNull( revInfo ); + // verify started before revision timestamp + final LocalDateTime started = LocalDateTime.ofInstant( timestampStart, ZoneId.systemDefault() ); + assertTrue( started.isBefore( revInfo.getLocalDateTimestamp() ) ); + // verify ended after revision timestamp + final LocalDateTime ended = LocalDateTime.ofInstant( timestampEnd, ZoneId.systemDefault() ); + assertTrue( ended.isAfter( revInfo.getLocalDateTimestamp() ) ); + } + +}