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]
