HHH-16572 - Skip enhancement for PROPERTY attributes with mismatched field and method names
Signed-off-by: Scott Marlow <smarlow@redhat.com>
This commit is contained in:
parent
14e0f12654
commit
356ea205ff
|
@ -4,17 +4,25 @@
|
||||||
*/
|
*/
|
||||||
package org.hibernate.bytecode.enhance.internal.bytebuddy;
|
package org.hibernate.bytecode.enhance.internal.bytebuddy;
|
||||||
|
|
||||||
import java.lang.annotation.Annotation;
|
import jakarta.persistence.Access;
|
||||||
import java.lang.reflect.Modifier;
|
import jakarta.persistence.AccessType;
|
||||||
import java.util.ArrayList;
|
import jakarta.persistence.metamodel.Type;
|
||||||
import java.util.Collection;
|
import net.bytebuddy.asm.Advice;
|
||||||
import java.util.Collections;
|
import net.bytebuddy.description.annotation.AnnotationDescription;
|
||||||
import java.util.List;
|
import net.bytebuddy.description.annotation.AnnotationList;
|
||||||
import java.util.Map;
|
import net.bytebuddy.description.field.FieldDescription;
|
||||||
import java.util.Objects;
|
import net.bytebuddy.description.field.FieldDescription.InDefinedShape;
|
||||||
import java.util.Optional;
|
import net.bytebuddy.description.method.MethodDescription;
|
||||||
import java.util.function.Supplier;
|
import net.bytebuddy.description.type.TypeDefinition;
|
||||||
|
import net.bytebuddy.description.type.TypeDescription;
|
||||||
|
import net.bytebuddy.description.type.TypeDescription.Generic;
|
||||||
|
import net.bytebuddy.description.type.TypeList;
|
||||||
|
import net.bytebuddy.dynamic.DynamicType;
|
||||||
|
import net.bytebuddy.dynamic.scaffold.MethodGraph;
|
||||||
|
import net.bytebuddy.implementation.FieldAccessor;
|
||||||
|
import net.bytebuddy.implementation.FixedValue;
|
||||||
|
import net.bytebuddy.implementation.Implementation;
|
||||||
|
import net.bytebuddy.implementation.StubMethod;
|
||||||
import org.hibernate.Version;
|
import org.hibernate.Version;
|
||||||
import org.hibernate.bytecode.enhance.VersionMismatchException;
|
import org.hibernate.bytecode.enhance.VersionMismatchException;
|
||||||
import org.hibernate.bytecode.enhance.internal.tracker.CompositeOwnerTracker;
|
import org.hibernate.bytecode.enhance.internal.tracker.CompositeOwnerTracker;
|
||||||
|
@ -39,23 +47,16 @@ import org.hibernate.engine.spi.SelfDirtinessTracker;
|
||||||
import org.hibernate.internal.CoreLogging;
|
import org.hibernate.internal.CoreLogging;
|
||||||
import org.hibernate.internal.CoreMessageLogger;
|
import org.hibernate.internal.CoreMessageLogger;
|
||||||
|
|
||||||
import jakarta.persistence.Access;
|
import java.lang.annotation.Annotation;
|
||||||
import jakarta.persistence.AccessType;
|
import java.lang.reflect.Modifier;
|
||||||
import jakarta.persistence.metamodel.Type;
|
import java.util.ArrayList;
|
||||||
import net.bytebuddy.asm.Advice;
|
import java.util.Collection;
|
||||||
import net.bytebuddy.description.annotation.AnnotationDescription;
|
import java.util.Collections;
|
||||||
import net.bytebuddy.description.annotation.AnnotationList;
|
import java.util.List;
|
||||||
import net.bytebuddy.description.field.FieldDescription;
|
import java.util.Map;
|
||||||
import net.bytebuddy.description.field.FieldDescription.InDefinedShape;
|
import java.util.Objects;
|
||||||
import net.bytebuddy.description.method.MethodDescription;
|
import java.util.Optional;
|
||||||
import net.bytebuddy.description.type.TypeDefinition;
|
import java.util.function.Supplier;
|
||||||
import net.bytebuddy.description.type.TypeDescription;
|
|
||||||
import net.bytebuddy.description.type.TypeDescription.Generic;
|
|
||||||
import net.bytebuddy.dynamic.DynamicType;
|
|
||||||
import net.bytebuddy.implementation.FieldAccessor;
|
|
||||||
import net.bytebuddy.implementation.FixedValue;
|
|
||||||
import net.bytebuddy.implementation.Implementation;
|
|
||||||
import net.bytebuddy.implementation.StubMethod;
|
|
||||||
|
|
||||||
import static net.bytebuddy.matcher.ElementMatchers.isDefaultFinalizer;
|
import static net.bytebuddy.matcher.ElementMatchers.isDefaultFinalizer;
|
||||||
|
|
||||||
|
@ -170,6 +171,11 @@ public class EnhancerImpl implements Enhancer {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( enhancementContext.isEntityClass( managedCtClass ) ) {
|
if ( enhancementContext.isEntityClass( managedCtClass ) ) {
|
||||||
|
if ( hasUnsupportedAttributeNaming( managedCtClass ) ) {
|
||||||
|
// do not enhance classes with mismatched names for PROPERTY-access persistent attributes
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
log.debugf( "Enhancing [%s] as Entity", managedCtClass.getName() );
|
log.debugf( "Enhancing [%s] as Entity", managedCtClass.getName() );
|
||||||
DynamicType.Builder<?> builder = builderSupplier.get();
|
DynamicType.Builder<?> builder = builderSupplier.get();
|
||||||
builder = builder.implement( ManagedEntity.class )
|
builder = builder.implement( ManagedEntity.class )
|
||||||
|
@ -329,6 +335,11 @@ public class EnhancerImpl implements Enhancer {
|
||||||
return createTransformer( managedCtClass ).applyTo( builder );
|
return createTransformer( managedCtClass ).applyTo( builder );
|
||||||
}
|
}
|
||||||
else if ( enhancementContext.isCompositeClass( managedCtClass ) ) {
|
else if ( enhancementContext.isCompositeClass( managedCtClass ) ) {
|
||||||
|
if ( hasUnsupportedAttributeNaming( managedCtClass ) ) {
|
||||||
|
// do not enhance classes with mismatched names for PROPERTY-access persistent attributes
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
log.debugf( "Enhancing [%s] as Composite", managedCtClass.getName() );
|
log.debugf( "Enhancing [%s] as Composite", managedCtClass.getName() );
|
||||||
|
|
||||||
DynamicType.Builder<?> builder = builderSupplier.get();
|
DynamicType.Builder<?> builder = builderSupplier.get();
|
||||||
|
@ -362,6 +373,12 @@ public class EnhancerImpl implements Enhancer {
|
||||||
return createTransformer( managedCtClass ).applyTo( builder );
|
return createTransformer( managedCtClass ).applyTo( builder );
|
||||||
}
|
}
|
||||||
else if ( enhancementContext.isMappedSuperclassClass( managedCtClass ) ) {
|
else if ( enhancementContext.isMappedSuperclassClass( managedCtClass ) ) {
|
||||||
|
|
||||||
|
// Check for HHH-16572 (PROPERTY attributes with mismatched field and method names)
|
||||||
|
if ( hasUnsupportedAttributeNaming( managedCtClass ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
log.debugf( "Enhancing [%s] as MappedSuperclass", managedCtClass.getName() );
|
log.debugf( "Enhancing [%s] as MappedSuperclass", managedCtClass.getName() );
|
||||||
|
|
||||||
DynamicType.Builder<?> builder = builderSupplier.get();
|
DynamicType.Builder<?> builder = builderSupplier.get();
|
||||||
|
@ -378,6 +395,82 @@ public class EnhancerImpl implements Enhancer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether an entity class ({@code managedCtClass}) has mismatched names between a persistent field and its
|
||||||
|
* getter/setter when using {@link AccessType#PROPERTY}, which Hibernate does not currently support for enhancement.
|
||||||
|
* See https://hibernate.atlassian.net/browse/HHH-16572
|
||||||
|
*/
|
||||||
|
private boolean hasUnsupportedAttributeNaming(TypeDescription managedCtClass) {
|
||||||
|
// For process access rules, See https://jakarta.ee/specifications/persistence/3.2/jakarta-persistence-spec-3.2#default-access-type
|
||||||
|
// and https://jakarta.ee/specifications/persistence/3.2/jakarta-persistence-spec-3.2#a122
|
||||||
|
//
|
||||||
|
// This check will determine if entity field names do not match Property accessor method name
|
||||||
|
// For example:
|
||||||
|
// @Entity
|
||||||
|
// class Book {
|
||||||
|
// Integer id;
|
||||||
|
// String smtg;
|
||||||
|
//
|
||||||
|
// @Id Integer getId() { return id; }
|
||||||
|
// String getSomething() { return smtg; }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Check name of the getter/setter method with persistence annotation and getter/setter method name that doesn't refer to an entity field
|
||||||
|
// and will return false. If the property accessor method(s) are named to match the field name(s), return true.
|
||||||
|
boolean propertyHasAnnotation = false;
|
||||||
|
MethodGraph.Linked methodGraph = MethodGraph.Compiler.Default.forJavaHierarchy().compile((TypeDefinition) managedCtClass);
|
||||||
|
for (MethodGraph.Node node : methodGraph.listNodes()) {
|
||||||
|
MethodDescription methodDescription = node.getRepresentative();
|
||||||
|
if (methodDescription.getDeclaringType().represents(Object.class)) { // skip class java.lang.Object methods
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String methodName = methodDescription.getActualName();
|
||||||
|
if (methodName.equals("") ||
|
||||||
|
(!methodName.startsWith("get") && !methodName.startsWith("set") && !methodName.startsWith("is"))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String methodFieldName;
|
||||||
|
if (methodName.startsWith("is")) { // skip past "is"
|
||||||
|
methodFieldName = methodName.substring(2);
|
||||||
|
}
|
||||||
|
else if (methodName.startsWith("get") ||
|
||||||
|
methodName.startsWith("set")) { // skip past "get" or "set"
|
||||||
|
methodFieldName = methodName.substring(3);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// not a property accessor method so ignore it
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
boolean propertyNameMatchesFieldName = false;
|
||||||
|
// convert field letter to lower case
|
||||||
|
methodFieldName = methodFieldName.substring(0, 1).toLowerCase() + methodFieldName.substring(1);
|
||||||
|
TypeList typeList = methodDescription.getDeclaredAnnotations().asTypeList();
|
||||||
|
if (typeList.stream().anyMatch(typeDefinitions ->
|
||||||
|
(typeDefinitions.getName().contains("jakarta.persistence")))) {
|
||||||
|
propertyHasAnnotation = true;
|
||||||
|
}
|
||||||
|
for (FieldDescription ctField : methodDescription.getDeclaringType().getDeclaredFields()) {
|
||||||
|
if (!Modifier.isStatic(ctField.getModifiers())) {
|
||||||
|
AnnotatedFieldDescription annotatedField = new AnnotatedFieldDescription(enhancementContext, ctField);
|
||||||
|
boolean containsPropertyAccessorMethods = false;
|
||||||
|
if (enhancementContext.isPersistentField(annotatedField)) {
|
||||||
|
if (methodFieldName.equals(ctField.getActualName())) {
|
||||||
|
propertyNameMatchesFieldName = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (propertyHasAnnotation && !propertyNameMatchesFieldName) {
|
||||||
|
log.debugf("Skipping enhancement of [%s]: due to class [%s] not having a property accessor method name matching field name [%s]",
|
||||||
|
managedCtClass, methodDescription.getDeclaringType().getActualName(), methodFieldName);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private static void verifyVersions(TypeDescription managedCtClass, ByteBuddyEnhancementContext enhancementContext) {
|
private static void verifyVersions(TypeDescription managedCtClass, ByteBuddyEnhancementContext enhancementContext) {
|
||||||
final AnnotationDescription.Loadable<EnhancementInfo> existingInfo = managedCtClass
|
final AnnotationDescription.Loadable<EnhancementInfo> existingInfo = managedCtClass
|
||||||
.getDeclaredAnnotations()
|
.getDeclaredAnnotations()
|
||||||
|
@ -493,7 +586,7 @@ public class EnhancerImpl implements Enhancer {
|
||||||
AnnotatedFieldDescription annotatedField = new AnnotatedFieldDescription( enhancementContext, ctField );
|
AnnotatedFieldDescription annotatedField = new AnnotatedFieldDescription( enhancementContext, ctField );
|
||||||
if ( enhancementContext.isPersistentField( annotatedField ) && enhancementContext.isMappedCollection( annotatedField ) ) {
|
if ( enhancementContext.isPersistentField( annotatedField ) && enhancementContext.isMappedCollection( annotatedField ) ) {
|
||||||
if ( ctField.getType().asErasure().isAssignableTo( Collection.class ) || ctField.getType().asErasure().isAssignableTo( Map.class ) ) {
|
if ( ctField.getType().asErasure().isAssignableTo( Collection.class ) || ctField.getType().asErasure().isAssignableTo( Map.class ) ) {
|
||||||
collectionList.add( annotatedField );
|
collectionList.add( annotatedField );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
/*
|
||||||
|
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
* Copyright Red Hat Inc. and Hibernate Authors
|
||||||
|
*/
|
||||||
|
package org.hibernate.orm.test.bytecode.enhancement.access;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced;
|
||||||
|
import org.hibernate.testing.orm.junit.*;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
@DomainModel(
|
||||||
|
annotatedClasses = {
|
||||||
|
InvalidPropertyNameTest.SomeEntity.class,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@SessionFactory
|
||||||
|
@JiraKey("HHH-16572")
|
||||||
|
@BytecodeEnhanced
|
||||||
|
public class InvalidPropertyNameTest {
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@FailureExpected(jiraKey = "HHH-16572")
|
||||||
|
public void test(SessionFactoryScope scope) {
|
||||||
|
scope.inTransaction( session -> {
|
||||||
|
session.persist( new SomeEntity( 1L, "field", "property" ) );
|
||||||
|
} );
|
||||||
|
|
||||||
|
scope.inTransaction( session -> {
|
||||||
|
SomeEntity entity = session.get( SomeEntity.class, 1L );
|
||||||
|
assertThat( entity.property ).isEqualTo( "from getter: property" );
|
||||||
|
|
||||||
|
entity.setPropertyMethod( "updated" );
|
||||||
|
} );
|
||||||
|
|
||||||
|
scope.inTransaction( session -> {
|
||||||
|
SomeEntity entity = session.get( SomeEntity.class, 1L );
|
||||||
|
assertThat( entity.property ).isEqualTo( "from getter: updated" );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
public void cleanup(SessionFactoryScope scope) {
|
||||||
|
// uncomment the following when @FailureExpected is removed above
|
||||||
|
// scope.inTransaction( session -> {
|
||||||
|
// session.remove( session.get( SomeEntity.class, 1L ) );
|
||||||
|
// PropertyAccessTest} );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "SOME_ENTITY")
|
||||||
|
static class SomeEntity {
|
||||||
|
@Id
|
||||||
|
Long id;
|
||||||
|
|
||||||
|
@Basic
|
||||||
|
String field;
|
||||||
|
|
||||||
|
String property;
|
||||||
|
|
||||||
|
public SomeEntity() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public SomeEntity(Long id, String field, String property) {
|
||||||
|
this.id = id;
|
||||||
|
this.field = field;
|
||||||
|
this.property = property;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The following property accessor methods are purposely named incorrectly to
|
||||||
|
* not match the "property" field. The HHH-16572 change ensures that
|
||||||
|
* this entity is not (bytecode) enhanced. Eventually further changes will be made
|
||||||
|
* such that this entity is enhanced in which case the FailureExpected can be removed
|
||||||
|
* and the cleanup() uncommented.
|
||||||
|
*/
|
||||||
|
@Basic
|
||||||
|
@Access(AccessType.PROPERTY)
|
||||||
|
public String getPropertyMethod() {
|
||||||
|
return "from getter: " + property;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPropertyMethod(String property) {
|
||||||
|
this.property = property;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,7 +8,6 @@ import org.hibernate.cfg.AvailableSettings;
|
||||||
|
|
||||||
import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced;
|
import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced;
|
||||||
import org.hibernate.testing.orm.junit.DomainModel;
|
import org.hibernate.testing.orm.junit.DomainModel;
|
||||||
import org.hibernate.testing.orm.junit.FailureExpected;
|
|
||||||
import org.hibernate.testing.orm.junit.JiraKey;
|
import org.hibernate.testing.orm.junit.JiraKey;
|
||||||
import org.hibernate.testing.orm.junit.ServiceRegistry;
|
import org.hibernate.testing.orm.junit.ServiceRegistry;
|
||||||
import org.hibernate.testing.orm.junit.SessionFactory;
|
import org.hibernate.testing.orm.junit.SessionFactory;
|
||||||
|
@ -77,7 +76,7 @@ public class LazyLoadingByEnhancerSetterTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@FailureExpected( jiraKey = "HHH-10747" )
|
// failure doesn't occur with HHH-16572 change @FailureExpected( jiraKey = "HHH-10747" )
|
||||||
public void testProperty(SessionFactoryScope scope) {
|
public void testProperty(SessionFactoryScope scope) {
|
||||||
scope.inTransaction( s -> {
|
scope.inTransaction( s -> {
|
||||||
ItemProperty input = new ItemProperty();
|
ItemProperty input = new ItemProperty();
|
||||||
|
|
Loading…
Reference in New Issue