matrei commented on code in PR #15118:
URL: https://github.com/apache/grails-core/pull/15118#discussion_r2486557017
##########
grails-datamapping-core/src/main/groovy/grails/gorm/annotation/AutoTimestamp.java:
##########
@@ -29,7 +29,9 @@
*
* @author Scott Murphy Heiberg
* @since 7.0
+ * @deprecated Use {@link CreatedDate} for creation timestamps or {@link
LastModifiedDate} for update timestamps instead
*/
+@Deprecated
Review Comment:
```suggestion
@Deprecated(forRemoval = true)
```
##########
grails-datamapping-core/src/main/groovy/grails/gorm/annotation/CreatedDate.java:
##########
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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
+ *
+ * https://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 events. This is an alias for
@AutoTimestamp(EventType.CREATED).
+ *
+ * @author Scott Murphy Heiberg
+ * @since 7.0
Review Comment:
```suggestion
* @since 7.1
```
##########
grails-datamapping-core/src/main/groovy/grails/gorm/annotation/CreatedBy.java:
##########
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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
+ *
+ * https://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 automatically populate a field with the
current auditor
+ * upon GORM insert events. The current auditor is retrieved from an {@link
org.grails.datastore.gorm.timestamp.AuditorAware}
+ * bean registered in the Spring application context.
+ *
+ * <p>Example usage:</p>
+ * <pre>
+ * class Book {
+ * @CreatedBy
+ * String createdBy
+ *
+ * @CreatedBy
+ * User creator
+ *
+ * @CreatedBy
+ * Long creatorId
+ * }
+ * </pre>
+ *
+ * <p>The field type should match the type parameter of your {@link
org.grails.datastore.gorm.timestamp.AuditorAware}
+ * implementation (e.g., String, Long, User, etc.).</p>
+ *
+ * @author Scott Murphy Heiberg
+ * @since 7.0
Review Comment:
```suggestion
* @since 7.1
```
##########
grails-datamapping-core/src/main/groovy/grails/gorm/annotation/LastModifiedDate.java:
##########
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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
+ *
+ * https://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. This is an alias for
@AutoTimestamp(EventType.UPDATED).
+ *
+ * @author Scott Murphy Heiberg
+ * @since 7.0
Review Comment:
```suggestion
* @since 7.1
```
##########
grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/model/AutoTimestampUtils.java:
##########
@@ -0,0 +1,202 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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
+ *
+ * https://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 org.grails.datastore.mapping.model;
+
+import java.lang.reflect.Field;
+
+import org.springframework.util.ReflectionUtils;
+
+import grails.util.Environment;
+import org.grails.datastore.mapping.config.Property;
+import org.grails.datastore.mapping.config.Property.AutoTimestampType;
+
+/**
+ * Utility class for detecting and caching auto-timestamp and auditing
annotations on domain properties.
+ * This avoids repeated reflection calls by storing the annotation type in the
Property metadata.
+ *
+ * <p>Supports the following annotations (both GORM and Spring Data
variants):</p>
+ * <ul>
+ * <li>@CreatedDate / @grails.gorm.annotation.CreatedDate - automatically
set on insert</li>
+ * <li>@LastModifiedDate / @grails.gorm.annotation.LastModifiedDate -
automatically set on insert and update</li>
+ * <li>@CreatedBy / @grails.gorm.annotation.CreatedBy - automatically
populated with current auditor on insert</li>
+ * <li>@LastModifiedBy / @grails.gorm.annotation.LastModifiedBy -
automatically populated with current auditor on insert and update</li>
+ * <li>@AutoTimestamp - GORM-specific annotation for backwards
compatibility</li>
+ * </ul>
+ *
+ * <p>Caching is automatically disabled in development mode ({@link
Environment#isDevelopmentMode()})
+ * to ensure annotation changes are picked up during class reloading.</p>
+ *
+ * @author Scott Murphy Heiberg
+ * @since 7.0
+ */
+public class AutoTimestampUtils {
+
+ private static final String CREATED_DATE_ANNOTATION =
"grails.gorm.annotation.CreatedDate";
+ private static final String LAST_MODIFIED_DATE_ANNOTATION =
"grails.gorm.annotation.LastModifiedDate";
+ private static final String AUTO_TIMESTAMP_ANNOTATION =
"grails.gorm.annotation.AutoTimestamp";
+ private static final String CREATED_BY_ANNOTATION =
"grails.gorm.annotation.CreatedBy";
+ private static final String LAST_MODIFIED_BY_ANNOTATION =
"grails.gorm.annotation.LastModifiedBy";
+
+ private static final String CREATED_DATE_SPRING_ANNOTATION =
"org.springframework.data.annotation.CreatedDate";
+ private static final String LAST_MODIFIED_DATE_SPRING_ANNOTATION =
"org.springframework.data.annotation.LastModifiedDate";
+ private static final String CREATED_BY_SPRING_ANNOTATION =
"org.springframework.data.annotation.CreatedBy";
+ private static final String LAST_MODIFIED_BY_SPRING_ANNOTATION =
"org.springframework.data.annotation.LastModifiedBy";
+
+ /**
+ * Gets the auto-timestamp type for a persistent property, using cached
metadata when not in development mode.
+ *
+ * <p>In development mode, this method will always perform reflection to
detect the current
+ * annotation state, ensuring that annotation changes during class
reloading are immediately
+ * recognized. In production, the result is cached to avoid repeated
reflection calls.</p>
+ *
+ * @param persistentProperty The persistent property to check
+ * @return The auto-timestamp type (CREATED, UPDATED, or NONE)
+ */
+ public static AutoTimestampType getAutoTimestampType(PersistentProperty<?>
persistentProperty) {
+ Property mappedForm = persistentProperty.getMapping().getMappedForm();
+
+ // In development mode, always detect fresh to support class reloading
+ if (Environment.isDevelopmentMode()) {
+ return detectAutoTimestampType(persistentProperty);
+ }
+
+ // Return cached value if available
+ if (mappedForm.getAutoTimestampType() != null) {
+ return mappedForm.getAutoTimestampType();
+ }
+
+ // Detect and cache the annotation type
+ AutoTimestampType type = detectAutoTimestampType(persistentProperty);
+ mappedForm.setAutoTimestampType(type);
+ return type;
+ }
+
+ /**
+ * Detects the auto-timestamp annotation type on a property using
reflection.
+ *
+ * <p>When caching is enabled (production mode), this method is called
once per property
+ * and the result is cached. When caching is disabled (development mode),
this method
+ * is called on every access to ensure annotation changes are detected.</p>
+ *
+ * @param persistentProperty The persistent property to check
+ * @return The auto-timestamp type (CREATED, UPDATED, or NONE)
+ */
+ private static AutoTimestampType
detectAutoTimestampType(PersistentProperty<?> persistentProperty) {
+ try {
+ Field field = ReflectionUtils.findField(
+ persistentProperty.getOwner().getJavaClass(),
+ persistentProperty.getName()
+ );
+
+ if (field != null) {
+ for (java.lang.annotation.Annotation annotation :
field.getDeclaredAnnotations()) {
+ String annotationName =
annotation.annotationType().getName();
+
+ if (CREATED_DATE_ANNOTATION.equals(annotationName) ||
+ CREATED_DATE_SPRING_ANNOTATION.equals(annotationName))
{
+ return AutoTimestampType.CREATED;
+ } else if
(LAST_MODIFIED_DATE_ANNOTATION.equals(annotationName) ||
+
LAST_MODIFIED_DATE_SPRING_ANNOTATION.equals(annotationName)) {
+ return AutoTimestampType.UPDATED;
+ } else if (CREATED_BY_ANNOTATION.equals(annotationName) ||
+
CREATED_BY_SPRING_ANNOTATION.equals(annotationName)) {
+ return AutoTimestampType.CREATED_BY;
+ } else if
(LAST_MODIFIED_BY_ANNOTATION.equals(annotationName) ||
+
LAST_MODIFIED_BY_SPRING_ANNOTATION.equals(annotationName)) {
+ return AutoTimestampType.UPDATED_BY;
+ } else if
(AUTO_TIMESTAMP_ANNOTATION.equals(annotationName)) {
+ // For @AutoTimestamp, check the EventType value
+ try {
+ Object eventTypeValue = annotation.annotationType()
+ .getMethod("value")
+ .invoke(annotation);
+
+ if (eventTypeValue != null) {
+ String eventTypeName =
eventTypeValue.toString();
+ if (eventTypeName.equals("UPDATED")) {
+ return AutoTimestampType.UPDATED;
+ } else {
+ return AutoTimestampType.CREATED;
+ }
+ }
+ } catch (Exception e) {
+ // If we can't read the value, default to CREATED
+ return AutoTimestampType.CREATED;
+ }
+ }
+ }
+ }
+ } catch (Exception ignored) {
+ // If reflection fails, return NONE
+ }
+
+ return AutoTimestampType.NONE;
+ }
+
+ /**
+ * Checks if a property has any auto-timestamp or auditing annotation.
+ *
+ * @param persistentProperty The persistent property to check
+ * @return true if the property has any supported annotation
(@CreatedDate, @LastModifiedDate,
+ * @CreatedBy, @LastModifiedBy, or @AutoTimestamp) from either
GORM or Spring Data
Review Comment:
```suggestion
* @return true if the property has any supported annotation ({@code
@CreatedDate}, {@code @LastModifiedDate},
* {@code @CreatedBy}, {@code @LastModifiedBy}, or {@code
@AutoTimestamp}) from either GORM or Spring Data
```
##########
grails-fields/src/main/groovy/org/grails/scaffolding/model/DomainModelServiceImpl.groovy:
##########
Review Comment:
This class is no longer implementing `getInputProperties(PersistentEntity,
List)` from the `DomainModelService` interface.
##########
grails-fields/grails-app/taglib/grails/plugin/formfields/FormFieldsTagLib.groovy:
##########
@@ -618,7 +618,8 @@ class FormFieldsTagLib {
}
} else {
properties = list ?
domainModelService.getListOutputProperties(domainClass) :
domainModelService.getInputProperties(domainClass,
- exclusionType == ExclusionType.Input ? exclusionsInput :
exclusionsDisplay)
+ exclusionType == ExclusionType.Input ? exclusionsInput :
exclusionsDisplay,
+ exclusionType == ExclusionType.Input)
Review Comment:
`getInputProperties(PersistenProperty, List<String>, boolean)` is not part
of the `DomainModelService` interface.
##########
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy:
##########
@@ -226,17 +226,47 @@ class GrailsGradlePlugin extends GroovyPlugin {
protected Closure<String> getGroovyCompilerScript(GroovyCompile compile,
Project project) {
GrailsExtension grails = project.extensions.findByType(GrailsExtension)
- if (!grails.importJavaTime) {
+
+ List<String> starImports = []
+
+ // Add java.time if enabled
+ if (grails.importJavaTime) {
+ starImports.add('java.time')
+ }
+
+ // Add Grails annotation packages if enabled and dependencies are
present
+ if (grails.importGrailsAnnotations) {
+ // Check for grails-datamapping-core (grails.gorm.annotation.*)
+ def datamappingCoreDep =
project.configurations.getByName('compileClasspath').dependencies.find {
Dependency d ->
+ d.group == 'org.apache.grails.data' && d.name ==
'grails-datamapping-core'
+ }
+ if (datamappingCoreDep) {
+ starImports.add('grails.gorm.annotation')
+ }
+
+ // Check for grails-scaffolding
(grails.plugin.scaffolding.annotation.*)
+ def scaffoldingDep =
project.configurations.getByName('compileClasspath').dependencies.find {
Dependency d ->
+ d.group == 'org.apache.grails' && d.name ==
'grails-scaffolding'
+ }
+ if (scaffoldingDep) {
+ starImports.add('grails.plugin.scaffolding.annotation')
+ }
Review Comment:
> what are your thoughts about getting rid of @AutoTimestamp?
@codeconsole Seems reasonable :+1:
##########
grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/model/AutoTimestampUtils.java:
##########
@@ -0,0 +1,202 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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
+ *
+ * https://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 org.grails.datastore.mapping.model;
+
+import java.lang.reflect.Field;
+
+import org.springframework.util.ReflectionUtils;
+
+import grails.util.Environment;
+import org.grails.datastore.mapping.config.Property;
+import org.grails.datastore.mapping.config.Property.AutoTimestampType;
+
+/**
+ * Utility class for detecting and caching auto-timestamp and auditing
annotations on domain properties.
+ * This avoids repeated reflection calls by storing the annotation type in the
Property metadata.
+ *
+ * <p>Supports the following annotations (both GORM and Spring Data
variants):</p>
+ * <ul>
+ * <li>@CreatedDate / @grails.gorm.annotation.CreatedDate - automatically
set on insert</li>
+ * <li>@LastModifiedDate / @grails.gorm.annotation.LastModifiedDate -
automatically set on insert and update</li>
+ * <li>@CreatedBy / @grails.gorm.annotation.CreatedBy - automatically
populated with current auditor on insert</li>
+ * <li>@LastModifiedBy / @grails.gorm.annotation.LastModifiedBy -
automatically populated with current auditor on insert and update</li>
+ * <li>@AutoTimestamp - GORM-specific annotation for backwards
compatibility</li>
+ * </ul>
+ *
+ * <p>Caching is automatically disabled in development mode ({@link
Environment#isDevelopmentMode()})
+ * to ensure annotation changes are picked up during class reloading.</p>
+ *
+ * @author Scott Murphy Heiberg
+ * @since 7.0
+ */
+public class AutoTimestampUtils {
+
+ private static final String CREATED_DATE_ANNOTATION =
"grails.gorm.annotation.CreatedDate";
+ private static final String LAST_MODIFIED_DATE_ANNOTATION =
"grails.gorm.annotation.LastModifiedDate";
+ private static final String AUTO_TIMESTAMP_ANNOTATION =
"grails.gorm.annotation.AutoTimestamp";
+ private static final String CREATED_BY_ANNOTATION =
"grails.gorm.annotation.CreatedBy";
+ private static final String LAST_MODIFIED_BY_ANNOTATION =
"grails.gorm.annotation.LastModifiedBy";
+
+ private static final String CREATED_DATE_SPRING_ANNOTATION =
"org.springframework.data.annotation.CreatedDate";
+ private static final String LAST_MODIFIED_DATE_SPRING_ANNOTATION =
"org.springframework.data.annotation.LastModifiedDate";
+ private static final String CREATED_BY_SPRING_ANNOTATION =
"org.springframework.data.annotation.CreatedBy";
+ private static final String LAST_MODIFIED_BY_SPRING_ANNOTATION =
"org.springframework.data.annotation.LastModifiedBy";
+
+ /**
+ * Gets the auto-timestamp type for a persistent property, using cached
metadata when not in development mode.
+ *
+ * <p>In development mode, this method will always perform reflection to
detect the current
+ * annotation state, ensuring that annotation changes during class
reloading are immediately
+ * recognized. In production, the result is cached to avoid repeated
reflection calls.</p>
+ *
+ * @param persistentProperty The persistent property to check
+ * @return The auto-timestamp type (CREATED, UPDATED, or NONE)
+ */
+ public static AutoTimestampType getAutoTimestampType(PersistentProperty<?>
persistentProperty) {
+ Property mappedForm = persistentProperty.getMapping().getMappedForm();
+
+ // In development mode, always detect fresh to support class reloading
+ if (Environment.isDevelopmentMode()) {
+ return detectAutoTimestampType(persistentProperty);
+ }
+
+ // Return cached value if available
+ if (mappedForm.getAutoTimestampType() != null) {
+ return mappedForm.getAutoTimestampType();
+ }
+
+ // Detect and cache the annotation type
+ AutoTimestampType type = detectAutoTimestampType(persistentProperty);
+ mappedForm.setAutoTimestampType(type);
+ return type;
+ }
+
+ /**
+ * Detects the auto-timestamp annotation type on a property using
reflection.
+ *
+ * <p>When caching is enabled (production mode), this method is called
once per property
+ * and the result is cached. When caching is disabled (development mode),
this method
+ * is called on every access to ensure annotation changes are detected.</p>
+ *
+ * @param persistentProperty The persistent property to check
+ * @return The auto-timestamp type (CREATED, UPDATED, or NONE)
+ */
+ private static AutoTimestampType
detectAutoTimestampType(PersistentProperty<?> persistentProperty) {
+ try {
+ Field field = ReflectionUtils.findField(
+ persistentProperty.getOwner().getJavaClass(),
+ persistentProperty.getName()
+ );
+
+ if (field != null) {
+ for (java.lang.annotation.Annotation annotation :
field.getDeclaredAnnotations()) {
+ String annotationName =
annotation.annotationType().getName();
+
+ if (CREATED_DATE_ANNOTATION.equals(annotationName) ||
+ CREATED_DATE_SPRING_ANNOTATION.equals(annotationName))
{
+ return AutoTimestampType.CREATED;
+ } else if
(LAST_MODIFIED_DATE_ANNOTATION.equals(annotationName) ||
+
LAST_MODIFIED_DATE_SPRING_ANNOTATION.equals(annotationName)) {
+ return AutoTimestampType.UPDATED;
+ } else if (CREATED_BY_ANNOTATION.equals(annotationName) ||
+
CREATED_BY_SPRING_ANNOTATION.equals(annotationName)) {
+ return AutoTimestampType.CREATED_BY;
+ } else if
(LAST_MODIFIED_BY_ANNOTATION.equals(annotationName) ||
+
LAST_MODIFIED_BY_SPRING_ANNOTATION.equals(annotationName)) {
+ return AutoTimestampType.UPDATED_BY;
+ } else if
(AUTO_TIMESTAMP_ANNOTATION.equals(annotationName)) {
+ // For @AutoTimestamp, check the EventType value
+ try {
+ Object eventTypeValue = annotation.annotationType()
+ .getMethod("value")
+ .invoke(annotation);
+
+ if (eventTypeValue != null) {
+ String eventTypeName =
eventTypeValue.toString();
+ if (eventTypeName.equals("UPDATED")) {
+ return AutoTimestampType.UPDATED;
+ } else {
+ return AutoTimestampType.CREATED;
+ }
+ }
+ } catch (Exception e) {
+ // If we can't read the value, default to CREATED
+ return AutoTimestampType.CREATED;
+ }
+ }
+ }
+ }
+ } catch (Exception ignored) {
+ // If reflection fails, return NONE
+ }
+
+ return AutoTimestampType.NONE;
+ }
+
+ /**
+ * Checks if a property has any auto-timestamp or auditing annotation.
+ *
+ * @param persistentProperty The persistent property to check
+ * @return true if the property has any supported annotation
(@CreatedDate, @LastModifiedDate,
+ * @CreatedBy, @LastModifiedBy, or @AutoTimestamp) from either
GORM or Spring Data
+ */
+ public static boolean hasAutoTimestampAnnotation(PersistentProperty<?>
persistentProperty) {
+ return persistentProperty != null &&
getAutoTimestampType(persistentProperty) != AutoTimestampType.NONE;
+ }
+
+ /**
+ * Checks if a property has a @CreatedDate annotation (GORM or Spring
Data) or @AutoTimestamp(CREATED).
Review Comment:
```suggestion
* Checks if a property has a {@code @CreatedDate} annotation (GORM or
Spring Data) or {@code @AutoTimestamp(CREATED)}.
```
##########
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/timestamp/AuditorAware.java:
##########
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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
+ *
+ * https://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 org.grails.datastore.gorm.timestamp;
+
+import java.util.Optional;
+
+/**
+ * Interface for components that are aware of the application's current
auditor.
+ * This will be used to populate @CreatedBy and @LastModifiedBy annotated
fields
+ * in domain objects.
+ *
+ * <p>Implementations should be registered as Spring beans. The type parameter
+ * should match the type of the auditor field in your domain classes (e.g.,
User,
+ * Long, String, etc.).</p>
+ *
+ * <p>Example implementation:</p>
+ * <pre>
+ * @Component
+ * public class SpringSecurityAuditorAware implements
AuditorAware<String> {
+ * @Override
+ * public Optional<String> getCurrentAuditor() {
+ * return Optional.ofNullable(SecurityContextHolder.getContext())
+ * .map(SecurityContext::getAuthentication)
+ * .filter(Authentication::isAuthenticated)
+ * .map(Authentication::getName);
+ * }
+ * }
+ * </pre>
+ *
+ * @param <T> the type of the auditor (e.g., User, Long, String)
+ * @author Scott Murphy Heiberg
+ * @since 7.0
Review Comment:
```suggestion
* @since 7.1
```
##########
grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/model/AutoTimestampUtils.java:
##########
@@ -0,0 +1,202 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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
+ *
+ * https://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 org.grails.datastore.mapping.model;
+
+import java.lang.reflect.Field;
+
+import org.springframework.util.ReflectionUtils;
+
+import grails.util.Environment;
+import org.grails.datastore.mapping.config.Property;
+import org.grails.datastore.mapping.config.Property.AutoTimestampType;
+
+/**
+ * Utility class for detecting and caching auto-timestamp and auditing
annotations on domain properties.
+ * This avoids repeated reflection calls by storing the annotation type in the
Property metadata.
+ *
+ * <p>Supports the following annotations (both GORM and Spring Data
variants):</p>
+ * <ul>
+ * <li>@CreatedDate / @grails.gorm.annotation.CreatedDate - automatically
set on insert</li>
+ * <li>@LastModifiedDate / @grails.gorm.annotation.LastModifiedDate -
automatically set on insert and update</li>
+ * <li>@CreatedBy / @grails.gorm.annotation.CreatedBy - automatically
populated with current auditor on insert</li>
+ * <li>@LastModifiedBy / @grails.gorm.annotation.LastModifiedBy -
automatically populated with current auditor on insert and update</li>
+ * <li>@AutoTimestamp - GORM-specific annotation for backwards
compatibility</li>
+ * </ul>
+ *
+ * <p>Caching is automatically disabled in development mode ({@link
Environment#isDevelopmentMode()})
+ * to ensure annotation changes are picked up during class reloading.</p>
+ *
+ * @author Scott Murphy Heiberg
+ * @since 7.0
Review Comment:
```suggestion
* @since 7.1
```
##########
grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/model/AutoTimestampUtils.java:
##########
@@ -0,0 +1,202 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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
+ *
+ * https://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 org.grails.datastore.mapping.model;
+
+import java.lang.reflect.Field;
+
+import org.springframework.util.ReflectionUtils;
+
+import grails.util.Environment;
+import org.grails.datastore.mapping.config.Property;
+import org.grails.datastore.mapping.config.Property.AutoTimestampType;
+
+/**
+ * Utility class for detecting and caching auto-timestamp and auditing
annotations on domain properties.
+ * This avoids repeated reflection calls by storing the annotation type in the
Property metadata.
+ *
+ * <p>Supports the following annotations (both GORM and Spring Data
variants):</p>
+ * <ul>
+ * <li>@CreatedDate / @grails.gorm.annotation.CreatedDate - automatically
set on insert</li>
+ * <li>@LastModifiedDate / @grails.gorm.annotation.LastModifiedDate -
automatically set on insert and update</li>
+ * <li>@CreatedBy / @grails.gorm.annotation.CreatedBy - automatically
populated with current auditor on insert</li>
+ * <li>@LastModifiedBy / @grails.gorm.annotation.LastModifiedBy -
automatically populated with current auditor on insert and update</li>
+ * <li>@AutoTimestamp - GORM-specific annotation for backwards
compatibility</li>
+ * </ul>
+ *
+ * <p>Caching is automatically disabled in development mode ({@link
Environment#isDevelopmentMode()})
+ * to ensure annotation changes are picked up during class reloading.</p>
+ *
+ * @author Scott Murphy Heiberg
+ * @since 7.0
+ */
+public class AutoTimestampUtils {
+
+ private static final String CREATED_DATE_ANNOTATION =
"grails.gorm.annotation.CreatedDate";
+ private static final String LAST_MODIFIED_DATE_ANNOTATION =
"grails.gorm.annotation.LastModifiedDate";
+ private static final String AUTO_TIMESTAMP_ANNOTATION =
"grails.gorm.annotation.AutoTimestamp";
+ private static final String CREATED_BY_ANNOTATION =
"grails.gorm.annotation.CreatedBy";
+ private static final String LAST_MODIFIED_BY_ANNOTATION =
"grails.gorm.annotation.LastModifiedBy";
+
+ private static final String CREATED_DATE_SPRING_ANNOTATION =
"org.springframework.data.annotation.CreatedDate";
+ private static final String LAST_MODIFIED_DATE_SPRING_ANNOTATION =
"org.springframework.data.annotation.LastModifiedDate";
+ private static final String CREATED_BY_SPRING_ANNOTATION =
"org.springframework.data.annotation.CreatedBy";
+ private static final String LAST_MODIFIED_BY_SPRING_ANNOTATION =
"org.springframework.data.annotation.LastModifiedBy";
+
+ /**
+ * Gets the auto-timestamp type for a persistent property, using cached
metadata when not in development mode.
+ *
+ * <p>In development mode, this method will always perform reflection to
detect the current
+ * annotation state, ensuring that annotation changes during class
reloading are immediately
+ * recognized. In production, the result is cached to avoid repeated
reflection calls.</p>
+ *
+ * @param persistentProperty The persistent property to check
+ * @return The auto-timestamp type (CREATED, UPDATED, or NONE)
+ */
+ public static AutoTimestampType getAutoTimestampType(PersistentProperty<?>
persistentProperty) {
+ Property mappedForm = persistentProperty.getMapping().getMappedForm();
+
+ // In development mode, always detect fresh to support class reloading
+ if (Environment.isDevelopmentMode()) {
+ return detectAutoTimestampType(persistentProperty);
+ }
+
+ // Return cached value if available
+ if (mappedForm.getAutoTimestampType() != null) {
+ return mappedForm.getAutoTimestampType();
+ }
+
+ // Detect and cache the annotation type
+ AutoTimestampType type = detectAutoTimestampType(persistentProperty);
+ mappedForm.setAutoTimestampType(type);
+ return type;
+ }
+
+ /**
+ * Detects the auto-timestamp annotation type on a property using
reflection.
+ *
+ * <p>When caching is enabled (production mode), this method is called
once per property
+ * and the result is cached. When caching is disabled (development mode),
this method
+ * is called on every access to ensure annotation changes are detected.</p>
+ *
+ * @param persistentProperty The persistent property to check
+ * @return The auto-timestamp type (CREATED, UPDATED, or NONE)
+ */
+ private static AutoTimestampType
detectAutoTimestampType(PersistentProperty<?> persistentProperty) {
+ try {
+ Field field = ReflectionUtils.findField(
+ persistentProperty.getOwner().getJavaClass(),
+ persistentProperty.getName()
+ );
+
+ if (field != null) {
+ for (java.lang.annotation.Annotation annotation :
field.getDeclaredAnnotations()) {
+ String annotationName =
annotation.annotationType().getName();
+
+ if (CREATED_DATE_ANNOTATION.equals(annotationName) ||
+ CREATED_DATE_SPRING_ANNOTATION.equals(annotationName))
{
+ return AutoTimestampType.CREATED;
+ } else if
(LAST_MODIFIED_DATE_ANNOTATION.equals(annotationName) ||
+
LAST_MODIFIED_DATE_SPRING_ANNOTATION.equals(annotationName)) {
+ return AutoTimestampType.UPDATED;
+ } else if (CREATED_BY_ANNOTATION.equals(annotationName) ||
+
CREATED_BY_SPRING_ANNOTATION.equals(annotationName)) {
+ return AutoTimestampType.CREATED_BY;
+ } else if
(LAST_MODIFIED_BY_ANNOTATION.equals(annotationName) ||
+
LAST_MODIFIED_BY_SPRING_ANNOTATION.equals(annotationName)) {
+ return AutoTimestampType.UPDATED_BY;
+ } else if
(AUTO_TIMESTAMP_ANNOTATION.equals(annotationName)) {
+ // For @AutoTimestamp, check the EventType value
+ try {
+ Object eventTypeValue = annotation.annotationType()
+ .getMethod("value")
+ .invoke(annotation);
+
+ if (eventTypeValue != null) {
+ String eventTypeName =
eventTypeValue.toString();
+ if (eventTypeName.equals("UPDATED")) {
+ return AutoTimestampType.UPDATED;
+ } else {
+ return AutoTimestampType.CREATED;
+ }
+ }
+ } catch (Exception e) {
+ // If we can't read the value, default to CREATED
+ return AutoTimestampType.CREATED;
+ }
+ }
+ }
+ }
+ } catch (Exception ignored) {
+ // If reflection fails, return NONE
+ }
+
+ return AutoTimestampType.NONE;
+ }
+
+ /**
+ * Checks if a property has any auto-timestamp or auditing annotation.
+ *
+ * @param persistentProperty The persistent property to check
+ * @return true if the property has any supported annotation
(@CreatedDate, @LastModifiedDate,
+ * @CreatedBy, @LastModifiedBy, or @AutoTimestamp) from either
GORM or Spring Data
+ */
+ public static boolean hasAutoTimestampAnnotation(PersistentProperty<?>
persistentProperty) {
+ return persistentProperty != null &&
getAutoTimestampType(persistentProperty) != AutoTimestampType.NONE;
+ }
+
+ /**
+ * Checks if a property has a @CreatedDate annotation (GORM or Spring
Data) or @AutoTimestamp(CREATED).
+ *
+ * @param persistentProperty The persistent property to check
+ * @return true if the property represents a creation timestamp
+ */
+ public static boolean isCreatedTimestamp(PersistentProperty<?>
persistentProperty) {
+ return getAutoTimestampType(persistentProperty) ==
AutoTimestampType.CREATED;
+ }
+
+ /**
+ * Checks if a property has a @LastModifiedDate annotation (GORM or Spring
Data) or @AutoTimestamp(UPDATED).
+ *
+ * @param persistentProperty The persistent property to check
+ * @return true if the property represents an update timestamp
+ */
+ public static boolean isUpdatedTimestamp(PersistentProperty<?>
persistentProperty) {
+ return getAutoTimestampType(persistentProperty) ==
AutoTimestampType.UPDATED;
+ }
+
+ /**
+ * Checks if a property has a @CreatedBy annotation (GORM or Spring Data).
Review Comment:
```suggestion
* Checks if a property has a {@code @CreatedBy} annotation (GORM or
Spring Data).
```
##########
grails-datamapping-core/src/main/groovy/grails/gorm/annotation/LastModifiedBy.java:
##########
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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
+ *
+ * https://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 automatically populate a field with the
current auditor
+ * upon GORM insert and update events. The current auditor is retrieved from an
+ * {@link org.grails.datastore.gorm.timestamp.AuditorAware} bean registered in
the Spring application context.
+ *
+ * <p>Example usage:</p>
+ * <pre>
+ * class Book {
+ * @LastModifiedBy
+ * String lastModifiedBy
+ *
+ * @LastModifiedBy
+ * User lastModifier
+ *
+ * @LastModifiedBy
+ * Long lastModifierId
+ * }
+ * </pre>
+ *
+ * <p>The field type should match the type parameter of your {@link
org.grails.datastore.gorm.timestamp.AuditorAware}
+ * implementation (e.g., String, Long, User, etc.).</p>
+ *
+ * @author Scott Murphy Heiberg
+ * @since 7.0
Review Comment:
```suggestion
* @since 7.1
```
##########
grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/model/AutoTimestampUtils.java:
##########
@@ -0,0 +1,202 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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
+ *
+ * https://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 org.grails.datastore.mapping.model;
+
+import java.lang.reflect.Field;
+
+import org.springframework.util.ReflectionUtils;
+
+import grails.util.Environment;
+import org.grails.datastore.mapping.config.Property;
+import org.grails.datastore.mapping.config.Property.AutoTimestampType;
+
+/**
+ * Utility class for detecting and caching auto-timestamp and auditing
annotations on domain properties.
+ * This avoids repeated reflection calls by storing the annotation type in the
Property metadata.
+ *
+ * <p>Supports the following annotations (both GORM and Spring Data
variants):</p>
+ * <ul>
+ * <li>@CreatedDate / @grails.gorm.annotation.CreatedDate - automatically
set on insert</li>
+ * <li>@LastModifiedDate / @grails.gorm.annotation.LastModifiedDate -
automatically set on insert and update</li>
+ * <li>@CreatedBy / @grails.gorm.annotation.CreatedBy - automatically
populated with current auditor on insert</li>
+ * <li>@LastModifiedBy / @grails.gorm.annotation.LastModifiedBy -
automatically populated with current auditor on insert and update</li>
+ * <li>@AutoTimestamp - GORM-specific annotation for backwards
compatibility</li>
+ * </ul>
+ *
+ * <p>Caching is automatically disabled in development mode ({@link
Environment#isDevelopmentMode()})
+ * to ensure annotation changes are picked up during class reloading.</p>
+ *
+ * @author Scott Murphy Heiberg
+ * @since 7.0
+ */
+public class AutoTimestampUtils {
+
+ private static final String CREATED_DATE_ANNOTATION =
"grails.gorm.annotation.CreatedDate";
+ private static final String LAST_MODIFIED_DATE_ANNOTATION =
"grails.gorm.annotation.LastModifiedDate";
+ private static final String AUTO_TIMESTAMP_ANNOTATION =
"grails.gorm.annotation.AutoTimestamp";
+ private static final String CREATED_BY_ANNOTATION =
"grails.gorm.annotation.CreatedBy";
+ private static final String LAST_MODIFIED_BY_ANNOTATION =
"grails.gorm.annotation.LastModifiedBy";
+
+ private static final String CREATED_DATE_SPRING_ANNOTATION =
"org.springframework.data.annotation.CreatedDate";
+ private static final String LAST_MODIFIED_DATE_SPRING_ANNOTATION =
"org.springframework.data.annotation.LastModifiedDate";
+ private static final String CREATED_BY_SPRING_ANNOTATION =
"org.springframework.data.annotation.CreatedBy";
+ private static final String LAST_MODIFIED_BY_SPRING_ANNOTATION =
"org.springframework.data.annotation.LastModifiedBy";
+
+ /**
+ * Gets the auto-timestamp type for a persistent property, using cached
metadata when not in development mode.
+ *
+ * <p>In development mode, this method will always perform reflection to
detect the current
+ * annotation state, ensuring that annotation changes during class
reloading are immediately
+ * recognized. In production, the result is cached to avoid repeated
reflection calls.</p>
+ *
+ * @param persistentProperty The persistent property to check
+ * @return The auto-timestamp type (CREATED, UPDATED, or NONE)
+ */
+ public static AutoTimestampType getAutoTimestampType(PersistentProperty<?>
persistentProperty) {
+ Property mappedForm = persistentProperty.getMapping().getMappedForm();
+
+ // In development mode, always detect fresh to support class reloading
+ if (Environment.isDevelopmentMode()) {
+ return detectAutoTimestampType(persistentProperty);
+ }
+
+ // Return cached value if available
+ if (mappedForm.getAutoTimestampType() != null) {
+ return mappedForm.getAutoTimestampType();
+ }
+
+ // Detect and cache the annotation type
+ AutoTimestampType type = detectAutoTimestampType(persistentProperty);
+ mappedForm.setAutoTimestampType(type);
+ return type;
+ }
+
+ /**
+ * Detects the auto-timestamp annotation type on a property using
reflection.
+ *
+ * <p>When caching is enabled (production mode), this method is called
once per property
+ * and the result is cached. When caching is disabled (development mode),
this method
+ * is called on every access to ensure annotation changes are detected.</p>
+ *
+ * @param persistentProperty The persistent property to check
+ * @return The auto-timestamp type (CREATED, UPDATED, or NONE)
+ */
+ private static AutoTimestampType
detectAutoTimestampType(PersistentProperty<?> persistentProperty) {
+ try {
+ Field field = ReflectionUtils.findField(
+ persistentProperty.getOwner().getJavaClass(),
+ persistentProperty.getName()
+ );
+
+ if (field != null) {
+ for (java.lang.annotation.Annotation annotation :
field.getDeclaredAnnotations()) {
+ String annotationName =
annotation.annotationType().getName();
+
+ if (CREATED_DATE_ANNOTATION.equals(annotationName) ||
+ CREATED_DATE_SPRING_ANNOTATION.equals(annotationName))
{
+ return AutoTimestampType.CREATED;
+ } else if
(LAST_MODIFIED_DATE_ANNOTATION.equals(annotationName) ||
+
LAST_MODIFIED_DATE_SPRING_ANNOTATION.equals(annotationName)) {
+ return AutoTimestampType.UPDATED;
+ } else if (CREATED_BY_ANNOTATION.equals(annotationName) ||
+
CREATED_BY_SPRING_ANNOTATION.equals(annotationName)) {
+ return AutoTimestampType.CREATED_BY;
+ } else if
(LAST_MODIFIED_BY_ANNOTATION.equals(annotationName) ||
+
LAST_MODIFIED_BY_SPRING_ANNOTATION.equals(annotationName)) {
+ return AutoTimestampType.UPDATED_BY;
+ } else if
(AUTO_TIMESTAMP_ANNOTATION.equals(annotationName)) {
+ // For @AutoTimestamp, check the EventType value
+ try {
+ Object eventTypeValue = annotation.annotationType()
+ .getMethod("value")
+ .invoke(annotation);
+
+ if (eventTypeValue != null) {
+ String eventTypeName =
eventTypeValue.toString();
+ if (eventTypeName.equals("UPDATED")) {
+ return AutoTimestampType.UPDATED;
+ } else {
+ return AutoTimestampType.CREATED;
+ }
+ }
+ } catch (Exception e) {
+ // If we can't read the value, default to CREATED
+ return AutoTimestampType.CREATED;
+ }
+ }
+ }
Review Comment:
```suggestion
switch (annotation.annotationType().getName()) {
case CREATED_DATE_ANNOTATION,
CREATED_DATE_SPRING_ANNOTATION -> {
return AutoTimestampType.CREATED;
}
case LAST_MODIFIED_DATE_ANNOTATION,
LAST_MODIFIED_DATE_SPRING_ANNOTATION -> {
return AutoTimestampType.UPDATED;
}
case CREATED_BY_ANNOTATION,
CREATED_BY_SPRING_ANNOTATION -> {
return AutoTimestampType.CREATED_BY;
}
case LAST_MODIFIED_BY_ANNOTATION,
LAST_MODIFIED_BY_SPRING_ANNOTATION -> {
return AutoTimestampType.UPDATED_BY;
}
case AUTO_TIMESTAMP_ANNOTATION -> {
// For @AutoTimestamp, check the EventType value
try {
Object eventTypeValue =
annotation.annotationType()
.getMethod("value")
.invoke(annotation);
if (eventTypeValue != null) {
String eventTypeName =
eventTypeValue.toString();
if (eventTypeName.equals("UPDATED")) {
return AutoTimestampType.UPDATED;
} else {
return AutoTimestampType.CREATED;
}
}
} catch (Exception e) {
// If we can't read the value, default to
CREATED
return AutoTimestampType.CREATED;
}
}
}
```
##########
grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/model/AutoTimestampUtils.java:
##########
@@ -0,0 +1,202 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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
+ *
+ * https://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 org.grails.datastore.mapping.model;
+
+import java.lang.reflect.Field;
+
+import org.springframework.util.ReflectionUtils;
+
+import grails.util.Environment;
+import org.grails.datastore.mapping.config.Property;
+import org.grails.datastore.mapping.config.Property.AutoTimestampType;
+
+/**
+ * Utility class for detecting and caching auto-timestamp and auditing
annotations on domain properties.
+ * This avoids repeated reflection calls by storing the annotation type in the
Property metadata.
+ *
+ * <p>Supports the following annotations (both GORM and Spring Data
variants):</p>
+ * <ul>
+ * <li>@CreatedDate / @grails.gorm.annotation.CreatedDate - automatically
set on insert</li>
+ * <li>@LastModifiedDate / @grails.gorm.annotation.LastModifiedDate -
automatically set on insert and update</li>
+ * <li>@CreatedBy / @grails.gorm.annotation.CreatedBy - automatically
populated with current auditor on insert</li>
+ * <li>@LastModifiedBy / @grails.gorm.annotation.LastModifiedBy -
automatically populated with current auditor on insert and update</li>
+ * <li>@AutoTimestamp - GORM-specific annotation for backwards
compatibility</li>
+ * </ul>
+ *
+ * <p>Caching is automatically disabled in development mode ({@link
Environment#isDevelopmentMode()})
+ * to ensure annotation changes are picked up during class reloading.</p>
+ *
+ * @author Scott Murphy Heiberg
+ * @since 7.0
+ */
+public class AutoTimestampUtils {
+
+ private static final String CREATED_DATE_ANNOTATION =
"grails.gorm.annotation.CreatedDate";
+ private static final String LAST_MODIFIED_DATE_ANNOTATION =
"grails.gorm.annotation.LastModifiedDate";
+ private static final String AUTO_TIMESTAMP_ANNOTATION =
"grails.gorm.annotation.AutoTimestamp";
+ private static final String CREATED_BY_ANNOTATION =
"grails.gorm.annotation.CreatedBy";
+ private static final String LAST_MODIFIED_BY_ANNOTATION =
"grails.gorm.annotation.LastModifiedBy";
+
+ private static final String CREATED_DATE_SPRING_ANNOTATION =
"org.springframework.data.annotation.CreatedDate";
+ private static final String LAST_MODIFIED_DATE_SPRING_ANNOTATION =
"org.springframework.data.annotation.LastModifiedDate";
+ private static final String CREATED_BY_SPRING_ANNOTATION =
"org.springframework.data.annotation.CreatedBy";
+ private static final String LAST_MODIFIED_BY_SPRING_ANNOTATION =
"org.springframework.data.annotation.LastModifiedBy";
+
+ /**
+ * Gets the auto-timestamp type for a persistent property, using cached
metadata when not in development mode.
+ *
+ * <p>In development mode, this method will always perform reflection to
detect the current
+ * annotation state, ensuring that annotation changes during class
reloading are immediately
+ * recognized. In production, the result is cached to avoid repeated
reflection calls.</p>
+ *
+ * @param persistentProperty The persistent property to check
+ * @return The auto-timestamp type (CREATED, UPDATED, or NONE)
+ */
+ public static AutoTimestampType getAutoTimestampType(PersistentProperty<?>
persistentProperty) {
+ Property mappedForm = persistentProperty.getMapping().getMappedForm();
+
+ // In development mode, always detect fresh to support class reloading
+ if (Environment.isDevelopmentMode()) {
+ return detectAutoTimestampType(persistentProperty);
+ }
+
+ // Return cached value if available
+ if (mappedForm.getAutoTimestampType() != null) {
+ return mappedForm.getAutoTimestampType();
+ }
+
+ // Detect and cache the annotation type
+ AutoTimestampType type = detectAutoTimestampType(persistentProperty);
+ mappedForm.setAutoTimestampType(type);
+ return type;
+ }
+
+ /**
+ * Detects the auto-timestamp annotation type on a property using
reflection.
+ *
+ * <p>When caching is enabled (production mode), this method is called
once per property
+ * and the result is cached. When caching is disabled (development mode),
this method
+ * is called on every access to ensure annotation changes are detected.</p>
+ *
+ * @param persistentProperty The persistent property to check
+ * @return The auto-timestamp type (CREATED, UPDATED, or NONE)
+ */
+ private static AutoTimestampType
detectAutoTimestampType(PersistentProperty<?> persistentProperty) {
+ try {
+ Field field = ReflectionUtils.findField(
+ persistentProperty.getOwner().getJavaClass(),
+ persistentProperty.getName()
+ );
+
+ if (field != null) {
+ for (java.lang.annotation.Annotation annotation :
field.getDeclaredAnnotations()) {
+ String annotationName =
annotation.annotationType().getName();
+
+ if (CREATED_DATE_ANNOTATION.equals(annotationName) ||
+ CREATED_DATE_SPRING_ANNOTATION.equals(annotationName))
{
+ return AutoTimestampType.CREATED;
+ } else if
(LAST_MODIFIED_DATE_ANNOTATION.equals(annotationName) ||
+
LAST_MODIFIED_DATE_SPRING_ANNOTATION.equals(annotationName)) {
+ return AutoTimestampType.UPDATED;
+ } else if (CREATED_BY_ANNOTATION.equals(annotationName) ||
+
CREATED_BY_SPRING_ANNOTATION.equals(annotationName)) {
+ return AutoTimestampType.CREATED_BY;
+ } else if
(LAST_MODIFIED_BY_ANNOTATION.equals(annotationName) ||
+
LAST_MODIFIED_BY_SPRING_ANNOTATION.equals(annotationName)) {
+ return AutoTimestampType.UPDATED_BY;
+ } else if
(AUTO_TIMESTAMP_ANNOTATION.equals(annotationName)) {
+ // For @AutoTimestamp, check the EventType value
+ try {
+ Object eventTypeValue = annotation.annotationType()
+ .getMethod("value")
+ .invoke(annotation);
+
+ if (eventTypeValue != null) {
+ String eventTypeName =
eventTypeValue.toString();
+ if (eventTypeName.equals("UPDATED")) {
+ return AutoTimestampType.UPDATED;
+ } else {
+ return AutoTimestampType.CREATED;
+ }
+ }
+ } catch (Exception e) {
+ // If we can't read the value, default to CREATED
+ return AutoTimestampType.CREATED;
+ }
+ }
+ }
+ }
+ } catch (Exception ignored) {
+ // If reflection fails, return NONE
+ }
+
+ return AutoTimestampType.NONE;
+ }
+
+ /**
+ * Checks if a property has any auto-timestamp or auditing annotation.
+ *
+ * @param persistentProperty The persistent property to check
+ * @return true if the property has any supported annotation
(@CreatedDate, @LastModifiedDate,
+ * @CreatedBy, @LastModifiedBy, or @AutoTimestamp) from either
GORM or Spring Data
+ */
+ public static boolean hasAutoTimestampAnnotation(PersistentProperty<?>
persistentProperty) {
+ return persistentProperty != null &&
getAutoTimestampType(persistentProperty) != AutoTimestampType.NONE;
+ }
+
+ /**
+ * Checks if a property has a @CreatedDate annotation (GORM or Spring
Data) or @AutoTimestamp(CREATED).
+ *
+ * @param persistentProperty The persistent property to check
+ * @return true if the property represents a creation timestamp
+ */
+ public static boolean isCreatedTimestamp(PersistentProperty<?>
persistentProperty) {
+ return getAutoTimestampType(persistentProperty) ==
AutoTimestampType.CREATED;
+ }
+
+ /**
+ * Checks if a property has a @LastModifiedDate annotation (GORM or Spring
Data) or @AutoTimestamp(UPDATED).
+ *
+ * @param persistentProperty The persistent property to check
+ * @return true if the property represents an update timestamp
+ */
+ public static boolean isUpdatedTimestamp(PersistentProperty<?>
persistentProperty) {
+ return getAutoTimestampType(persistentProperty) ==
AutoTimestampType.UPDATED;
+ }
+
+ /**
+ * Checks if a property has a @CreatedBy annotation (GORM or Spring Data).
+ *
+ * @param persistentProperty The persistent property to check
+ * @return true if the property represents a creation auditor
+ */
+ public static boolean isCreatedBy(PersistentProperty<?>
persistentProperty) {
+ return getAutoTimestampType(persistentProperty) ==
AutoTimestampType.CREATED_BY;
+ }
+
+ /**
+ * Checks if a property has a @LastModifiedBy annotation (GORM or Spring
Data).
Review Comment:
```suggestion
* Checks if a property has a {@code @LastModifiedBy} annotation (GORM
or Spring Data).
```
##########
grails-datamapping-validation/src/main/groovy/org/grails/datastore/gorm/validation/constraints/eval/DefaultConstraintEvaluator.java:
##########
@@ -305,6 +306,11 @@ protected boolean
isConstrainableProperty(PersistentProperty persistentProperty,
return NameUtils.isNotConfigurational(propertyName);
}
else {
+ // Check if property has @CreatedDate or @LastModifiedDate
annotations
Review Comment:
The condition below returns true for all "timestamp" annotations?
##########
grails-datamapping-validation/src/main/groovy/org/grails/datastore/gorm/validation/constraints/eval/DefaultConstraintEvaluator.java:
##########
@@ -305,6 +306,11 @@ protected boolean
isConstrainableProperty(PersistentProperty persistentProperty,
return NameUtils.isNotConfigurational(propertyName);
}
else {
+ // Check if property has @CreatedDate or @LastModifiedDate
annotations
+ if
(AutoTimestampUtils.hasAutoTimestampAnnotation(persistentProperty)) {
Review Comment:
This is true, as auditor can be different types, and constraints influence
the database schema.
##########
grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/model/AutoTimestampUtils.java:
##########
@@ -0,0 +1,202 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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
+ *
+ * https://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 org.grails.datastore.mapping.model;
+
+import java.lang.reflect.Field;
+
+import org.springframework.util.ReflectionUtils;
+
+import grails.util.Environment;
+import org.grails.datastore.mapping.config.Property;
+import org.grails.datastore.mapping.config.Property.AutoTimestampType;
+
+/**
+ * Utility class for detecting and caching auto-timestamp and auditing
annotations on domain properties.
+ * This avoids repeated reflection calls by storing the annotation type in the
Property metadata.
+ *
+ * <p>Supports the following annotations (both GORM and Spring Data
variants):</p>
+ * <ul>
+ * <li>@CreatedDate / @grails.gorm.annotation.CreatedDate - automatically
set on insert</li>
+ * <li>@LastModifiedDate / @grails.gorm.annotation.LastModifiedDate -
automatically set on insert and update</li>
+ * <li>@CreatedBy / @grails.gorm.annotation.CreatedBy - automatically
populated with current auditor on insert</li>
+ * <li>@LastModifiedBy / @grails.gorm.annotation.LastModifiedBy -
automatically populated with current auditor on insert and update</li>
+ * <li>@AutoTimestamp - GORM-specific annotation for backwards
compatibility</li>
+ * </ul>
+ *
+ * <p>Caching is automatically disabled in development mode ({@link
Environment#isDevelopmentMode()})
+ * to ensure annotation changes are picked up during class reloading.</p>
+ *
+ * @author Scott Murphy Heiberg
+ * @since 7.0
+ */
+public class AutoTimestampUtils {
+
+ private static final String CREATED_DATE_ANNOTATION =
"grails.gorm.annotation.CreatedDate";
+ private static final String LAST_MODIFIED_DATE_ANNOTATION =
"grails.gorm.annotation.LastModifiedDate";
+ private static final String AUTO_TIMESTAMP_ANNOTATION =
"grails.gorm.annotation.AutoTimestamp";
+ private static final String CREATED_BY_ANNOTATION =
"grails.gorm.annotation.CreatedBy";
+ private static final String LAST_MODIFIED_BY_ANNOTATION =
"grails.gorm.annotation.LastModifiedBy";
+
+ private static final String CREATED_DATE_SPRING_ANNOTATION =
"org.springframework.data.annotation.CreatedDate";
+ private static final String LAST_MODIFIED_DATE_SPRING_ANNOTATION =
"org.springframework.data.annotation.LastModifiedDate";
+ private static final String CREATED_BY_SPRING_ANNOTATION =
"org.springframework.data.annotation.CreatedBy";
+ private static final String LAST_MODIFIED_BY_SPRING_ANNOTATION =
"org.springframework.data.annotation.LastModifiedBy";
+
+ /**
+ * Gets the auto-timestamp type for a persistent property, using cached
metadata when not in development mode.
+ *
+ * <p>In development mode, this method will always perform reflection to
detect the current
+ * annotation state, ensuring that annotation changes during class
reloading are immediately
+ * recognized. In production, the result is cached to avoid repeated
reflection calls.</p>
+ *
+ * @param persistentProperty The persistent property to check
+ * @return The auto-timestamp type (CREATED, UPDATED, or NONE)
+ */
+ public static AutoTimestampType getAutoTimestampType(PersistentProperty<?>
persistentProperty) {
+ Property mappedForm = persistentProperty.getMapping().getMappedForm();
+
+ // In development mode, always detect fresh to support class reloading
+ if (Environment.isDevelopmentMode()) {
+ return detectAutoTimestampType(persistentProperty);
+ }
+
+ // Return cached value if available
+ if (mappedForm.getAutoTimestampType() != null) {
+ return mappedForm.getAutoTimestampType();
+ }
+
+ // Detect and cache the annotation type
+ AutoTimestampType type = detectAutoTimestampType(persistentProperty);
+ mappedForm.setAutoTimestampType(type);
+ return type;
+ }
+
+ /**
+ * Detects the auto-timestamp annotation type on a property using
reflection.
+ *
+ * <p>When caching is enabled (production mode), this method is called
once per property
+ * and the result is cached. When caching is disabled (development mode),
this method
+ * is called on every access to ensure annotation changes are detected.</p>
+ *
+ * @param persistentProperty The persistent property to check
+ * @return The auto-timestamp type (CREATED, UPDATED, or NONE)
+ */
+ private static AutoTimestampType
detectAutoTimestampType(PersistentProperty<?> persistentProperty) {
+ try {
+ Field field = ReflectionUtils.findField(
+ persistentProperty.getOwner().getJavaClass(),
+ persistentProperty.getName()
+ );
+
+ if (field != null) {
+ for (java.lang.annotation.Annotation annotation :
field.getDeclaredAnnotations()) {
+ String annotationName =
annotation.annotationType().getName();
+
+ if (CREATED_DATE_ANNOTATION.equals(annotationName) ||
+ CREATED_DATE_SPRING_ANNOTATION.equals(annotationName))
{
+ return AutoTimestampType.CREATED;
+ } else if
(LAST_MODIFIED_DATE_ANNOTATION.equals(annotationName) ||
+
LAST_MODIFIED_DATE_SPRING_ANNOTATION.equals(annotationName)) {
+ return AutoTimestampType.UPDATED;
+ } else if (CREATED_BY_ANNOTATION.equals(annotationName) ||
+
CREATED_BY_SPRING_ANNOTATION.equals(annotationName)) {
+ return AutoTimestampType.CREATED_BY;
+ } else if
(LAST_MODIFIED_BY_ANNOTATION.equals(annotationName) ||
+
LAST_MODIFIED_BY_SPRING_ANNOTATION.equals(annotationName)) {
+ return AutoTimestampType.UPDATED_BY;
+ } else if
(AUTO_TIMESTAMP_ANNOTATION.equals(annotationName)) {
+ // For @AutoTimestamp, check the EventType value
+ try {
+ Object eventTypeValue = annotation.annotationType()
+ .getMethod("value")
+ .invoke(annotation);
+
+ if (eventTypeValue != null) {
+ String eventTypeName =
eventTypeValue.toString();
+ if (eventTypeName.equals("UPDATED")) {
+ return AutoTimestampType.UPDATED;
+ } else {
+ return AutoTimestampType.CREATED;
+ }
+ }
+ } catch (Exception e) {
+ // If we can't read the value, default to CREATED
+ return AutoTimestampType.CREATED;
+ }
+ }
+ }
+ }
+ } catch (Exception ignored) {
+ // If reflection fails, return NONE
+ }
+
+ return AutoTimestampType.NONE;
+ }
+
+ /**
+ * Checks if a property has any auto-timestamp or auditing annotation.
+ *
+ * @param persistentProperty The persistent property to check
+ * @return true if the property has any supported annotation
(@CreatedDate, @LastModifiedDate,
+ * @CreatedBy, @LastModifiedBy, or @AutoTimestamp) from either
GORM or Spring Data
+ */
+ public static boolean hasAutoTimestampAnnotation(PersistentProperty<?>
persistentProperty) {
+ return persistentProperty != null &&
getAutoTimestampType(persistentProperty) != AutoTimestampType.NONE;
+ }
+
+ /**
+ * Checks if a property has a @CreatedDate annotation (GORM or Spring
Data) or @AutoTimestamp(CREATED).
+ *
+ * @param persistentProperty The persistent property to check
+ * @return true if the property represents a creation timestamp
+ */
+ public static boolean isCreatedTimestamp(PersistentProperty<?>
persistentProperty) {
+ return getAutoTimestampType(persistentProperty) ==
AutoTimestampType.CREATED;
+ }
+
+ /**
+ * Checks if a property has a @LastModifiedDate annotation (GORM or Spring
Data) or @AutoTimestamp(UPDATED).
Review Comment:
```suggestion
* Checks if a property has a {@code @LastModifiedDate} annotation (GORM
or Spring Data) or {@code @AutoTimestamp(UPDATED)}.
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]