jdaugherty commented on code in PR #15118:
URL: https://github.com/apache/grails-core/pull/15118#discussion_r2515714866


##########
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>{@code
+ * @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:
   This is still showing 7.0



##########
grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/model/AutoTimestampUtils.java:
##########
@@ -0,0 +1,172 @@
+/*
+ *  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 org.grails.datastore.mapping.config.AuditMetadataType;
+import org.grails.datastore.mapping.config.Property;
+
+/**
+ * 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>{@code @CreatedDate} / {@code @grails.gorm.annotation.CreatedDate} - 
automatically set on insert</li>
+ *   <li>{@code @LastModifiedDate} / {@code 
@grails.gorm.annotation.LastModifiedDate} - automatically set on insert and 
update</li>
+ *   <li>{@code @CreatedBy} / {@code @grails.gorm.annotation.CreatedBy} - 
automatically populated with current auditor on insert</li>
+ *   <li>{@code @LastModifiedBy} / {@code 
@grails.gorm.annotation.LastModifiedBy} - automatically populated with current 
auditor on insert and update</li>
+ *   <li>{@code @AutoTimestamp} - GORM-specific annotation for backwards 
compatibility</li>
+ * </ul>
+ *
+ * <p>Caching behavior is controlled by the {@code 
grails.gorm.events.autoTimestampCacheAnnotations}
+ * configuration property. When disabled (typically in development mode), 
annotation changes during
+ * class reloading are immediately recognized. When enabled (production mode), 
annotations are cached
+ * for optimal performance.</p>
+ *
+ * @author Scott Murphy Heiberg
+ * @since 7.1
+ */
+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 audit metadata type for a persistent property, using cached 
metadata when caching is enabled.
+     *
+     * <p>When caching is disabled (typically 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. When caching is enabled 
(production mode), the
+     * result is cached to avoid repeated reflection calls.</p>
+     *
+     * @param persistentProperty The persistent property to check
+     * @param cacheAnnotations Whether to cache the annotation metadata
+     * @return The audit metadata type (CREATED, UPDATED, CREATED_BY, 
UPDATED_BY, or NONE)
+     */
+    public static AuditMetadataType getAuditMetadataType(PersistentProperty<?> 
persistentProperty, boolean cacheAnnotations) {
+        Property mappedForm = persistentProperty.getMapping().getMappedForm();
+
+        // If caching is disabled, always detect fresh to support class 
reloading
+        if (!cacheAnnotations) {
+            return detectAuditMetadataType(persistentProperty);
+        }
+
+        // Return cached value if available
+        if (mappedForm.getAuditMetadataType() != null) {
+            return mappedForm.getAuditMetadataType();
+        }
+
+        // Detect and cache the annotation type
+        AuditMetadataType type = detectAuditMetadataType(persistentProperty);
+        mappedForm.setAuditMetadataType(type);
+        return type;
+    }
+
+    /**
+     * Detects the audit metadata 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 audit metadata type (CREATED, UPDATED, CREATED_BY, 
UPDATED_BY, or NONE)
+     */
+    private static AuditMetadataType 
detectAuditMetadataType(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();
+
+                    switch (annotationName) {
+                        case CREATED_DATE_ANNOTATION:
+                        case CREATED_DATE_SPRING_ANNOTATION:
+                            return AuditMetadataType.CREATED;
+
+                        case LAST_MODIFIED_DATE_ANNOTATION:
+                        case LAST_MODIFIED_DATE_SPRING_ANNOTATION:
+                            return AuditMetadataType.UPDATED;
+
+                        case CREATED_BY_ANNOTATION:
+                        case CREATED_BY_SPRING_ANNOTATION:
+                            return AuditMetadataType.CREATED_BY;
+
+                        case LAST_MODIFIED_BY_ANNOTATION:
+                        case LAST_MODIFIED_BY_SPRING_ANNOTATION:
+                            return AuditMetadataType.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 AuditMetadataType.UPDATED;
+                                    } else {
+                                        return AuditMetadataType.CREATED;
+                                    }
+                                }
+                            } catch (Exception e) {
+                                // If we can't read the value, default to 
CREATED
+                                return AuditMetadataType.CREATED;
+                            }
+                            break;
+                    }
+                }
+            }
+        } catch (Exception ignored) {
+            // If reflection fails, return NONE

Review Comment:
   Would it be useful to add log.debug for these ignored exceptions?  



-- 
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]

Reply via email to