Skip to content

HHH-19140 Reproducer test and issue fix #9758

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,11 @@
*/
package org.hibernate.property.access.internal;

import java.beans.Introspector;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Locale;

import org.hibernate.MappingException;
import org.hibernate.PropertyNotFoundException;
import org.hibernate.bytecode.enhance.spi.interceptor.BytecodeLazyAttributeInterceptor;
import org.hibernate.engine.spi.CompositeOwner;
Expand All @@ -31,6 +28,7 @@
import static org.hibernate.engine.internal.ManagedTypeHelper.isPersistentAttributeInterceptableType;
import static org.hibernate.internal.util.ReflectHelper.NO_PARAM_SIGNATURE;
import static org.hibernate.internal.util.ReflectHelper.findField;
import static org.hibernate.internal.util.ReflectHelper.getterMethodOrNull;
import static org.hibernate.internal.util.ReflectHelper.isRecord;

/**
Expand Down Expand Up @@ -85,89 +83,9 @@ public static AccessType getAccessType(Class<?> containerJavaType, String proper
return AccessType.FIELD;
}

for ( Method method : containerClass.getDeclaredMethods() ) {
// if the method has parameters, skip it
if ( method.getParameterCount() != 0 ) {
continue;
}

// if the method is a "bridge", skip it
if ( method.isBridge() ) {
continue;
}

if ( method.isAnnotationPresent( Transient.class ) ) {
continue;
}

if ( Modifier.isStatic( method.getModifiers() ) ) {
continue;
}

final String methodName = method.getName();

// try "get"
if ( methodName.startsWith( "get" ) ) {
final String stemName = methodName.substring( 3 );
final String decapitalizedStemName = Introspector.decapitalize( stemName );
if ( stemName.equals( propertyName ) || decapitalizedStemName.equals( propertyName ) ) {
if ( method.isAnnotationPresent( Access.class ) ) {
return AccessType.PROPERTY;
}
else {
checkIsMethodVariant( containerClass, propertyName, method, stemName );
}
}
}

// if not "get", then try "is"
if ( methodName.startsWith( "is" ) ) {
final String stemName = methodName.substring( 2 );
String decapitalizedStemName = Introspector.decapitalize( stemName );
if ( stemName.equals( propertyName ) || decapitalizedStemName.equals( propertyName ) ) {
if ( method.isAnnotationPresent( Access.class ) ) {
return AccessType.PROPERTY;
}
}
}
}

return null;
}

private static void checkIsMethodVariant(
Class<?> containerClass,
String propertyName,
Method method,
String stemName) {
final Method isMethodVariant = findIsMethodVariant( containerClass, stemName );
if ( isMethodVariant == null ) {
return;
}

if ( !isMethodVariant.isAnnotationPresent( Access.class ) ) {
throw new MappingException(
String.format(
Locale.ROOT,
"Class '%s' declares both 'get' [%s] and 'is' [%s] variants of getter for property '%s'",
containerClass.getName(),
method.toString(),
isMethodVariant,
propertyName
)
);
}
}

public static @Nullable Method findIsMethodVariant(Class<?> containerClass, String stemName) {
// verify that the Class does not also define a method with the same stem name with 'is'
try {
final Method isMethod = containerClass.getDeclaredMethod( "is" + stemName );
if ( !Modifier.isStatic( isMethod.getModifiers() ) && isMethod.getAnnotation( Transient.class ) == null ) {
return isMethod;
}
}
catch (NoSuchMethodException ignore) {
final Method getter = getterMethodOrNull( containerClass, propertyName );
if ( getter != null && getter.isAnnotationPresent( Access.class ) ) {
return AccessType.PROPERTY;
}

return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.orm.test.bytecode.enhancement.access;

import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.JiraKey;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

import jakarta.persistence.Access;
import jakarta.persistence.AccessType;
import jakarta.persistence.Basic;
import jakarta.persistence.DiscriminatorColumn;
import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Inheritance;
import jakarta.persistence.Table;
import jakarta.persistence.Transient;

import static org.assertj.core.api.Assertions.assertThat;

@DomainModel(
annotatedClasses = {
HierarchyPropertyAccessTest.ChildEntity.class,
}
)
@SessionFactory
@JiraKey("HHH-19140")
@BytecodeEnhanced
public class HierarchyPropertyAccessTest {


@Test
public void testParent(SessionFactoryScope scope) {
scope.inTransaction( session -> {
session.persist( new ParentEntity( 1L, "field", "transient: property" ) );
} );

scope.inTransaction( session -> {
ParentEntity entity = session.get( ParentEntity.class, 1L );
assertThat( entity.persistProperty ).isEqualTo( "property" );
assertThat( entity.property ).isEqualTo( "transient: property" );

entity.setProperty( "transient: updated" );
} );

scope.inTransaction( session -> {
ParentEntity entity = session.get( ParentEntity.class, 1L );
assertThat( entity.persistProperty ).isEqualTo( "updated" );
assertThat( entity.property ).isEqualTo( "transient: updated" );
} );
}

@Test
public void testChild(SessionFactoryScope scope) {
scope.inTransaction( session -> {
session.persist( new ChildEntity( 2L, "field", "transient: property" ) );
} );

scope.inTransaction( session -> {
ChildEntity entity = session.get( ChildEntity.class, 2L );
assertThat( entity.persistProperty ).isEqualTo( "property" );
assertThat( entity.property ).isEqualTo( "transient: property" );

entity.setProperty( "transient: updated" );
} );

scope.inTransaction( session -> {
ChildEntity entity = session.get( ChildEntity.class, 2L );
assertThat( entity.persistProperty ).isEqualTo( "updated" );
assertThat( entity.property ).isEqualTo( "transient: updated" );
} );
}

@AfterEach
public void cleanup(SessionFactoryScope scope) {
scope.inTransaction( session -> {
ParentEntity parentEntity = session.get( ParentEntity.class, 1L );
if (parentEntity != null) {
session.remove( parentEntity );
}
ChildEntity childEntity = session.get( ChildEntity.class, 2L );
if (childEntity != null) {
session.remove( childEntity );
}
} );
}

@Entity
@Table(name = "PARENT_ENTITY")
@Inheritance
@DiscriminatorColumn(name = "type")
@DiscriminatorValue("Parent")
static class ParentEntity {
@Id
Long id;

@Basic
String field;

String persistProperty;

@Transient
String property;

public ParentEntity() {
}

public ParentEntity(Long id, String field, String property) {
this.id = id;
this.field = field;
this.property = property;
}

@Basic
@Access(AccessType.PROPERTY)
public String getPersistProperty() {
this.persistProperty = this.property.substring( 11 );
return this.persistProperty;
}

public void setPersistProperty(String persistProperty) {
this.property = "transient: " + persistProperty;
this.persistProperty = persistProperty;
}

public String getProperty() {
return this.property;
}

public void setProperty(String property) {
this.property = property;
}
}

@Entity
@DiscriminatorValue("Child")
static class ChildEntity extends ParentEntity {

public ChildEntity() {
}

public ChildEntity(Long id, String field, String property) {
super(id, field, property);
}
}
}
Loading