HHH-8111 - AttributeConverter doesn't override built-in type mappings

This commit is contained in:
Steve Ebersole 2013-08-21 13:14:23 -05:00
parent 8ccbd7d121
commit 9f86babd88
6 changed files with 466 additions and 31 deletions

View File

@ -121,4 +121,15 @@ public class AttributeConverterDefinition {
return (Class) boundTypes[0];
}
@Override
public String toString() {
return String.format(
"%s[converterClass=%s, domainType=%s, jdbcType=%s]",
this.getClass().getName(),
attributeConverter.getClass().getName(),
entityAttributeType.getName(),
databaseColumnType.getName()
);
}
}

View File

@ -306,25 +306,51 @@ public class SimpleValueBinder {
}
private void applyAttributeConverter(XProperty property) {
final boolean canBeConverted = ! property.isAnnotationPresent( Id.class )
&& ! isVersion
&& ! isAssociation()
&& ! property.isAnnotationPresent( Temporal.class )
&& ! property.isAnnotationPresent( Enumerated.class );
if ( attributeConverterDefinition != null ) {
return;
}
if ( canBeConverted ) {
// @Convert annotations take precedence
LOG.debugf( "Starting applyAttributeConverter [%s:%s]", persistentClassName, property.getName() );
if ( property.isAnnotationPresent( Id.class ) ) {
LOG.debugf( "Skipping AttributeConverter checks for Id attribute [%s]", property.getName() );
return;
}
if ( isVersion ) {
LOG.debugf( "Skipping AttributeConverter checks for version attribute [%s]", property.getName() );
return;
}
if ( property.isAnnotationPresent( Temporal.class ) ) {
LOG.debugf( "Skipping AttributeConverter checks for Temporal attribute [%s]", property.getName() );
return;
}
if ( property.isAnnotationPresent( Enumerated.class ) ) {
LOG.debugf( "Skipping AttributeConverter checks for Enumerated attribute [%s]", property.getName() );
return;
}
if ( isAssociation() ) {
LOG.debugf( "Skipping AttributeConverter checks for association attribute [%s]", property.getName() );
return;
}
// @Convert annotations take precedence if present
final Convert convertAnnotation = locateConvertAnnotation( property );
if ( convertAnnotation != null ) {
if ( ! convertAnnotation.disableConversion() ) {
LOG.debugf(
"Applying located @Convert AttributeConverter [%s] to attribute [%]",
convertAnnotation.converter().getName(),
property.getName()
);
attributeConverterDefinition = mappings.locateAttributeConverter( convertAnnotation.converter() );
}
}
else {
attributeConverterDefinition = locateAutoApplyAttributeConverter( property );
}
}
}
private AttributeConverterDefinition locateAutoApplyAttributeConverter(XProperty property) {
final Class propertyType = mappings.getReflectionManager().toClass( property.getType() );
@ -347,10 +373,41 @@ public class SimpleValueBinder {
@SuppressWarnings("unchecked")
private Convert locateConvertAnnotation(XProperty property) {
// first look locally on the property for @Convert
LOG.debugf(
"Attempting to locate Convert annotation for property [%s:%s]",
persistentClassName,
property.getName()
);
// first look locally on the property for @Convert/@Converts
{
Convert localConvertAnnotation = property.getAnnotation( Convert.class );
if ( localConvertAnnotation != null ) {
return localConvertAnnotation;
LOG.debugf(
"Found matching local @Convert annotation [disableConversion=%s]",
localConvertAnnotation.disableConversion()
);
return localConvertAnnotation.disableConversion()
? null
: localConvertAnnotation;
}
}
{
Converts localConvertsAnnotation = property.getAnnotation( Converts.class );
if ( localConvertsAnnotation != null ) {
for ( Convert localConvertAnnotation : localConvertsAnnotation.value() ) {
if ( isLocalMatch( localConvertAnnotation, property ) ) {
LOG.debugf(
"Found matching @Convert annotation as part local @Converts [disableConversion=%s]",
localConvertAnnotation.disableConversion()
);
return localConvertAnnotation.disableConversion()
? null
: localConvertAnnotation;
}
}
}
}
if ( persistentClassName == null ) {
@ -376,10 +433,20 @@ public class SimpleValueBinder {
return null;
}
LOG.debugf(
"Attempting to locate any AttributeConverter defined via @Convert/@Converts on type-hierarchy [%s] to apply to attribute [%s]",
owner.getName(),
property.getName()
);
{
Convert convertAnnotation = owner.getAnnotation( Convert.class );
if ( convertAnnotation != null && isMatch( convertAnnotation, property ) ) {
return convertAnnotation;
LOG.debugf(
"Found matching @Convert annotation [disableConversion=%s]",
convertAnnotation.disableConversion()
);
return convertAnnotation.disableConversion() ? null : convertAnnotation;
}
}
@ -388,7 +455,11 @@ public class SimpleValueBinder {
if ( convertsAnnotation != null ) {
for ( Convert convertAnnotation : convertsAnnotation.value() ) {
if ( isMatch( convertAnnotation, property ) ) {
return convertAnnotation;
LOG.debugf(
"Found matching @Convert annotation as part @Converts [disableConversion=%s]",
convertAnnotation.disableConversion()
);
return convertAnnotation.disableConversion() ? null : convertAnnotation;
}
}
}
@ -399,11 +470,22 @@ public class SimpleValueBinder {
}
@SuppressWarnings("unchecked")
private boolean isMatch(Convert convertAnnotation, XProperty property) {
private boolean isLocalMatch(Convert convertAnnotation, XProperty property) {
if ( StringHelper.isEmpty( convertAnnotation.attributeName() ) ) {
return isTypeMatch( convertAnnotation.converter(), property );
}
return property.getName().equals( convertAnnotation.attributeName() )
&& isTypeMatch( convertAnnotation.converter(), property );
}
@SuppressWarnings("unchecked")
private boolean isMatch(Convert convertAnnotation, XProperty property) {
return property.getName().equals( convertAnnotation.attributeName() );
// return property.getName().equals( convertAnnotation.attributeName() )
// && isTypeMatch( convertAnnotation.converter(), property );
}
private boolean isTypeMatch(Class<? extends AttributeConverter> attributeConverterClass, XProperty property) {
return areTypeMatch(
extractEntityAttributeType( attributeConverterClass ),
@ -577,7 +659,7 @@ public class SimpleValueBinder {
}
public void fillSimpleValue() {
LOG.debugf( "Setting SimpleValue typeName for %s", propertyName );
LOG.debugf( "Starting fillSimpleValue for %s", propertyName );
if ( attributeConverterDefinition != null ) {
if ( ! BinderHelper.isEmptyAnnotationValue( explicitType ) ) {
@ -590,6 +672,12 @@ public class SimpleValueBinder {
)
);
}
LOG.debugf(
"Applying JPA AttributeConverter [%s] to [%s:%s]",
attributeConverterDefinition,
persistentClassName,
propertyName
);
simpleValue.setJpaAttributeConverterDefinition( attributeConverterDefinition );
}
else {

View File

@ -437,8 +437,17 @@ public class SimpleValue implements KeyValue {
// todo : cache the AttributeConverterTypeAdapter in case that AttributeConverter is applied multiple times.
final String name = "BasicType adapter for AttributeConverter<" + entityAttributeJavaType + "," + databaseColumnJavaType + ">";
return new AttributeConverterTypeAdapter( sqlTypeDescriptorAdapter, entityAttributeJavaTypeDescriptor, name );
final String name = String.format(
"BasicType adapter for AttributeConverter<%s,%s>",
entityAttributeJavaType.getSimpleName(),
databaseColumnJavaType.getSimpleName()
);
return new AttributeConverterTypeAdapter(
name,
jpaAttributeConverterDefinition.getAttributeConverter(),
sqlTypeDescriptorAdapter,
entityAttributeJavaTypeDescriptor
);
}
public boolean isTypeSpecified() {

View File

@ -23,6 +23,8 @@
*/
package org.hibernate.type.descriptor.converter;
import javax.persistence.AttributeConverter;
import org.jboss.logging.Logger;
import org.hibernate.type.AbstractSingleColumnStandardBasicType;
@ -30,19 +32,24 @@ import org.hibernate.type.descriptor.java.JavaTypeDescriptor;
import org.hibernate.type.descriptor.sql.SqlTypeDescriptor;
/**
* Adapts the Hibernate Type contract to incorporate JPA AttributeConverter calls.
*
* @author Steve Ebersole
*/
public class AttributeConverterTypeAdapter extends AbstractSingleColumnStandardBasicType {
public class AttributeConverterTypeAdapter<T> extends AbstractSingleColumnStandardBasicType<T> {
private static final Logger log = Logger.getLogger( AttributeConverterTypeAdapter.class );
private final String name;
private final AttributeConverter<? extends T,?> attributeConverter;
public AttributeConverterTypeAdapter(
SqlTypeDescriptor sqlTypeDescriptor,
JavaTypeDescriptor javaTypeDescriptor,
String name) {
super( sqlTypeDescriptor, javaTypeDescriptor );
String name,
AttributeConverter<? extends T,?> attributeConverter,
SqlTypeDescriptor sqlTypeDescriptorAdapter,
JavaTypeDescriptor<T> entityAttributeJavaTypeDescriptor) {
super( sqlTypeDescriptorAdapter, entityAttributeJavaTypeDescriptor );
this.name = name;
this.attributeConverter = attributeConverter;
log.debug( "Created AttributeConverterTypeAdapter -> " + name );
}
@ -51,4 +58,8 @@ public class AttributeConverterTypeAdapter extends AbstractSingleColumnStandardB
public String getName() {
return name;
}
public AttributeConverter<? extends T,?> getAttributeConverter() {
return attributeConverter;
}
}

View File

@ -0,0 +1,157 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* Copyright (c) 2013, Red Hat Inc. or third-party contributors as
* indicated by the @author tags or express copyright attribution
* statements applied by the authors. All third-party contributions are
* distributed under license by Red Hat Inc.
*
* This copyrighted material is made available to anyone wishing to use, modify,
* copy, or redistribute it subject to the terms and conditions of the GNU
* Lesser General Public License, as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this distribution; if not, write to:
* Free Software Foundation, Inc.
* 51 Franklin Street, Fifth Floor
* Boston, MA 02110-1301 USA
*/
package org.hibernate.jpa.test.convert;
import javax.persistence.AttributeConverter;
import javax.persistence.Convert;
import javax.persistence.Converter;
import javax.persistence.Entity;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Id;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.jpa.boot.spi.Bootstrap;
import org.hibernate.jpa.test.PersistenceUnitDescriptorAdapter;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.type.Type;
import org.hibernate.type.descriptor.converter.AttributeConverterTypeAdapter;
import org.junit.Test;
import org.hibernate.testing.junit4.BaseUnitTestCase;
import static org.hibernate.testing.junit4.ExtraAssertions.assertTyping;
import static org.junit.Assert.assertEquals;
/**
* @author Steve Ebersole
*/
public class SimpleConvertAnnotationTest extends BaseUnitTestCase {
// test handling of an AttributeConverter explicitly named via a @Convert annotation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@Test
public void testSimpleConvertUsage() throws MalformedURLException {
final PersistenceUnitDescriptorAdapter pu = new PersistenceUnitDescriptorAdapter() {
@Override
public List<String> getManagedClassNames() {
return Arrays.asList( Entity1.class.getName(), UrlConverter.class.getName(), AutoUrlConverter.class.getName() );
}
};
final Map settings = new HashMap();
settings.put( AvailableSettings.HBM2DDL_AUTO, "create-drop" );
EntityManagerFactory emf = Bootstrap.getEntityManagerFactoryBuilder( pu, settings ).build();
final EntityPersister ep = emf.unwrap( SessionFactoryImplementor.class ).getEntityPersister( Entity1.class.getName() );
final Type websitePropertyType = ep.getPropertyType( "website" );
final AttributeConverterTypeAdapter type = assertTyping( AttributeConverterTypeAdapter.class, websitePropertyType );
assertTyping( UrlConverter.class, type.getAttributeConverter() );
try {
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
em.persist( new Entity1( 1, "1", new URL( "http://hibernate.org" ) ) );
em.getTransaction().commit();
em.close();
assertEquals( 1, callsToConverter );
em = emf.createEntityManager();
em.getTransaction().begin();
em.createQuery( "delete Entity1" ).executeUpdate();
em.getTransaction().commit();
em.close();
}
finally {
emf.close();
}
}
static int callsToConverter = 0;
@Converter(autoApply = false)
public static class UrlConverter implements AttributeConverter<URL,String> {
@Override
public String convertToDatabaseColumn(URL attribute) {
callsToConverter++;
return attribute == null ? null : attribute.toExternalForm();
}
@Override
public URL convertToEntityAttribute(String dbData) {
callsToConverter++;
if ( dbData == null ) {
return null;
}
try {
return new URL( dbData );
}
catch (MalformedURLException e) {
throw new IllegalArgumentException( "Could not convert incoming value to URL : " + dbData );
}
}
}
@Converter( autoApply = true )
public static class AutoUrlConverter implements AttributeConverter<URL,String> {
@Override
public String convertToDatabaseColumn(URL attribute) {
throw new IllegalStateException( "Should not be called" );
}
@Override
public URL convertToEntityAttribute(String dbData) {
throw new IllegalStateException( "Should not be called" );
}
}
@Entity( name = "Entity1" )
public static class Entity1 {
@Id
private Integer id;
private String name;
@Convert( converter = UrlConverter.class )
private URL website;
public Entity1() {
}
public Entity1(Integer id, String name, URL website) {
this.id = id;
this.name = name;
this.website = website;
}
}
}

View File

@ -0,0 +1,159 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* Copyright (c) 2013, Red Hat Inc. or third-party contributors as
* indicated by the @author tags or express copyright attribution
* statements applied by the authors. All third-party contributions are
* distributed under license by Red Hat Inc.
*
* This copyrighted material is made available to anyone wishing to use, modify,
* copy, or redistribute it subject to the terms and conditions of the GNU
* Lesser General Public License, as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this distribution; if not, write to:
* Free Software Foundation, Inc.
* 51 Franklin Street, Fifth Floor
* Boston, MA 02110-1301 USA
*/
package org.hibernate.jpa.test.convert;
import javax.persistence.AttributeConverter;
import javax.persistence.Convert;
import javax.persistence.Converter;
import javax.persistence.Converts;
import javax.persistence.Entity;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Id;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.jpa.boot.spi.Bootstrap;
import org.hibernate.jpa.test.PersistenceUnitDescriptorAdapter;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.type.Type;
import org.hibernate.type.descriptor.converter.AttributeConverterTypeAdapter;
import org.junit.Test;
import org.hibernate.testing.junit4.BaseUnitTestCase;
import static org.hibernate.testing.junit4.ExtraAssertions.assertTyping;
import static org.junit.Assert.assertEquals;
/**
* @author Steve Ebersole
*/
public class SimpleConvertsAnnotationTest extends BaseUnitTestCase {
// test handling of an AttributeConverter explicitly named via a @Convert annotation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@Test
public void testSimpleConvertUsage() throws MalformedURLException {
final PersistenceUnitDescriptorAdapter pu = new PersistenceUnitDescriptorAdapter() {
@Override
public List<String> getManagedClassNames() {
return Arrays.asList( Entity1.class.getName(), UrlConverter.class.getName(), AutoUrlConverter.class.getName() );
}
};
final Map settings = new HashMap();
settings.put( AvailableSettings.HBM2DDL_AUTO, "create-drop" );
EntityManagerFactory emf = Bootstrap.getEntityManagerFactoryBuilder( pu, settings ).build();
final EntityPersister ep = emf.unwrap( SessionFactoryImplementor.class ).getEntityPersister( Entity1.class.getName() );
final Type websitePropertyType = ep.getPropertyType( "website" );
final AttributeConverterTypeAdapter type = assertTyping( AttributeConverterTypeAdapter.class, websitePropertyType );
assertTyping( UrlConverter.class, type.getAttributeConverter() );
try {
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
em.persist( new Entity1( 1, "1", new URL( "http://hibernate.org" ) ) );
em.getTransaction().commit();
em.close();
assertEquals( 1, callsToConverter );
em = emf.createEntityManager();
em.getTransaction().begin();
em.createQuery( "delete Entity1" ).executeUpdate();
em.getTransaction().commit();
em.close();
}
finally {
emf.close();
}
}
static int callsToConverter = 0;
@Converter(autoApply = false)
public static class UrlConverter implements AttributeConverter<URL,String> {
@Override
public String convertToDatabaseColumn(URL attribute) {
callsToConverter++;
return attribute == null ? null : attribute.toExternalForm();
}
@Override
public URL convertToEntityAttribute(String dbData) {
callsToConverter++;
if ( dbData == null ) {
return null;
}
try {
return new URL( dbData );
}
catch (MalformedURLException e) {
throw new IllegalArgumentException( "Could not convert incoming value to URL : " + dbData );
}
}
}
@Converter( autoApply = true )
public static class AutoUrlConverter implements AttributeConverter<URL,String> {
@Override
public String convertToDatabaseColumn(URL attribute) {
throw new IllegalStateException( "Should not be called" );
}
@Override
public URL convertToEntityAttribute(String dbData) {
throw new IllegalStateException( "Should not be called" );
}
}
@Entity( name = "Entity1" )
@Converts(
@Convert( attributeName = "website", converter = UrlConverter.class )
)
public static class Entity1 {
@Id
private Integer id;
private String name;
private URL website;
public Entity1() {
}
public Entity1(Integer id, String name, URL website) {
this.id = id;
this.name = name;
this.website = website;
}
}
}