This is an automated email from the ASF dual-hosted git repository.

borinquenkid pushed a commit to branch 8.0.x-hibernate7
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit cc7959f1406dfe91e7bd91049b537e2cfe0389d7
Author: Walter Duque de Estrada <[email protected]>
AuthorDate: Mon Feb 23 15:50:12 2026 -0600

    Removed deprecated methods in ClosureEventTriggeringInterceptor
---
 grails-data-hibernate7/REMOVAL_WARNINGS.md         |  27 --
 .../support/ClosureEventTriggeringInterceptor.java |  33 +-
 .../ClosureEventTriggeringInterceptorSpec.groovy   | 382 +++++++++++++++++++++
 3 files changed, 401 insertions(+), 41 deletions(-)

diff --git a/grails-data-hibernate7/REMOVAL_WARNINGS.md 
b/grails-data-hibernate7/REMOVAL_WARNINGS.md
deleted file mode 100644
index 5af6e42ed2..0000000000
--- a/grails-data-hibernate7/REMOVAL_WARNINGS.md
+++ /dev/null
@@ -1,27 +0,0 @@
-# Removal Deprecation Warnings — grails-data-hibernate7-core
-
-Warnings collected by compiling `grails-data-hibernate7-core` with 
`-Xlint:removal`.  
-These APIs are **marked for removal** in a future Hibernate / JDK release and 
must be migrated.
-
-Generated from: Hibernate `7.1.11.Final`
-
----
-
-| Fully Qualified Class | Line | Warning |
-|---|---|---|
-| `org.grails.orm.hibernate.support.ClosureEventTriggeringInterceptor` | 286 | 
`EntityMetamodel in org.hibernate.tuple.entity` has been deprecated and marked 
for removal |
-| `org.grails.orm.hibernate.support.ClosureEventTriggeringInterceptor` | 305 | 
`EntityMetamodel in org.hibernate.tuple.entity` has been deprecated and marked 
for removal |
-
----
-
-## Summary by API
-
-| Deprecated API | Affected Classes | Occurrences |
-|---|---|---|
-| `LockOptions` (`org.hibernate`) | `GrailsHibernateTemplate` | 3 |
-| `Session.get(Class,Object)` / `get(Class,Object,LockOptions)` | 
`GrailsHibernateTemplate` | 2 |
-| `Session.refresh(Object,LockOptions)` | `GrailsHibernateTemplate` | 1 |
-| `SchemaAutoTooling` (`org.hibernate.boot`) | `HibernateDatastore` | 4 |
-| `IdentifierGenerator.configure(Type,Properties,ServiceRegistry)` | 
`GrailsIncrementGenerator` | 1 |
-| `KeyValue/SimpleValue.createGenerator(Dialect,RootClass)` | `GrailsOneToOne` 
| 2 |
-| `EntityMetamodel` (`org.hibernate.tuple.entity`) | `ClosureEventListener`, 
`ClosureEventTriggeringInterceptor` | 3 |
diff --git 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/ClosureEventTriggeringInterceptor.java
 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/ClosureEventTriggeringInterceptor.java
index 13242871e3..ad107e5722 100644
--- 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/ClosureEventTriggeringInterceptor.java
+++ 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/support/ClosureEventTriggeringInterceptor.java
@@ -49,8 +49,9 @@ import org.hibernate.event.spi.PreInsertEvent;
 import org.hibernate.event.spi.PreLoadEvent;
 import org.hibernate.event.spi.PreUpdateEvent;
 import org.hibernate.jpa.event.spi.CallbackRegistry;
+import org.hibernate.metamodel.mapping.AttributeMapping;
+import org.hibernate.metamodel.mapping.EntityMappingType;
 import org.hibernate.persister.entity.EntityPersister;
-import org.hibernate.tuple.entity.EntityMetamodel;
 import org.springframework.beans.BeansException;
 import org.springframework.context.ApplicationContext;
 import org.springframework.context.ConfigurableApplicationContext;
@@ -283,30 +284,34 @@ public class ClosureEventTriggeringInterceptor extends 
AbstractClosureEventTrigg
   private void updateModifiedPropertiesWithAutoTimestamp(
       Map<String, Object> modifiedProperties, PreUpdateEvent hibernateEvent) {
 
-    EntityMetamodel entityMetamodel = 
hibernateEvent.getPersister().getEntityMetamodel();
-    Integer dateCreatedIdx =
-        
entityMetamodel.getPropertyIndexOrNull(AutoTimestampEventListener.DATE_CREATED_PROPERTY);
+
+    EntityPersister persister = hibernateEvent.getPersister();
+    EntityMappingType entityMappingType = persister.getEntityMappingType();
+    AttributeMapping dateCreatedMapping =
+        
entityMappingType.findAttributeMapping(AutoTimestampEventListener.DATE_CREATED_PROPERTY);
 
     Object[] oldState = hibernateEvent.getOldState();
     Object[] state = hibernateEvent.getState();
 
     // Only for "dateCreated" property, "lastUpdated" is handled correctly
-    if (dateCreatedIdx != null
-        && oldState != null
-        && oldState[dateCreatedIdx] != null
-        && !oldState[dateCreatedIdx].equals(state[dateCreatedIdx])) {
-      modifiedProperties.put(
-          AutoTimestampEventListener.DATE_CREATED_PROPERTY, 
oldState[dateCreatedIdx]);
+    if (dateCreatedMapping != null) {
+      int dateCreatedIdx = dateCreatedMapping.getStateArrayPosition();
+      if (oldState != null
+          && oldState[dateCreatedIdx] != null
+          && !oldState[dateCreatedIdx].equals(state[dateCreatedIdx])) {
+        modifiedProperties.put(
+            AutoTimestampEventListener.DATE_CREATED_PROPERTY, 
oldState[dateCreatedIdx]);
+      }
     }
   }
 
   private void synchronizeHibernateState(
       EntityPersister persister, Object[] state, Map<String, Object> 
modifiedProperties) {
-    EntityMetamodel entityMetamodel = persister.getEntityMetamodel();
+    EntityMappingType entityMappingType = persister.getEntityMappingType();
     for (Map.Entry<String, Object> entry : modifiedProperties.entrySet()) {
-      Integer index = entityMetamodel.getPropertyIndexOrNull(entry.getKey());
-      if (index != null) {
-        state[index] = entry.getValue();
+      AttributeMapping attributeMapping = 
entityMappingType.findAttributeMapping(entry.getKey());
+      if (attributeMapping != null) {
+        state[attributeMapping.getStateArrayPosition()] = entry.getValue();
       }
     }
   }
diff --git 
a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/support/ClosureEventTriggeringInterceptorSpec.groovy
 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/support/ClosureEventTriggeringInterceptorSpec.groovy
new file mode 100644
index 0000000000..91e11b72f8
--- /dev/null
+++ 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/support/ClosureEventTriggeringInterceptorSpec.groovy
@@ -0,0 +1,382 @@
+/*
+ * 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.orm.hibernate.support
+
+import grails.gorm.annotation.Entity
+import grails.gorm.hibernate.HibernateEntity
+import grails.gorm.specs.HibernateGormDatastoreSpec
+import grails.gorm.transactions.Rollback
+import org.grails.datastore.gorm.events.ConfigurableApplicationEventPublisher
+import org.grails.datastore.mapping.core.Datastore
+import org.grails.datastore.mapping.engine.event.AbstractPersistenceEvent
+import 
org.grails.datastore.mapping.engine.event.AbstractPersistenceEventListener
+import org.grails.datastore.mapping.engine.event.PostDeleteEvent
+import org.grails.datastore.mapping.engine.event.PostInsertEvent
+import org.grails.datastore.mapping.engine.event.PostLoadEvent
+import org.grails.datastore.mapping.engine.event.PostUpdateEvent
+import org.grails.datastore.mapping.engine.event.PreDeleteEvent
+import org.grails.datastore.mapping.engine.event.PreInsertEvent
+import org.grails.datastore.mapping.engine.event.PreLoadEvent
+import org.grails.datastore.mapping.engine.event.PreUpdateEvent
+import org.hibernate.engine.spi.SessionFactoryImplementor
+import org.hibernate.event.service.spi.EventListenerRegistry
+import org.hibernate.event.spi.EventType
+import org.springframework.context.ApplicationEvent
+
+/**
+ * Integration tests for {@link ClosureEventTriggeringInterceptor}.
+ *
+ * The interceptor bridges Hibernate's native event system to GORM's 
Spring-based
+ * ApplicationEvent infrastructure.  Each test registers a capturing listener 
on the
+ * datastore's event publisher so we can assert which GORM events are fired 
and that
+ * state mutations made inside a Pre* listener are synchronised back into 
Hibernate's
+ * state array (and therefore persisted).
+ */
+class ClosureEventTriggeringInterceptorSpec extends HibernateGormDatastoreSpec 
{
+
+    @Override
+    void setupSpec() {
+        manager.addAllDomainClasses([
+            InterceptorBook,
+            TimestampedBook,
+        ])
+    }
+
+    // 
-------------------------------------------------------------------------
+    // Helper: add a capturing listener for the duration of one test
+    // 
-------------------------------------------------------------------------
+
+    private CapturingListener addCapturingListener() {
+        def listener = new CapturingListener(datastore)
+        ((ConfigurableApplicationEventPublisher) 
datastore.applicationEventPublisher)
+            .addApplicationListener(listener)
+        listener
+    }
+
+    // 
-------------------------------------------------------------------------
+    // Interceptor is wired into the Hibernate event listener registry
+    // 
-------------------------------------------------------------------------
+
+    void "ClosureEventTriggeringInterceptor is registered for PRE_INSERT in 
the Hibernate registry"() {
+        given:
+        def sfi = sessionFactory.unwrap(SessionFactoryImplementor)
+        def registry = sfi.serviceRegistry.getService(EventListenerRegistry)
+
+        expect:
+        registry.getEventListenerGroup(EventType.PRE_INSERT)
+                .listeners()
+                .any { it instanceof ClosureEventTriggeringInterceptor }
+    }
+
+    void "ClosureEventTriggeringInterceptor is registered for PRE_UPDATE, 
PRE_DELETE, POST_INSERT, POST_UPDATE, POST_DELETE, PRE_LOAD, POST_LOAD"() {
+        given:
+        def sfi = sessionFactory.unwrap(SessionFactoryImplementor)
+        def registry = sfi.serviceRegistry.getService(EventListenerRegistry)
+
+        expect: "all 8 lifecycle event types carry the interceptor"
+        [
+            EventType.PRE_UPDATE, EventType.PRE_DELETE,
+            EventType.POST_INSERT, EventType.POST_UPDATE, 
EventType.POST_DELETE,
+            EventType.PRE_LOAD, EventType.POST_LOAD,
+        ].every { type ->
+            registry.getEventListenerGroup(type)
+                    .listeners()
+                    .any { it instanceof ClosureEventTriggeringInterceptor }
+        }
+    }
+
+    // 
-------------------------------------------------------------------------
+    // requiresPostCommitHandling
+    // 
-------------------------------------------------------------------------
+
+    void "requiresPostCommitHandling returns false"() {
+        given:
+        def sfi = sessionFactory.unwrap(SessionFactoryImplementor)
+        def registry = sfi.serviceRegistry.getService(EventListenerRegistry)
+        def interceptor = registry.getEventListenerGroup(EventType.PRE_INSERT)
+                .listeners()
+                .find { it instanceof ClosureEventTriggeringInterceptor } as 
ClosureEventTriggeringInterceptor
+
+        expect:
+        !interceptor.requiresPostCommitHandling(null)
+    }
+
+    // 
-------------------------------------------------------------------------
+    // setDatastore – mappingContext wired
+    // 
-------------------------------------------------------------------------
+
+    void "interceptor has a non-null mappingContext after setDatastore"() {
+        given:
+        def interceptor = new ClosureEventTriggeringInterceptor()
+        interceptor.setDatastore(datastore)
+
+        expect:
+        interceptor.@mappingContext != null
+        interceptor.@proxyHandler != null
+    }
+
+    // 
-------------------------------------------------------------------------
+    // Event publishing – each lifecycle publishes the right GORM event type
+    // 
-------------------------------------------------------------------------
+
+    @Rollback
+    void "saving an entity fires PreInsertEvent then PostInsertEvent"() {
+        given:
+        def listener = addCapturingListener()
+
+        when:
+        new InterceptorBook(title: "Clean Code").save(flush: true, 
failOnError: true)
+
+        then:
+        listener.eventTypes.contains(PreInsertEvent)
+        listener.eventTypes.contains(PostInsertEvent)
+        listener.eventTypes.indexOf(PreInsertEvent) < 
listener.eventTypes.indexOf(PostInsertEvent)
+    }
+
+    @Rollback
+    void "updating an entity fires PreUpdateEvent then PostUpdateEvent"() {
+        given:
+        def book = new InterceptorBook(title: "First").save(flush: true, 
failOnError: true)
+        def listener = addCapturingListener()
+
+        when:
+        book.title = "Second"
+        book.save(flush: true, failOnError: true)
+
+        then:
+        listener.eventTypes.contains(PreUpdateEvent)
+        listener.eventTypes.contains(PostUpdateEvent)
+        listener.eventTypes.indexOf(PreUpdateEvent) < 
listener.eventTypes.indexOf(PostUpdateEvent)
+    }
+
+    @Rollback
+    void "deleting an entity fires PreDeleteEvent then PostDeleteEvent"() {
+        given:
+        def book = new InterceptorBook(title: "Ephemeral").save(flush: true, 
failOnError: true)
+        def listener = addCapturingListener()
+
+        when:
+        book.delete(flush: true)
+
+        then:
+        listener.eventTypes.contains(PreDeleteEvent)
+        listener.eventTypes.contains(PostDeleteEvent)
+        listener.eventTypes.indexOf(PreDeleteEvent) < 
listener.eventTypes.indexOf(PostDeleteEvent)
+    }
+
+    @Rollback
+    void "loading an entity fires PreLoadEvent then PostLoadEvent"() {
+        given:
+        def book = new InterceptorBook(title: "Loaded").save(flush: true, 
failOnError: true)
+        session.clear()
+        def listener = addCapturingListener()
+
+        when:
+        InterceptorBook.get(book.id)
+
+        then:
+        listener.eventTypes.contains(PreLoadEvent)
+        listener.eventTypes.contains(PostLoadEvent)
+        listener.eventTypes.indexOf(PreLoadEvent) < 
listener.eventTypes.indexOf(PostLoadEvent)
+    }
+
+    // 
-------------------------------------------------------------------------
+    // State synchronisation – mutations via entityAccess are persisted
+    // 
-------------------------------------------------------------------------
+
+    @Rollback
+    void "property set via entityAccess in a PreInsertEvent listener is 
written to the database"() {
+        given:
+        ((ConfigurableApplicationEventPublisher) 
datastore.applicationEventPublisher)
+            .addApplicationListener(new UpperCaseTitleListener(datastore, 
PreInsertEvent))
+
+        when:
+        def book = new InterceptorBook(title: "lower case").save(flush: true, 
failOnError: true)
+        session.clear()
+
+        then:
+        InterceptorBook.get(book.id).title == "LOWER CASE"
+    }
+
+    @Rollback
+    void "property set via entityAccess in a PreUpdateEvent listener is 
written to the database"() {
+        given:
+        def book = new InterceptorBook(title: "original").save(flush: true, 
failOnError: true)
+        session.clear()
+        ((ConfigurableApplicationEventPublisher) 
datastore.applicationEventPublisher)
+            .addApplicationListener(new UpperCaseTitleListener(datastore, 
PreUpdateEvent))
+
+        when:
+        def loaded = InterceptorBook.get(book.id)
+        loaded.title = "updated"
+        loaded.save(flush: true, failOnError: true)
+        session.clear()
+
+        then:
+        InterceptorBook.get(book.id).title == "UPDATED"
+    }
+
+    // 
-------------------------------------------------------------------------
+    // Dirty checking is activated after PostLoadEvent
+    // 
-------------------------------------------------------------------------
+
+    @Rollback
+    void "entity loaded from database has dirty checking activated"() {
+        given:
+        def book = new InterceptorBook(title: "Track Me").save(flush: true, 
failOnError: true)
+        session.clear()
+
+        when:
+        def loaded = InterceptorBook.get(book.id)
+
+        then: "the loaded entity implements DirtyCheckable and is tracking 
changes"
+        loaded instanceof 
org.grails.datastore.mapping.dirty.checking.DirtyCheckable
+        ((org.grails.datastore.mapping.dirty.checking.DirtyCheckable) loaded)
+            .listDirtyPropertyNames() != null
+    }
+
+    // 
-------------------------------------------------------------------------
+    // Auto-timestamp: dateCreated is preserved on update
+    // 
-------------------------------------------------------------------------
+
+    @Rollback
+    void "dateCreated is not overwritten when the entity is updated"() {
+        given:
+        def book = new TimestampedBook(title: "Original").save(flush: true, 
failOnError: true)
+        Date originalDateCreated = book.dateCreated
+        session.clear()
+
+        when:
+        def loaded = TimestampedBook.get(book.id)
+        loaded.title = "Updated"
+        loaded.save(flush: true, failOnError: true)
+        session.clear()
+
+        then:
+        def reloaded = TimestampedBook.get(book.id)
+        reloaded.dateCreated != null
+        reloaded.dateCreated == originalDateCreated
+    }
+
+    // 
-------------------------------------------------------------------------
+    // PreInsertEvent carries a valid entity access
+    // 
-------------------------------------------------------------------------
+
+    @Rollback
+    void "PreInsertEvent provides a non-null entityAccess for mapped 
entities"() {
+        given:
+        def captured = []
+        ((ConfigurableApplicationEventPublisher) 
datastore.applicationEventPublisher)
+            .addApplicationListener(new 
AbstractPersistenceEventListener(datastore) {
+                @Override
+                protected void onPersistenceEvent(AbstractPersistenceEvent 
event) {
+                    if (event instanceof PreInsertEvent && event.entityAccess 
!= null) {
+                        captured << event.entityObject
+                    }
+                }
+                @Override
+                boolean supportsEventType(Class<? extends ApplicationEvent> t) 
{
+                    t == PreInsertEvent
+                }
+            })
+
+        when:
+        new InterceptorBook(title: "Access Check").save(flush: true, 
failOnError: true)
+
+        then:
+        !captured.isEmpty()
+        captured[0] instanceof InterceptorBook
+    }
+}
+
+// ---------------------------------------------------------------------------
+// Domain classes
+// ---------------------------------------------------------------------------
+
+@Entity
+class InterceptorBook implements HibernateEntity<InterceptorBook> {
+    String title
+
+    static mapping = {
+        id generator: 'identity'
+    }
+}
+
+@Entity
+class TimestampedBook implements HibernateEntity<TimestampedBook> {
+    String title
+    Date dateCreated
+    Date lastUpdated
+
+    static mapping = {
+        id generator: 'identity'
+    }
+}
+
+// ---------------------------------------------------------------------------
+// Helper listeners
+// ---------------------------------------------------------------------------
+
+/**
+ * Records the Class of every GORM event it receives, in order.
+ */
+class CapturingListener extends AbstractPersistenceEventListener {
+    final List<Class<?>> eventTypes = [].asSynchronized() as List<Class<?>>
+
+    CapturingListener(Datastore datastore) {
+        super(datastore)
+    }
+
+    @Override
+    protected void onPersistenceEvent(AbstractPersistenceEvent event) {
+        eventTypes << event.class
+    }
+
+    @Override
+    boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
+        AbstractPersistenceEvent.isAssignableFrom(eventType)
+    }
+}
+
+/**
+ * Upper-cases the title property via entityAccess in a Pre* event.
+ */
+class UpperCaseTitleListener extends AbstractPersistenceEventListener {
+    private final Class<?> targetEventType
+
+    UpperCaseTitleListener(Datastore datastore, Class<?> targetEventType) {
+        super(datastore)
+        this.targetEventType = targetEventType
+    }
+
+    @Override
+    protected void onPersistenceEvent(AbstractPersistenceEvent event) {
+        if (event.entityAccess != null) {
+            String title = event.entityAccess.getProperty("title") as String
+            if (title) {
+                event.entityAccess.setProperty("title", title.toUpperCase())
+            }
+        }
+    }
+
+    @Override
+    boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
+        targetEventType.isAssignableFrom(eventType)
+    }
+}

Reply via email to