diff --git a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/config/Entity.groovy b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/config/Entity.groovy index e80dc417d1e..37bda1acd32 100644 --- a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/config/Entity.groovy +++ b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/config/Entity.groovy @@ -56,6 +56,7 @@ class Entity

{ * @return Whether automatic time stamps should be applied to 'lastUpdate' and 'dateCreated' properties */ boolean autoTimestamp = true + /** * @return Whether the entity should be autowired */ diff --git a/grails-datastore-gorm-test/src/test/groovy/org/grails/datastore/gorm/CustomAutoTimestampSpec.groovy b/grails-datastore-gorm-test/src/test/groovy/org/grails/datastore/gorm/CustomAutoTimestampSpec.groovy new file mode 100644 index 00000000000..ce6f8f6e28d --- /dev/null +++ b/grails-datastore-gorm-test/src/test/groovy/org/grails/datastore/gorm/CustomAutoTimestampSpec.groovy @@ -0,0 +1,45 @@ +package org.grails.datastore.gorm + +import grails.gorm.annotation.AutoTimestamp +import static grails.gorm.annotation.AutoTimestamp.EventType.*; +import grails.gorm.tests.GormDatastoreSpec +import grails.persistence.Entity + +class CustomAutoTimestampSpec extends GormDatastoreSpec { + + void "Test when the auto timestamp properties are customized, they are correctly set"() { + when:"An entity is persisted" + def r = new RecordCustom(name: "Test") + r.save(flush:true, failOnError:true) + session.clear() + r = RecordCustom.get(r.id) + + then:"the custom lastUpdated and dateCreated are set" + r.modified != null && r.modified < new Date() + r.created != null && r.created < new Date() + + when:"An entity is modified" + Date previousCreated = r.created + Date previousModified = r.modified + r.name = "Test 2" + r.save(flush:true) + session.clear() + r = RecordCustom.get(r.id) + + then:"the custom lastUpdated property is updated and dateCreated is not" + r.modified != null && previousModified < r.modified + previousCreated == r.created + } + @Override + List getDomainClasses() { + [RecordCustom] + } +} + +@Entity +class RecordCustom { + Long id + String name + @AutoTimestamp(CREATED) Date created + @AutoTimestamp Date modified +} diff --git a/grails-datastore-gorm/src/main/groovy/grails/gorm/annotation/AutoTimestamp.java b/grails-datastore-gorm/src/main/groovy/grails/gorm/annotation/AutoTimestamp.java new file mode 100644 index 00000000000..5b2463adf4b --- /dev/null +++ b/grails-datastore-gorm/src/main/groovy/grails/gorm/annotation/AutoTimestamp.java @@ -0,0 +1,46 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package grails.gorm.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A property annotation used to apply auto-timestamping on a field + * upon gorm insert and update events + * + * @author Scott Murphy Heiberg + * @since 7.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD}) +public @interface AutoTimestamp { + + /** + * Enum to specify when auto-timestamping should occur. + */ + enum EventType { + CREATED, + UPDATED + } + + /** + * When to apply auto-timestamping + */ + EventType value() default EventType.UPDATED; +} diff --git a/grails-datastore-gorm/src/main/groovy/org/grails/datastore/gorm/events/AutoTimestampEventListener.java b/grails-datastore-gorm/src/main/groovy/org/grails/datastore/gorm/events/AutoTimestampEventListener.java index fddff3a7876..d48e7be4d19 100644 --- a/grails-datastore-gorm/src/main/groovy/org/grails/datastore/gorm/events/AutoTimestampEventListener.java +++ b/grails-datastore-gorm/src/main/groovy/org/grails/datastore/gorm/events/AutoTimestampEventListener.java @@ -14,10 +14,12 @@ */ package org.grails.datastore.gorm.events; +import java.lang.reflect.Field; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; +import grails.gorm.annotation.AutoTimestamp; import org.grails.datastore.gorm.timestamp.DefaultTimestampProvider; import org.grails.datastore.gorm.timestamp.TimestampProvider; import org.grails.datastore.mapping.config.Entity; @@ -45,13 +47,11 @@ public class AutoTimestampEventListener extends AbstractPersistenceEventListener public static final String DATE_CREATED_PROPERTY = "dateCreated"; public static final String LAST_UPDATED_PROPERTY = "lastUpdated"; - protected Map entitiesWithDateCreated = new ConcurrentHashMap(); - protected Map entitiesWithLastUpdated = new ConcurrentHashMap(); - protected Collection uninitializedEntities = new ConcurrentLinkedQueue(); - - + protected Map> entitiesWithDateCreated = new ConcurrentHashMap<>(); + protected Map> entitiesWithLastUpdated = new ConcurrentHashMap<>(); + protected Collection uninitializedEntities = new ConcurrentLinkedQueue<>(); + private TimestampProvider timestampProvider = new DefaultTimestampProvider(); - public AutoTimestampEventListener(final Datastore datastore) { super(datastore); @@ -80,8 +80,7 @@ protected void onPersistenceEvent(final AbstractPersistenceEvent event) { if (event.getEventType() == EventType.PreInsert) { beforeInsert(event.getEntity(), event.getEntityAccess()); - } - else if (event.getEventType() == EventType.PreUpdate) { + } else if (event.getEventType() == EventType.PreUpdate) { beforeUpdate(event.getEntity(), event.getEntityAccess()); } } @@ -96,82 +95,100 @@ public boolean beforeInsert(PersistentEntity entity, EntityAccess ea) { initializeIfNecessary(entity, name); Class dateCreatedType = null; Object timestamp = null; - if (hasDateCreated(name)) { - dateCreatedType = ea.getPropertyType(DATE_CREATED_PROPERTY); - timestamp = timestampProvider.createTimestamp(dateCreatedType); - ea.setProperty(DATE_CREATED_PROPERTY, timestamp); + Set props = getDateCreatedPropertyNames(name); + if (props != null) { + for (String prop : props) { + dateCreatedType = ea.getPropertyType(prop); + timestamp = timestampProvider.createTimestamp(dateCreatedType); + ea.setProperty(prop, timestamp); + } } - if (hasLastUpdated(name)) { - Class lastUpdateType = ea.getPropertyType(LAST_UPDATED_PROPERTY); - if(dateCreatedType == null || !lastUpdateType.isAssignableFrom(dateCreatedType)) { - timestamp = timestampProvider.createTimestamp(lastUpdateType); + props = getLastUpdatedPropertyNames(name); + if (props != null) { + for (String prop : props) { + Class lastUpdateType = ea.getPropertyType(prop); + if (dateCreatedType == null || !lastUpdateType.isAssignableFrom(dateCreatedType)) { + timestamp = timestampProvider.createTimestamp(lastUpdateType); + } + ea.setProperty(prop, timestamp); } - ea.setProperty(LAST_UPDATED_PROPERTY, timestamp); } return true; } private void initializeIfNecessary(PersistentEntity entity, String name) { - if(uninitializedEntities.contains(name)) { + if (uninitializedEntities.contains(name)) { storeDateCreatedAndLastUpdatedInfo(entity); uninitializedEntities.remove(name); } } public boolean beforeUpdate(PersistentEntity entity, EntityAccess ea) { - if (hasLastUpdated(entity.getName())) { - Class lastUpdateType = ea.getPropertyType(LAST_UPDATED_PROPERTY); - Object timestamp = timestampProvider.createTimestamp(lastUpdateType); - ea.setProperty(LAST_UPDATED_PROPERTY, timestamp); + Set props = getLastUpdatedPropertyNames(entity.getName()); + if (props != null) { + for (String prop : props) { + Class lastUpdateType = ea.getPropertyType(prop); + Object timestamp = timestampProvider.createTimestamp(lastUpdateType); + ea.setProperty(prop, timestamp); + } } return true; } - /** - * Here for binary compatibility. Deprecated. - * - * @deprecated Use {@link #hasLastUpdated(String)} instead - */ - @Deprecated - protected boolean hasLastUpdated(PersistentEntity entity) { - return hasLastUpdated(entity.getName()); - } - - protected boolean hasLastUpdated(String n) { - return entitiesWithLastUpdated.containsKey(n) && entitiesWithLastUpdated.get(n); + protected Set getLastUpdatedPropertyNames(String entityName) { + return entitiesWithLastUpdated.get(entityName); } - /** - * Here for binary compatibility. Deprecated. - * - * @deprecated Use {@link #hasDateCreated(String)} instead - */ - @Deprecated - protected boolean hasDateCreated(PersistentEntity entity) { - return hasDateCreated(entity.getName()); + protected Set getDateCreatedPropertyNames(String entityName) { + return entitiesWithDateCreated.get(entityName); } - protected boolean hasDateCreated(String n) { - return entitiesWithDateCreated.containsKey(n)&& entitiesWithDateCreated.get(n); + private static Field getFieldFromHierarchy(PersistentEntity persistentEntity, String fieldName) { + Class clazz = persistentEntity.getJavaClass(); + while (clazz != null) { + try { + return clazz.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + persistentEntity = persistentEntity.getParentEntity(); + clazz = persistentEntity == null? null : persistentEntity.getJavaClass(); + } + } + return null; } protected void storeDateCreatedAndLastUpdatedInfo(PersistentEntity persistentEntity) { - if(persistentEntity.isInitialized()) { - + if (persistentEntity.isInitialized()) { ClassMapping classMapping = persistentEntity.getMapping(); - Entity mappedForm = classMapping.getMappedForm(); - if(mappedForm == null || mappedForm.isAutoTimestamp()) { - storeTimestampAvailability(entitiesWithDateCreated, persistentEntity, persistentEntity.getPropertyByName(DATE_CREATED_PROPERTY)); - storeTimestampAvailability(entitiesWithLastUpdated, persistentEntity, persistentEntity.getPropertyByName(LAST_UPDATED_PROPERTY)); + Entity mappedForm = classMapping.getMappedForm(); + if (mappedForm == null || mappedForm.isAutoTimestamp()) { + for (PersistentProperty property : persistentEntity.getPersistentProperties()) { + if (property.getName().equals(LAST_UPDATED_PROPERTY)) { + storeTimestampAvailability(entitiesWithLastUpdated, persistentEntity, property); + } else if (property.getName().equals(DATE_CREATED_PROPERTY)) { + storeTimestampAvailability(entitiesWithDateCreated, persistentEntity, property); + } else { + Field field = getFieldFromHierarchy(persistentEntity, property.getName()); + if (field != null && field.isAnnotationPresent(AutoTimestamp.class)) { + AutoTimestamp autoTimestamp = field.getAnnotation(AutoTimestamp.class); + if (autoTimestamp.value() == AutoTimestamp.EventType.UPDATED) { + storeTimestampAvailability(entitiesWithLastUpdated, persistentEntity, property); + } else { + storeTimestampAvailability(entitiesWithDateCreated, persistentEntity, property); + } + } + } + } } - } - else { + } else { uninitializedEntities.add(persistentEntity.getName()); } } - protected void storeTimestampAvailability(Map timestampAvailabilityMap, PersistentEntity persistentEntity, PersistentProperty property) { - timestampAvailabilityMap.put(persistentEntity.getName(), property != null && timestampProvider.supportsCreating(property.getType())); + protected void storeTimestampAvailability(Map> timestampAvailabilityMap, PersistentEntity persistentEntity, PersistentProperty property) { + if (property != null && timestampProvider.supportsCreating(property.getType())) { + Set timestampProperties = timestampAvailabilityMap.computeIfAbsent(persistentEntity.getName(), k -> new HashSet<>()); + timestampProperties.add(property.getName()); + } } public void persistentEntityAdded(PersistentEntity entity) { @@ -186,25 +203,25 @@ public void setTimestampProvider(TimestampProvider timestampProvider) { this.timestampProvider = timestampProvider; } - private void processAllEntries(final Set> entries, final Runnable runnable) { - Map originalValues = new LinkedHashMap(); - for(Map.Entry entry: entries) { + private void processAllEntries(final Set>> entries, final Runnable runnable) { + Map> originalValues = new LinkedHashMap<>(); + for (Map.Entry> entry: entries) { originalValues.put(entry.getKey(), entry.getValue()); - entry.setValue(false); + entry.setValue(null); } runnable.run(); - for(Map.Entry entry: entries) { + for (Map.Entry> entry: entries) { entry.setValue(originalValues.get(entry.getKey())); } } - private void processEntries(final List classes, Map entities, final Runnable runnable) { - Set> entries = new HashSet<>(); + private void processEntries(final List classes, Map> entities, final Runnable runnable) { + Set>> entries = new HashSet<>(); final List classNames = new ArrayList<>(classes.size()); - for(Class clazz: classes) { + for (Class clazz: classes) { classNames.add(clazz.getName()); } - for (Map.Entry entry: entities.entrySet()) { + for (Map.Entry> entry: entities.entrySet()) { if (classNames.contains(entry.getKey())) { entries.add(entry); } @@ -238,7 +255,7 @@ public void withoutLastUpdated(final List classes, final Runnable runnabl * @param runnable The code to execute while the last updated listener is disabled */ public void withoutLastUpdated(final Class clazz, final Runnable runnable) { - ArrayList list = new ArrayList(1); + ArrayList list = new ArrayList<>(1); list.add(clazz); withoutLastUpdated(list, runnable); } @@ -269,7 +286,7 @@ public void withoutDateCreated(final List classes, final Runnable runnabl * @param runnable The code to execute while the date created listener is disabled */ public void withoutDateCreated(final Class clazz, final Runnable runnable) { - ArrayList list = new ArrayList(1); + ArrayList list = new ArrayList<>(1); list.add(clazz); withoutDateCreated(list, runnable); } @@ -280,12 +297,7 @@ public void withoutDateCreated(final Class clazz, final Runnable runnable) { * @param runnable The code to execute while the timestamp listeners are disabled */ public void withoutTimestamps(final Runnable runnable) { - withoutDateCreated(new Runnable() { - @Override - public void run() { - withoutLastUpdated(runnable); - } - }); + withoutDateCreated(() -> withoutLastUpdated(runnable)); } /** @@ -295,12 +307,7 @@ public void run() { * @param runnable The code to execute while the timestamp listeners are disabled */ public void withoutTimestamps(final List classes, final Runnable runnable) { - withoutDateCreated(classes, new Runnable() { - @Override - public void run() { - withoutLastUpdated(classes, runnable); - } - }); + withoutDateCreated(classes, () -> withoutLastUpdated(classes, runnable)); } /** @@ -310,12 +317,7 @@ public void run() { * @param runnable The code to execute while the timestamp listeners are disabled */ public void withoutTimestamps(final Class clazz, final Runnable runnable) { - withoutDateCreated(clazz, new Runnable() { - @Override - public void run() { - withoutLastUpdated(clazz, runnable); - } - }); + withoutDateCreated(clazz, () -> withoutLastUpdated(clazz, runnable)); } }