Skip to content
Open
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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/
package org.hibernate.boot.jaxb.hbm.transform;

import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
Expand Down Expand Up @@ -47,6 +48,21 @@ static Set<String> discoverAllPropertyNames(Class<?> javaClass, boolean fieldAcc
return discoverUnmappedPropertyNames( javaClass, Set.of(), fieldAccess );
}

static String extractPropertyName(Method method) {
if ( method.getParameterCount() != 0 ) {
return null;
}
final String methodName = method.getName();
if ( methodName.startsWith( "get" ) && methodName.length() > 3 ) {
return StringHelper.decapitalize( methodName.substring( 3 ) );
}
if ( methodName.startsWith( "is" ) && methodName.length() > 2
&& ( method.getReturnType() == boolean.class || method.getReturnType() == Boolean.class ) ) {
return StringHelper.decapitalize( methodName.substring( 2 ) );
}
return null;
}

static Set<String> discoverUnmappedPropertyNames(
Class<?> javaClass,
Set<String> mappedPropertyNames,
Expand All @@ -68,18 +84,7 @@ static Set<String> discoverUnmappedPropertyNames(
effectiveMappedNames.add( StringHelper.decapitalize( name ) );
}
for ( var method : javaClass.getMethods() ) {
if ( method.getParameterCount() != 0 ) {
continue;
}
String propertyName = null;
final String methodName = method.getName();
if ( methodName.startsWith( "get" ) && methodName.length() > 3 ) {
propertyName = StringHelper.decapitalize( methodName.substring( 3 ) );
}
else if ( methodName.startsWith( "is" ) && methodName.length() > 2
&& ( method.getReturnType() == boolean.class || method.getReturnType() == Boolean.class ) ) {
propertyName = StringHelper.decapitalize( methodName.substring( 2 ) );
}
final String propertyName = extractPropertyName( method );
if ( propertyName != null
&& !effectiveMappedNames.contains( propertyName )
&& !propertyName.equals( "class" ) ) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@
import org.hibernate.boot.jaxb.mapping.spi.JaxbEmbeddableImpl;
import org.hibernate.boot.jaxb.mapping.spi.JaxbEmbeddedImpl;
import org.hibernate.boot.jaxb.mapping.spi.JaxbEntityImpl;
import org.hibernate.boot.jaxb.mapping.spi.JaxbHqlImportImpl;
import org.hibernate.boot.jaxb.mapping.spi.JaxbEntityMappingsImpl;
import org.hibernate.boot.jaxb.mapping.spi.JaxbIdImpl;
import org.hibernate.boot.jaxb.mapping.spi.JaxbManyToManyImpl;
import org.hibernate.boot.jaxb.mapping.spi.JaxbMappedSuperclassImpl;
import org.hibernate.boot.jaxb.mapping.spi.JaxbManyToOneImpl;
import org.hibernate.boot.jaxb.mapping.spi.JaxbOneToManyImpl;
import org.hibernate.boot.jaxb.mapping.spi.JaxbOneToOneImpl;
Expand Down Expand Up @@ -777,6 +779,91 @@ public void testSharedEmbeddableFormulaPropertyTransformation(ServiceRegistrySco
} );
}

@Test
@JiraKey( "HHH-20709" )
public void testCollectionFetchJoinLazyTransformation(ServiceRegistryScope scope) {
transformAndVerify( "xml/jaxb/mapping/collection-fetch-join-lazy/hbm.xml", scope, (transformed) -> {
final JaxbEntityImpl userEntity = transformed.getEntities().stream()
.filter( e -> "User".equals( e.getClazz() ) )
.findFirst()
.orElseThrow();

assertThat( userEntity.getAttributes().getOneToManyAttributes() ).hasSize( 1 );

final JaxbOneToManyImpl emailAddresses = userEntity.getAttributes().getOneToManyAttributes().get( 0 );
assertThat( emailAddresses.getName() ).isEqualTo( "emailAddresses" );
assertThat( emailAddresses.getFetchMode() )
.as( "Lazy collection with fetch='join' should not have fetch-mode=JOIN" )
.isNotEqualTo( org.hibernate.boot.jaxb.mapping.spi.JaxbPluralFetchModeImpl.JOIN );
assertThat( emailAddresses.getFetch() )
.as( "Collection with default lazy='true' should have fetch=LAZY" )
.isEqualTo( jakarta.persistence.FetchType.LAZY );
} );
}

@Test
@JiraKey( "HHH-20712" )
public void testOnDeleteCascadeTransformation(ServiceRegistryScope scope) {
transformAndVerify( "xml/jaxb/mapping/on-delete-toone/hbm.xml", scope, (transformed) -> {
final JaxbEntityImpl childEntity = transformed.getEntities().stream()
.filter( e -> "Child".equals( e.getClazz() ) )
.findFirst()
.orElseThrow();

assertThat( childEntity.getAttributes().getManyToOneAttributes() ).hasSize( 1 );

final JaxbManyToOneImpl parentManyToOne = childEntity.getAttributes().getManyToOneAttributes().get( 0 );
assertThat( parentManyToOne.getName() ).isEqualTo( "parent" );
assertThat( parentManyToOne.getOnDelete() )
.as( "many-to-one with on-delete='cascade' should have on-delete=CASCADE" )
.isEqualTo( org.hibernate.annotations.OnDeleteAction.CASCADE );
} );
}

@Test
@JiraKey( "HHH-20709" )
public void testCollectionFetchJoinEagerTransformation(ServiceRegistryScope scope) {
transformAndVerify( "xml/jaxb/mapping/collection-fetch-join-eager/hbm.xml", scope, (transformed) -> {
final JaxbEntityImpl userEntity = transformed.getEntities().stream()
.filter( e -> "User".equals( e.getClazz() ) )
.findFirst()
.orElseThrow();

assertThat( userEntity.getAttributes().getOneToManyAttributes() ).hasSize( 1 );

final JaxbOneToManyImpl emailAddresses = userEntity.getAttributes().getOneToManyAttributes().get( 0 );
assertThat( emailAddresses.getName() ).isEqualTo( "emailAddresses" );
assertThat( emailAddresses.getFetchMode() )
.as( "Eager collection with fetch='join' should have fetch-mode=JOIN" )
.isEqualTo( org.hibernate.boot.jaxb.mapping.spi.JaxbPluralFetchModeImpl.JOIN );
assertThat( emailAddresses.getFetch() )
.as( "Collection with lazy='false' should have fetch=EAGER" )
.isEqualTo( jakarta.persistence.FetchType.EAGER );
} );
}

@Test
@JiraKey( "HHH-20711" )
public void testIdClassTransformation(ServiceRegistryScope scope) {
transformAndVerify( "xml/jaxb/mapping/id-class/hbm.xml", scope, (transformed) -> {
final JaxbEntityImpl customerEntity = transformed.getEntities().stream()
.filter( e -> "Customer".equals( e.getClazz() ) )
.findFirst()
.orElseThrow();

assertThat( customerEntity.getIdClass() )
.as( "Entity with composite-id class should have id-class" )
.isNotNull();
assertThat( customerEntity.getIdClass().getClazz() )
.as( "id-class should reference the fully qualified CustomerId class" )
.isEqualTo( "org.hibernate.orm.test.idclass.CustomerId" );

assertThat( customerEntity.getAttributes().getIdAttributes() )
.as( "Entity should have 2 id attributes" )
.hasSize( 2 );
} );
}

private void transformAndVerify(
String resourceName,
ServiceRegistryScope scope,
Expand Down Expand Up @@ -1216,6 +1303,92 @@ public void testInverseOneToManyWithCompositeKeyPropertyMappedBy(ServiceRegistry
} );
}

@Test
@JiraKey( "HHH-20715" )
public void testUnmappedSuperclassGeneratesMappedSuperclass(ServiceRegistryScope scope) {
// ConcreteEntity and AnotherEntity both extend AbstractBase (a plain Java class, not an entity).
// The hbm.xml maps properties (id, version, name, relatedBase) declared on AbstractBase.
// The transformer should generate a single <mapped-superclass> for AbstractBase,
// move the inherited attributes there, and mark unmapped superclass properties as transient.
transformAndVerify( "xml/jaxb/mapping/unmapped-superclass/hbm.xml", scope, (transformed) -> {
// A single mapped-superclass should be generated even though two entities share the same superclass
assertThat( transformed.getMappedSuperclasses() )
.as( "Exactly one <mapped-superclass> should be generated for the shared AbstractBase" )
.hasSize( 1 );

final JaxbMappedSuperclassImpl mappedSuperclass = transformed.getMappedSuperclasses().get( 0 );
assertThat( mappedSuperclass.getClazz() )
.isEqualTo( "org.hibernate.orm.test.boot.jaxb.mapping.unmappedsuperclass.AbstractBase" );
assertThat( mappedSuperclass.isMetadataComplete() ).isTrue();

final var superAttrs = mappedSuperclass.getAttributes();

// Id attribute should be on the mapped-superclass
assertThat( superAttrs.getIdAttributes() )
.extracting( JaxbIdImpl::getName )
.containsExactly( "id" );

// Version attribute should be on the mapped-superclass
assertThat( superAttrs.getVersion() )
.as( "version should be moved to the mapped-superclass" )
.isNotNull();
assertThat( superAttrs.getVersion().getName() )
.isEqualTo( "version" );

// Basic attribute 'name' should be on the mapped-superclass
assertThat( superAttrs.getBasicAttributes() )
.extracting( JaxbBasicImpl::getName )
.containsExactly( "name" );

// Many-to-one attribute 'relatedBase' should be on the mapped-superclass
assertThat( superAttrs.getManyToOneAttributes() )
.extracting( JaxbManyToOneImpl::getName )
.containsExactly( "relatedBase" );

// Unmapped property should be declared as transient
assertThat( superAttrs.getTransients() )
.extracting( JaxbTransientImpl::getName )
.contains( "unmappedProperty" );

// --- ConcreteEntity assertions ---
final JaxbEntityImpl concreteEntity = transformed.getEntities().stream()
.filter( e -> "ConcreteEntity".equals( e.getClazz() ) )
.findFirst()
.orElseThrow();

// Inherited attributes should NOT be on the entity
assertThat( concreteEntity.getAttributes().getIdAttributes() )
.as( "Entity should not have id — inherited from mapped-superclass" )
.isEmpty();
assertThat( concreteEntity.getAttributes().getVersion() )
.as( "Entity should not have version — inherited from mapped-superclass" )
.isNull();
assertThat( concreteEntity.getAttributes().getManyToOneAttributes() )
.as( "Entity should not have relatedBase — inherited from mapped-superclass" )
.isEmpty();

// Entity's own attribute should remain
assertThat( concreteEntity.getAttributes().getBasicAttributes() )
.extracting( JaxbBasicImpl::getName )
.containsExactly( "description" );

// --- AnotherEntity assertions ---
final JaxbEntityImpl anotherEntity = transformed.getEntities().stream()
.filter( e -> "AnotherEntity".equals( e.getClazz() ) )
.findFirst()
.orElseThrow();

// Inherited attributes should NOT be on the entity
assertThat( anotherEntity.getAttributes().getIdAttributes() ).isEmpty();
assertThat( anotherEntity.getAttributes().getVersion() ).isNull();

// Entity's own attribute should remain
assertThat( anotherEntity.getAttributes().getBasicAttributes() )
.extracting( JaxbBasicImpl::getName )
.containsExactly( "code" );
} );
}

@Test
@JiraKey( "HHH-20697" )
public void testNonAggregatedCompositeIdColumnsNotUnique(ServiceRegistryScope scope) {
Expand All @@ -1236,4 +1409,59 @@ public void testNonAggregatedCompositeIdColumnsNotUnique(ServiceRegistryScope sc
}
} );
}

@Test
@JiraKey( "HHH-20699" )
public void testCompositeKeyManyToOneFetchLazy(ServiceRegistryScope scope) {
transformAndVerify( "xml/jaxb/mapping/composite-key-many-to-one-fetch/hbm.xml", scope, transformed -> {
final JaxbEntityImpl addressEntity = transformed.getEntities().stream()
.filter( e -> "Address".equals( e.getClazz() ) )
.findFirst()
.orElseThrow();

assertThat( addressEntity.getAttributes().getManyToOneAttributes() )
.hasSize( 1 );

final JaxbManyToOneImpl personManyToOne = addressEntity.getAttributes().getManyToOneAttributes().get( 0 );
assertThat( personManyToOne.getName() ).isEqualTo( "person" );
assertThat( personManyToOne.isId() ).isTrue();
assertThat( personManyToOne.getFetch() )
.as( "Composite-id key-many-to-one should have fetch=LAZY" )
.isEqualTo( jakarta.persistence.FetchType.LAZY );
} );
}

@Test
@JiraKey( "HHH-20717" )
public void testImportWithoutRenameDefaultsToUnqualifiedClassName(ServiceRegistryScope scope) {
transformAndVerify( "xml/jaxb/mapping/import-no-rename/hbm.xml", scope, transformed -> {
assertThat( transformed.getHqlImports() ).hasSize( 1 );
final JaxbHqlImportImpl hqlImport = (JaxbHqlImportImpl) transformed.getHqlImports().get( 0 );
assertThat( hqlImport.getClazz() ).isEqualTo( "Animal" );
assertThat( hqlImport.getRename() )
.as( "When hbm.xml import has no rename, transformer should default to unqualified class name" )
.isEqualTo( "Animal" );
} );
}

@Test
@JiraKey( "HHH-20703" )
public void testCollectionTypeTypedefResolution(ServiceRegistryScope scope) {
transformAndVerify( "xml/jaxb/mapping/collection-type-typedef/hbm.xml", scope, transformed -> {
final JaxbEntityImpl entity = transformed.getEntities().get( 0 );

assertThat( entity.getAttributes().getElementCollectionAttributes() ).hasSize( 1 );
final var elementCollection = entity.getAttributes().getElementCollectionAttributes().get( 0 );
assertThat( elementCollection.getName() ).isEqualTo( "values" );
assertThat( elementCollection.getCollectionType() )
.as( "collection-type referencing a typedef should be resolved" )
.isNotNull();
assertThat( elementCollection.getCollectionType().getType() )
.as( "collection-type should use the typedef class, not the typedef name" )
.isEqualTo( "org.hibernate.orm.test.mapping.collections.custom.parameterized.DefaultableListType" );
assertThat( elementCollection.getCollectionType().getParameters() )
.as( "collection-type should include typedef parameters" )
.hasSize( 1 );
} );
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.orm.test.boot.jaxb.mapping.hqlimport;

public class Dog {
Long id;
String name;
String breed;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.orm.test.boot.jaxb.mapping.unmappedsuperclass;

public class AbstractBase {
private Long id;
private String name;
private int version;
private AbstractBase relatedBase;
private String unmappedProperty;

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getVersion() {
return version;
}

public void setVersion(int version) {
this.version = version;
}

public AbstractBase getRelatedBase() {
return relatedBase;
}

public void setRelatedBase(AbstractBase relatedBase) {
this.relatedBase = relatedBase;
}

public String getUnmappedProperty() {
return unmappedProperty;
}

public void setUnmappedProperty(String unmappedProperty) {
this.unmappedProperty = unmappedProperty;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.orm.test.boot.jaxb.mapping.unmappedsuperclass;

public class AnotherEntity extends AbstractBase {
private String code;

public String getCode() {
return code;
}

public void setCode(String code) {
this.code = code;
}
}
Loading