diff --git a/hibernate-models-orm/src/main/java/org/hibernate/models/orm/xml/internal/ManagedTypeProcessor.java b/hibernate-models-orm/src/main/java/org/hibernate/models/orm/xml/internal/ManagedTypeProcessor.java index f566618..5ac1f14 100644 --- a/hibernate-models-orm/src/main/java/org/hibernate/models/orm/xml/internal/ManagedTypeProcessor.java +++ b/hibernate-models-orm/src/main/java/org/hibernate/models/orm/xml/internal/ManagedTypeProcessor.java @@ -455,10 +455,8 @@ private static void processEntityMetadata( sourceModelBuildingContext ) ); - processCommonEntityOrMappedSuperclass( jaxbEntity, classDetails, sourceModelBuildingContext ); + processEntityOrMappedSuperclass( jaxbEntity, classDetails, sourceModelBuildingContext ); - // todo : callbacks - // todo : entity-listeners // todo : secondary-tables } @@ -581,10 +579,7 @@ private static void processMappedSuperclassMetadata( final JaxbAttributesContainer attributes = jaxbMappedSuperclass.getAttributes(); processAttributes( attributes, classDetails, classAccessType, sourceModelBuildingContext ); - processCommonEntityOrMappedSuperclass( jaxbMappedSuperclass, classDetails, sourceModelBuildingContext ); - - // todo : entity-listeners - // todo : callbacks + processEntityOrMappedSuperclass( jaxbMappedSuperclass, classDetails, sourceModelBuildingContext ); } public static void processOverrideMappedSuperclass( @@ -603,15 +598,19 @@ public static void processOverrideMappedSuperclass( } ); } - private static void processCommonEntityOrMappedSuperclass( + private static void processEntityOrMappedSuperclass( JaxbEntityOrMappedSuperclass jaxbEntity, MutableClassDetails classDetails, SourceModelBuildingContext sourceModelBuildingContext) { XmlAnnotationHelper.applyIdClass( jaxbEntity.getIdClass(), classDetails, sourceModelBuildingContext ); - // todo : entity-listeners - // todo : exclude default listeners (?) - // todo : exclude superclass listeners (?) + XmlAnnotationHelper.applyLifecycleCallbacks( jaxbEntity, classDetails, sourceModelBuildingContext ); + + if ( jaxbEntity.getEntityListeners() != null ) { + jaxbEntity.getEntityListeners().getEntityListener().forEach( ( jaxbEntityListener -> { + XmlAnnotationHelper.applyEntityListener( jaxbEntityListener, classDetails, sourceModelBuildingContext ); + } ) ); + } } public static void processCompleteEmbeddable( diff --git a/hibernate-models-orm/src/main/java/org/hibernate/models/orm/xml/internal/XmlAnnotationHelper.java b/hibernate-models-orm/src/main/java/org/hibernate/models/orm/xml/internal/XmlAnnotationHelper.java index 0ee8377..ad90be6 100644 --- a/hibernate-models-orm/src/main/java/org/hibernate/models/orm/xml/internal/XmlAnnotationHelper.java +++ b/hibernate-models-orm/src/main/java/org/hibernate/models/orm/xml/internal/XmlAnnotationHelper.java @@ -42,10 +42,13 @@ import org.hibernate.boot.jaxb.mapping.spi.JaxbConvertImpl; import org.hibernate.boot.jaxb.mapping.spi.JaxbEmbeddedIdImpl; import org.hibernate.boot.jaxb.mapping.spi.JaxbEntity; +import org.hibernate.boot.jaxb.mapping.spi.JaxbEntityListenerImpl; import org.hibernate.boot.jaxb.mapping.spi.JaxbGeneratedValueImpl; import org.hibernate.boot.jaxb.mapping.spi.JaxbHbmFilterImpl; import org.hibernate.boot.jaxb.mapping.spi.JaxbIdClassImpl; import org.hibernate.boot.jaxb.mapping.spi.JaxbIdImpl; +import org.hibernate.boot.jaxb.mapping.spi.JaxbLifecycleCallback; +import org.hibernate.boot.jaxb.mapping.spi.JaxbLifecycleCallbackContainer; import org.hibernate.boot.jaxb.mapping.spi.JaxbLobImpl; import org.hibernate.boot.jaxb.mapping.spi.JaxbNationalizedImpl; import org.hibernate.boot.jaxb.mapping.spi.JaxbNaturalId; @@ -67,6 +70,7 @@ import org.hibernate.models.source.spi.AnnotationUsage; import org.hibernate.models.source.spi.ClassDetails; import org.hibernate.models.source.spi.ClassDetailsRegistry; +import org.hibernate.models.source.spi.MethodDetails; import org.hibernate.models.source.spi.SourceModelBuildingContext; import org.hibernate.type.SqlTypes; @@ -79,6 +83,7 @@ import jakarta.persistence.Convert; import jakarta.persistence.EmbeddedId; import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; @@ -86,6 +91,13 @@ import jakarta.persistence.IdClass; import jakarta.persistence.Inheritance; import jakarta.persistence.Lob; +import jakarta.persistence.PostLoad; +import jakarta.persistence.PostPersist; +import jakarta.persistence.PostRemove; +import jakarta.persistence.PostUpdate; +import jakarta.persistence.PrePersist; +import jakarta.persistence.PreRemove; +import jakarta.persistence.PreUpdate; import jakarta.persistence.SequenceGenerator; import jakarta.persistence.Table; import jakarta.persistence.TableGenerator; @@ -861,4 +873,47 @@ static void applyIdClass( ); } } + + static void applyEntityListener( + JaxbEntityListenerImpl jaxbEntityListener, + MutableClassDetails classDetails, + SourceModelBuildingContext buildingContext) { + final MutableAnnotationUsage entityListeners = getOrMakeAnnotation( + EntityListeners.class, + classDetails + ); + final MutableClassDetails entityListenerClass = (MutableClassDetails) buildingContext.getClassDetailsRegistry() + .resolveClassDetails( jaxbEntityListener.getClazz() ); + applyLifecycleCallbacks( jaxbEntityListener, entityListenerClass, buildingContext ); + final List values = entityListeners.getAttributeValue( "value" ); + if ( values != null ) { + values.add( entityListenerClass ); + } + else { + entityListeners.setAttributeValue( "value", new ArrayList<>( List.of( entityListenerClass ) ) ); + } + } + + static void applyLifecycleCallbacks( + JaxbLifecycleCallbackContainer lifecycleCallbackContainer, + MutableClassDetails classDetails, + SourceModelBuildingContext buildingContext) { + applyLifecycleCallback( lifecycleCallbackContainer.getPrePersist(), PrePersist.class, classDetails ); + applyLifecycleCallback( lifecycleCallbackContainer.getPostPersist(), PostPersist.class, classDetails ); + applyLifecycleCallback( lifecycleCallbackContainer.getPreRemove(), PreRemove.class, classDetails ); + applyLifecycleCallback( lifecycleCallbackContainer.getPostRemove(), PostRemove.class, classDetails ); + applyLifecycleCallback( lifecycleCallbackContainer.getPreUpdate(), PreUpdate.class, classDetails ); + applyLifecycleCallback( lifecycleCallbackContainer.getPostUpdate(), PostUpdate.class, classDetails ); + applyLifecycleCallback( lifecycleCallbackContainer.getPostLoad(), PostLoad.class, classDetails ); + } + + private static void applyLifecycleCallback( + JaxbLifecycleCallback lifecycleCallback, + Class lifecycleAnnotation, + MutableClassDetails classDetails) { + if ( lifecycleCallback != null ) { + final MethodDetails method = classDetails.findMethodByName( lifecycleCallback.getMethodName() ); + makeAnnotation( lifecycleAnnotation, (MutableMemberDetails) method ); + } + } } diff --git a/hibernate-models-orm/src/test/java/org/hibernate/models/orm/xml/SimpleEntity.java b/hibernate-models-orm/src/test/java/org/hibernate/models/orm/xml/SimpleEntity.java index fd37d08..3ceb4e1 100644 --- a/hibernate-models-orm/src/test/java/org/hibernate/models/orm/xml/SimpleEntity.java +++ b/hibernate-models-orm/src/test/java/org/hibernate/models/orm/xml/SimpleEntity.java @@ -31,6 +31,18 @@ public SimpleEntity(Integer id, String name) { this.name = name; } + public void prePersist() { + // used by hibernate lifecycle callbacks + } + + public void preRemove() { + // used by hibernate lifecycle callbacks + } + + public void preUpdate() { + // used by hibernate lifecycle callbacks + } + public Integer getId() { return id; } diff --git a/hibernate-models-orm/src/test/java/org/hibernate/models/orm/xml/lifecycle/EntityLifecycleTests.java b/hibernate-models-orm/src/test/java/org/hibernate/models/orm/xml/lifecycle/EntityLifecycleTests.java new file mode 100644 index 0000000..eda9414 --- /dev/null +++ b/hibernate-models-orm/src/test/java/org/hibernate/models/orm/xml/lifecycle/EntityLifecycleTests.java @@ -0,0 +1,82 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.models.orm.xml.lifecycle; + +import java.util.List; + +import org.hibernate.boot.internal.BootstrapContextImpl; +import org.hibernate.boot.internal.MetadataBuilderImpl; +import org.hibernate.boot.model.process.spi.ManagedResources; +import org.hibernate.boot.registry.StandardServiceRegistry; +import org.hibernate.boot.registry.StandardServiceRegistryBuilder; +import org.hibernate.models.orm.process.ManagedResourcesImpl; +import org.hibernate.models.orm.spi.CategorizedDomainModel; +import org.hibernate.models.orm.spi.EntityTypeMetadata; +import org.hibernate.models.orm.xml.SimpleEntity; +import org.hibernate.models.source.spi.AnnotationUsage; +import org.hibernate.models.source.spi.ClassDetails; + +import org.junit.jupiter.api.Test; + +import jakarta.persistence.EntityListeners; +import jakarta.persistence.PostLoad; +import jakarta.persistence.PostPersist; +import jakarta.persistence.PostRemove; +import jakarta.persistence.PostUpdate; +import jakarta.persistence.PrePersist; +import jakarta.persistence.PreRemove; +import jakarta.persistence.PreUpdate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hibernate.models.orm.spi.ManagedResourcesProcessor.processManagedResources; + +/** + * @author Marco Belladelli + */ +public class EntityLifecycleTests { + @Test + void testEntityLifecycle() { + final ManagedResources managedResources = new ManagedResourcesImpl.Builder() + .addXmlMappings( "mappings/lifecycle/entity-lifecycle.xml" ) + .build(); + try (StandardServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder().build()) { + final BootstrapContextImpl bootstrapContext = new BootstrapContextImpl( + serviceRegistry, + new MetadataBuilderImpl.MetadataBuildingOptionsImpl( serviceRegistry ) + ); + final CategorizedDomainModel categorizedDomainModel = processManagedResources( + managedResources, + bootstrapContext + ); + + assertThat( categorizedDomainModel.getEntityHierarchies() ).hasSize( 1 ); + final EntityTypeMetadata rootEntity = categorizedDomainModel.getEntityHierarchies() + .iterator() + .next() + .getRoot(); + final ClassDetails classDetails = rootEntity.getClassDetails(); + assertThat( classDetails.getName() ).isEqualTo( SimpleEntity.class.getName() ); + + // lifecycle callback methods + assertThat( classDetails.findMethodByName( "prePersist" ).getAnnotationUsage( PrePersist.class ) ).isNotNull(); + assertThat( classDetails.findMethodByName( "preRemove" ).getAnnotationUsage( PreRemove.class ) ).isNotNull(); + assertThat( classDetails.findMethodByName( "preUpdate" ).getAnnotationUsage( PreUpdate.class ) ).isNotNull(); + + // entity listeners + final AnnotationUsage entityListenersAnn = classDetails.getAnnotationUsage( EntityListeners.class ); + assertThat( entityListenersAnn ).isNotNull(); + final List entityListeners = entityListenersAnn.getAttributeValue( "value" ); + assertThat( entityListeners ).hasSize( 1 ); + final ClassDetails listener = entityListeners.get( 0 ); + assertThat( listener.getName() ).isEqualTo( SimpleEntityListener.class.getName() ); + assertThat( listener.findMethodByName( "postPersist" ).getAnnotationUsage( PostPersist.class ) ).isNotNull(); + assertThat( listener.findMethodByName( "postRemove" ).getAnnotationUsage( PostRemove.class ) ).isNotNull(); + assertThat( listener.findMethodByName( "postUpdate" ).getAnnotationUsage( PostUpdate.class ) ).isNotNull(); + assertThat( listener.findMethodByName( "postLoad" ).getAnnotationUsage( PostLoad.class ) ).isNotNull(); + } + } +} diff --git a/hibernate-models-orm/src/test/java/org/hibernate/models/orm/xml/lifecycle/SimpleEntityListener.java b/hibernate-models-orm/src/test/java/org/hibernate/models/orm/xml/lifecycle/SimpleEntityListener.java new file mode 100644 index 0000000..c8f5737 --- /dev/null +++ b/hibernate-models-orm/src/test/java/org/hibernate/models/orm/xml/lifecycle/SimpleEntityListener.java @@ -0,0 +1,28 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.models.orm.xml.lifecycle; + +/** + * @author Marco Belladelli + */ +public class SimpleEntityListener { + public void postPersist() { + // used by hibernate lifecycle callbacks + } + + public void postRemove() { + // used by hibernate lifecycle callbacks + } + + public void postUpdate() { + // used by hibernate lifecycle callbacks + } + + public void postLoad() { + // used by hibernate lifecycle callbacks + } +} diff --git a/hibernate-models-orm/src/test/resources/mappings/lifecycle/entity-lifecycle.xml b/hibernate-models-orm/src/test/resources/mappings/lifecycle/entity-lifecycle.xml new file mode 100644 index 0000000..78cc6cc --- /dev/null +++ b/hibernate-models-orm/src/test/resources/mappings/lifecycle/entity-lifecycle.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/hibernate-models-source/src/main/java/org/hibernate/models/source/spi/ClassDetails.java b/hibernate-models-source/src/main/java/org/hibernate/models/source/spi/ClassDetails.java index e1abb23..cfb9540 100644 --- a/hibernate-models-source/src/main/java/org/hibernate/models/source/spi/ClassDetails.java +++ b/hibernate-models-source/src/main/java/org/hibernate/models/source/spi/ClassDetails.java @@ -108,6 +108,28 @@ default FieldDetails findFieldByName(String name) { */ List getMethods(); + /** + * Find a method by check + */ + default MethodDetails findMethod(Predicate check) { + final List methods = getMethods(); + for ( int i = 0; i < methods.size(); i++ ) { + final MethodDetails methodDetails = methods.get( i ); + if ( check.test( methodDetails ) ) { + return methodDetails; + } + } + return null; + } + + /** + * Find a method by name + */ + default MethodDetails findMethodByName(String name) { + assert name != null; + return findMethod( methodDetails -> name.equals( methodDetails.getName() ) ); + } + /** * Visit each method */