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