diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhance/internal/bytebuddy/DirtyCheckingWithEmbeddableAndNonVisibleGenericMappedSuperclassTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhance/internal/bytebuddy/DirtyCheckingWithEmbeddableAndNonVisibleGenericMappedSuperclassTest.java new file mode 100644 index 0000000000..f043df1796 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhance/internal/bytebuddy/DirtyCheckingWithEmbeddableAndNonVisibleGenericMappedSuperclassTest.java @@ -0,0 +1,189 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.bytecode.enhance.internal.bytebuddy; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.extractor.Extractors.resultOf; +import static org.hibernate.bytecode.enhance.spi.EnhancerConstants.ENTITY_ENTRY_FIELD_NAME; +import static org.hibernate.bytecode.enhance.spi.EnhancerConstants.ENTITY_ENTRY_GETTER_NAME; +import static org.hibernate.bytecode.enhance.spi.EnhancerConstants.ENTITY_INSTANCE_GETTER_NAME; +import static org.hibernate.bytecode.enhance.spi.EnhancerConstants.NEXT_FIELD_NAME; +import static org.hibernate.bytecode.enhance.spi.EnhancerConstants.NEXT_GETTER_NAME; +import static org.hibernate.bytecode.enhance.spi.EnhancerConstants.NEXT_SETTER_NAME; +import static org.hibernate.bytecode.enhance.spi.EnhancerConstants.PERSISTENT_FIELD_READER_PREFIX; +import static org.hibernate.bytecode.enhance.spi.EnhancerConstants.PERSISTENT_FIELD_WRITER_PREFIX; +import static org.hibernate.bytecode.enhance.spi.EnhancerConstants.PREVIOUS_FIELD_NAME; +import static org.hibernate.bytecode.enhance.spi.EnhancerConstants.PREVIOUS_GETTER_NAME; +import static org.hibernate.bytecode.enhance.spi.EnhancerConstants.PREVIOUS_SETTER_NAME; +import static org.hibernate.bytecode.enhance.spi.EnhancerConstants.TRACKER_CLEAR_NAME; +import static org.hibernate.bytecode.enhance.spi.EnhancerConstants.TRACKER_COMPOSITE_CLEAR_OWNER; +import static org.hibernate.bytecode.enhance.spi.EnhancerConstants.TRACKER_COMPOSITE_FIELD_NAME; +import static org.hibernate.bytecode.enhance.spi.EnhancerConstants.TRACKER_COMPOSITE_SET_OWNER; +import static org.hibernate.bytecode.enhance.spi.EnhancerConstants.TRACKER_FIELD_NAME; +import static org.hibernate.bytecode.enhance.spi.EnhancerConstants.TRACKER_GET_NAME; +import static org.hibernate.bytecode.enhance.spi.EnhancerConstants.TRACKER_HAS_CHANGED_NAME; +import static org.hibernate.bytecode.enhance.spi.EnhancerConstants.TRACKER_SUSPEND_NAME; + +import java.lang.reflect.Method; + +import org.hibernate.bytecode.enhance.internal.tracker.CompositeOwnerTracker; +import org.hibernate.bytecode.enhance.internal.tracker.SimpleFieldTracker; + +import org.hibernate.testing.bytecode.enhancement.BytecodeEnhancerRunner; +import org.hibernate.testing.bytecode.enhancement.EnhancementOptions; +import org.hibernate.testing.orm.junit.JiraKey; +import org.junit.Test; +import org.junit.runner.RunWith; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +@RunWith(BytecodeEnhancerRunner.class) +@EnhancementOptions(inlineDirtyChecking = true) +public class DirtyCheckingWithEmbeddableAndNonVisibleGenericMappedSuperclassTest { + + @JiraKey("HHH-16832") + public void shouldNotBreakConstructor() { + // This is the actual reproducer for HHH-16832, + // other method are just to be consistent with tests in the same package. + assertThatCode( () -> new MyEntity( 0, "Magic the Gathering" ) ) + .doesNotThrowAnyException(); + } + + @Test + public void shouldDeclareFieldsInEntityClass() { + assertThat( MyEntity.class ) + .hasDeclaredFields( ENTITY_ENTRY_FIELD_NAME, PREVIOUS_FIELD_NAME, NEXT_FIELD_NAME, TRACKER_FIELD_NAME ); + } + + @Test + public void shouldDeclareMethodsInEntityClass() { + assertThat( MyEntity.class ) + .hasDeclaredMethods( PERSISTENT_FIELD_READER_PREFIX + "id", PERSISTENT_FIELD_WRITER_PREFIX + "id" ) + .hasDeclaredMethods( PERSISTENT_FIELD_READER_PREFIX + "embedded", PERSISTENT_FIELD_WRITER_PREFIX + "embedded" ) + .hasDeclaredMethods( ENTITY_INSTANCE_GETTER_NAME, ENTITY_ENTRY_GETTER_NAME ) + .hasDeclaredMethods( PREVIOUS_GETTER_NAME, PREVIOUS_SETTER_NAME, NEXT_GETTER_NAME, NEXT_SETTER_NAME ) + .hasDeclaredMethods( TRACKER_HAS_CHANGED_NAME, TRACKER_CLEAR_NAME, TRACKER_SUSPEND_NAME, TRACKER_GET_NAME ); + } + + @Test + public void shouldDeclareFieldsInEmbeddedClass() { + assertThat( MyEmbeddable.class ) + .hasDeclaredFields( TRACKER_COMPOSITE_FIELD_NAME ); + } + + @Test + public void shouldDeclareMethodsInEmbeddedClass() { + assertThat( MyEmbeddable.class ) + .hasDeclaredMethods( PERSISTENT_FIELD_READER_PREFIX + "text", PERSISTENT_FIELD_WRITER_PREFIX + "text" ) + .hasDeclaredMethods( TRACKER_COMPOSITE_SET_OWNER, TRACKER_COMPOSITE_CLEAR_OWNER ); + } + + @Test + public void shouldCreateTheTracker() { + MyEntity entity = new MyEntity( 0, "value1" ); + assertThat( entity ) + .extracting( NEXT_FIELD_NAME ).isNull(); + assertThat( entity ) + .extracting( PREVIOUS_FIELD_NAME ).isNull(); + assertThat( entity ) + .extracting( ENTITY_ENTRY_FIELD_NAME ).isNull(); + assertThat( entity ) + .extracting( TRACKER_FIELD_NAME ).isInstanceOf( SimpleFieldTracker.class ); + assertThat( entity.getEmbedded() ) + .extracting( TRACKER_COMPOSITE_FIELD_NAME ).isInstanceOf( CompositeOwnerTracker.class); + + assertThat( entity ).extracting( resultOf( TRACKER_HAS_CHANGED_NAME ) ).isEqualTo( true ); + assertThat( entity ).extracting( resultOf( TRACKER_GET_NAME ) ) + .isEqualTo( new String[] { "embedded" } ); + assertThat( entity.getEmbedded() ) + .extracting( TRACKER_COMPOSITE_FIELD_NAME + ".names" ).isEqualTo( new String[] { "embedded" } ); + } + + @Test + public void shouldResetTheTracker() throws Exception { + MyEntity entity = new MyEntity( 1, "value1" ); + + Method trackerClearMethod = MyEntity.class.getMethod( TRACKER_CLEAR_NAME ); + trackerClearMethod.invoke( entity ); + + assertThat( entity ).extracting( resultOf( TRACKER_HAS_CHANGED_NAME ) ).isEqualTo( false ); + assertThat( entity ).extracting( resultOf( TRACKER_GET_NAME ) ).isEqualTo( new String[0] ); + } + + @Test + public void shouldUpdateTheTracker() throws Exception { + MyEntity entity = new MyEntity( 2, "value1" ); + + Method trackerClearMethod = MyEntity.class.getMethod( TRACKER_CLEAR_NAME ); + trackerClearMethod.invoke( entity ); + + entity.getEmbedded().setText( "value2" ); + + assertThat( entity ).extracting( resultOf( TRACKER_HAS_CHANGED_NAME ) ).isEqualTo( true ); + assertThat( entity ).extracting( resultOf( TRACKER_GET_NAME ) ) + .isEqualTo( new String[] { "embedded" } ); + + trackerClearMethod.invoke( entity ); + + entity.setEmbedded( new MyEmbeddable( "value3" ) ); + assertThat( entity ).extracting( resultOf( TRACKER_GET_NAME ) ) + .isEqualTo( new String[] { "embedded" } ); + assertThat( entity.getEmbedded() ) + .extracting( TRACKER_COMPOSITE_FIELD_NAME + ".names" ).isEqualTo( new String[] { "embedded" } ); + } + + @Embeddable + public static class MyEmbeddable { + + @Column + private String text; + + public MyEmbeddable() { + } + + private MyEmbeddable(String text) { + this.text = text; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + } + + @Entity(name = "myentity") + public static class MyEntity extends MyNonVisibleGenericMappedSuperclass { + + @Id + private Integer id; + + public MyEntity() { + } + + private MyEntity(Integer id, String text) { + this.id = id; + setEmbedded( new MyEmbeddable( text ) ); + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhance/internal/bytebuddy/MyNonVisibleGenericMappedSuperclass.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhance/internal/bytebuddy/MyNonVisibleGenericMappedSuperclass.java new file mode 100644 index 0000000000..17cfbc0bb2 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhance/internal/bytebuddy/MyNonVisibleGenericMappedSuperclass.java @@ -0,0 +1,27 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.bytecode.enhance.internal.bytebuddy; + +import jakarta.persistence.Embedded; +import jakarta.persistence.MappedSuperclass; + +// This class must not be nested in the test class, otherwise its private fields will be visible +// from subclasses and we won't reproduce the bug. +@MappedSuperclass +public abstract class MyNonVisibleGenericMappedSuperclass { + + @Embedded + private C embedded; + + public C getEmbedded() { + return embedded; + } + + public void setEmbedded(C embedded) { + this.embedded = embedded; + } +}