Skip to content

Commit 5b50a4c

Browse files
committed
Introduce @AutoTimestamp annotation
1 parent df8ed81 commit 5b50a4c

File tree

4 files changed

+119
-55
lines changed

4 files changed

+119
-55
lines changed

grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/config/Entity.groovy

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,6 @@ class Entity<P extends Property> {
5656
* @return Whether automatic time stamps should be applied to 'lastUpdate' and 'dateCreated' properties
5757
*/
5858
boolean autoTimestamp = true
59-
String dateCreated = null
60-
String lastUpdated = null
6159

6260
/**
6361
* @return Whether the entity should be autowired

grails-datastore-gorm-tck/src/main/groovy/grails/gorm/tests/CustomAutotimeStampSpec.groovy renamed to grails-datastore-gorm-test/src/test/groovy/org/grails/datastore/gorm/CustomAutoTimestampSpec.groovy

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
package grails.gorm.tests
1+
package org.grails.datastore.gorm
22

3+
import grails.gorm.annotation.AutoTimestamp
4+
import grails.gorm.tests.GormDatastoreSpec
35
import grails.persistence.Entity
46

5-
class CustomAutotimeStampSpec extends GormDatastoreSpec{
7+
class CustomAutoTimestampSpec extends GormDatastoreSpec {
68

79
void "Test when the auto timestamp properties are customized, they are correctly set"() {
810
when:"An entity is persisted"
911
def r = new RecordCustom(name: "Test")
10-
r.save(flush:true)
12+
r.save(flush:true, failOnError:true)
1113
session.clear()
1214
r = RecordCustom.get(r.id)
1315

@@ -23,8 +25,8 @@ class CustomAutotimeStampSpec extends GormDatastoreSpec{
2325
session.clear()
2426
r = RecordCustom.get(r.id)
2527

26-
then:"the custom lastUpdated property is updated and dateCreated is not"
27-
r.modified != null && previousModified > r.modified
28+
then:"the custom lastUpdated property is updated and dateCreated is not"
29+
r.modified != null && previousModified < r.modified
2830
previousCreated == r.created
2931
}
3032
@Override
@@ -37,11 +39,6 @@ class CustomAutotimeStampSpec extends GormDatastoreSpec{
3739
class RecordCustom {
3840
Long id
3941
String name
40-
Date created
41-
Date modified
42-
43-
static mapping = {
44-
dateCreated 'created'
45-
lastUpdated 'modified'
46-
}
42+
@AutoTimestamp(false) Date created
43+
@AutoTimestamp Date modified
4744
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package grails.gorm.annotation;
17+
18+
import java.lang.annotation.ElementType;
19+
import java.lang.annotation.Retention;
20+
import java.lang.annotation.RetentionPolicy;
21+
import java.lang.annotation.Target;
22+
23+
/**
24+
* A property annotation used to enable automatic updates to time objects
25+
* upon gorm modification events
26+
*
27+
* @author Scott Murphy Heiberg
28+
* @since 7.0
29+
*/
30+
@Retention(RetentionPolicy.RUNTIME)
31+
@Target({ElementType.FIELD})
32+
public @interface AutoTimestamp {
33+
/**
34+
* A qualifier boolean value that represents if update events
35+
* should be updated as well. Setting value to false only updates on creation events.
36+
*/
37+
boolean value() default true;
38+
}

grails-datastore-gorm/src/main/groovy/org/grails/datastore/gorm/events/AutoTimestampEventListener.java

Lines changed: 72 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@
1414
*/
1515
package org.grails.datastore.gorm.events;
1616

17+
import java.lang.reflect.Field;
1718
import java.util.*;
1819
import java.util.concurrent.ConcurrentHashMap;
1920
import java.util.concurrent.ConcurrentLinkedQueue;
2021

22+
import grails.gorm.annotation.AutoTimestamp;
2123
import org.grails.datastore.gorm.timestamp.DefaultTimestampProvider;
2224
import org.grails.datastore.gorm.timestamp.TimestampProvider;
2325
import org.grails.datastore.mapping.config.Entity;
@@ -45,13 +47,11 @@ public class AutoTimestampEventListener extends AbstractPersistenceEventListener
4547
public static final String DATE_CREATED_PROPERTY = "dateCreated";
4648
public static final String LAST_UPDATED_PROPERTY = "lastUpdated";
4749

48-
protected Map<String, String> entitiesWithDateCreated = new ConcurrentHashMap<>();
49-
protected Map<String, String> entitiesWithLastUpdated = new ConcurrentHashMap<>();
50+
protected Map<String, Set<String>> entitiesWithDateCreated = new ConcurrentHashMap<>();
51+
protected Map<String, Set<String>> entitiesWithLastUpdated = new ConcurrentHashMap<>();
5052
protected Collection<String> uninitializedEntities = new ConcurrentLinkedQueue<>();
51-
52-
53+
5354
private TimestampProvider timestampProvider = new DefaultTimestampProvider();
54-
5555

5656
public AutoTimestampEventListener(final Datastore datastore) {
5757
super(datastore);
@@ -95,19 +95,23 @@ public boolean beforeInsert(PersistentEntity entity, EntityAccess ea) {
9595
initializeIfNecessary(entity, name);
9696
Class<?> dateCreatedType = null;
9797
Object timestamp = null;
98-
String prop = getDateCreatedPropertyName(name);
99-
if (prop != null) {
100-
dateCreatedType = ea.getPropertyType(prop);
101-
timestamp = timestampProvider.createTimestamp(dateCreatedType);
102-
ea.setProperty(prop, timestamp);
98+
Set<String> props = getDateCreatedPropertyNames(name);
99+
if (props != null) {
100+
for (String prop : props) {
101+
dateCreatedType = ea.getPropertyType(prop);
102+
timestamp = timestampProvider.createTimestamp(dateCreatedType);
103+
ea.setProperty(prop, timestamp);
104+
}
103105
}
104-
prop = getLastUpdatedPropertyName(name);
105-
if (prop != null) {
106-
Class<?> lastUpdateType = ea.getPropertyType(prop);
107-
if (dateCreatedType == null || !lastUpdateType.isAssignableFrom(dateCreatedType)) {
108-
timestamp = timestampProvider.createTimestamp(lastUpdateType);
106+
props = getLastUpdatedPropertyNames(name);
107+
if (props != null) {
108+
for (String prop : props) {
109+
Class<?> lastUpdateType = ea.getPropertyType(prop);
110+
if (dateCreatedType == null || !lastUpdateType.isAssignableFrom(dateCreatedType)) {
111+
timestamp = timestampProvider.createTimestamp(lastUpdateType);
112+
}
113+
ea.setProperty(prop, timestamp);
109114
}
110-
ea.setProperty(prop, timestamp);
111115
}
112116
return true;
113117
}
@@ -120,43 +124,70 @@ private void initializeIfNecessary(PersistentEntity entity, String name) {
120124
}
121125

122126
public boolean beforeUpdate(PersistentEntity entity, EntityAccess ea) {
123-
String prop = getLastUpdatedPropertyName(entity.getName());
124-
if (prop != null) {
125-
Class<?> lastUpdateType = ea.getPropertyType(prop);
126-
Object timestamp = timestampProvider.createTimestamp(lastUpdateType);
127-
ea.setProperty(prop, timestamp);
127+
Set<String> props = getLastUpdatedPropertyNames(entity.getName());
128+
if (props != null) {
129+
for (String prop : props) {
130+
Class<?> lastUpdateType = ea.getPropertyType(prop);
131+
Object timestamp = timestampProvider.createTimestamp(lastUpdateType);
132+
ea.setProperty(prop, timestamp);
133+
}
128134
}
129135
return true;
130136
}
131137

132-
protected String getLastUpdatedPropertyName(String n) {
133-
return entitiesWithLastUpdated.get(n);
138+
protected Set<String> getLastUpdatedPropertyNames(String entityName) {
139+
return entitiesWithLastUpdated.get(entityName);
134140
}
135141

136-
protected String getDateCreatedPropertyName(String n) {
137-
return entitiesWithDateCreated.get(n);
142+
protected Set<String> getDateCreatedPropertyNames(String entityName) {
143+
return entitiesWithDateCreated.get(entityName);
144+
}
145+
146+
private static Field getFieldFromHierarchy(PersistentEntity persistentEntity, String fieldName) {
147+
Class<?> clazz = persistentEntity.getJavaClass();
148+
while (clazz != null) {
149+
try {
150+
return clazz.getDeclaredField(fieldName);
151+
} catch (NoSuchFieldException e) {
152+
persistentEntity = persistentEntity.getParentEntity();
153+
clazz = persistentEntity == null? null : persistentEntity.getJavaClass();
154+
}
155+
}
156+
return null;
138157
}
139158

140159
protected void storeDateCreatedAndLastUpdatedInfo(PersistentEntity persistentEntity) {
141160
if (persistentEntity.isInitialized()) {
142161
ClassMapping<?> classMapping = persistentEntity.getMapping();
143-
Entity mappedForm = classMapping.getMappedForm();
162+
Entity<?> mappedForm = classMapping.getMappedForm();
144163
if (mappedForm == null || mappedForm.isAutoTimestamp()) {
145-
storeTimestampAvailability(entitiesWithDateCreated, persistentEntity,
146-
persistentEntity.getPropertyByName(mappedForm.getDateCreated() != null?
147-
mappedForm.getDateCreated() : DATE_CREATED_PROPERTY));
148-
storeTimestampAvailability(entitiesWithLastUpdated, persistentEntity,
149-
persistentEntity.getPropertyByName(mappedForm.getLastUpdated() != null?
150-
mappedForm.getLastUpdated() : LAST_UPDATED_PROPERTY));
164+
for (PersistentProperty<?> property : persistentEntity.getPersistentProperties()) {
165+
if (property.getName().equals(LAST_UPDATED_PROPERTY)) {
166+
storeTimestampAvailability(entitiesWithLastUpdated, persistentEntity, property);
167+
} else if (property.getName().equals(DATE_CREATED_PROPERTY)) {
168+
storeTimestampAvailability(entitiesWithDateCreated, persistentEntity, property);
169+
} else {
170+
Field field = getFieldFromHierarchy(persistentEntity, property.getName());
171+
if (field != null && field.isAnnotationPresent(AutoTimestamp.class)) {
172+
AutoTimestamp autoTimestamp = field.getAnnotation(AutoTimestamp.class);
173+
if (autoTimestamp.value()) {
174+
storeTimestampAvailability(entitiesWithLastUpdated, persistentEntity, property);
175+
} else {
176+
storeTimestampAvailability(entitiesWithDateCreated, persistentEntity, property);
177+
}
178+
}
179+
}
180+
}
151181
}
152182
} else {
153183
uninitializedEntities.add(persistentEntity.getName());
154184
}
155185
}
156186

157-
protected void storeTimestampAvailability(Map<String, String> timestampAvailabilityMap, PersistentEntity persistentEntity, PersistentProperty<?> property) {
187+
protected void storeTimestampAvailability(Map<String, Set<String>> timestampAvailabilityMap, PersistentEntity persistentEntity, PersistentProperty<?> property) {
158188
if (property != null && timestampProvider.supportsCreating(property.getType())) {
159-
timestampAvailabilityMap.put(persistentEntity.getName(), property.getName());
189+
Set<String> timestampProperties = timestampAvailabilityMap.computeIfAbsent(persistentEntity.getName(), k -> new HashSet<>());
190+
timestampProperties.add(property.getName());
160191
}
161192
}
162193

@@ -172,25 +203,25 @@ public void setTimestampProvider(TimestampProvider timestampProvider) {
172203
this.timestampProvider = timestampProvider;
173204
}
174205

175-
private void processAllEntries(final Set<Map.Entry<String, String>> entries, final Runnable runnable) {
176-
Map<String, String> originalValues = new LinkedHashMap<>();
177-
for (Map.Entry<String, String> entry: entries) {
206+
private void processAllEntries(final Set<Map.Entry<String, Set<String>>> entries, final Runnable runnable) {
207+
Map<String, Set<String>> originalValues = new LinkedHashMap<>();
208+
for (Map.Entry<String, Set<String>> entry: entries) {
178209
originalValues.put(entry.getKey(), entry.getValue());
179210
entry.setValue(null);
180211
}
181212
runnable.run();
182-
for (Map.Entry<String, String> entry: entries) {
213+
for (Map.Entry<String, Set<String>> entry: entries) {
183214
entry.setValue(originalValues.get(entry.getKey()));
184215
}
185216
}
186217

187-
private void processEntries(final List<Class> classes, Map<String, String> entities, final Runnable runnable) {
188-
Set<Map.Entry<String, String>> entries = new HashSet<>();
218+
private void processEntries(final List<Class> classes, Map<String, Set<String>> entities, final Runnable runnable) {
219+
Set<Map.Entry<String, Set<String>>> entries = new HashSet<>();
189220
final List<String> classNames = new ArrayList<>(classes.size());
190221
for (Class clazz: classes) {
191222
classNames.add(clazz.getName());
192223
}
193-
for (Map.Entry<String, String> entry: entities.entrySet()) {
224+
for (Map.Entry<String, Set<String>> entry: entities.entrySet()) {
194225
if (classNames.contains(entry.getKey())) {
195226
entries.add(entry);
196227
}

0 commit comments

Comments
 (0)