diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/CollectionPropertyHolder.java b/hibernate-core/src/main/java/org/hibernate/cfg/CollectionPropertyHolder.java index f63462d5b8..51cae19b07 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/CollectionPropertyHolder.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/CollectionPropertyHolder.java @@ -42,6 +42,7 @@ import org.hibernate.annotations.ManyToAny; import org.hibernate.annotations.MapKeyType; import org.hibernate.annotations.common.reflection.XClass; import org.hibernate.annotations.common.reflection.XProperty; +import org.hibernate.boot.model.source.spi.AttributePath; import org.hibernate.boot.spi.MetadataBuildingContext; import org.hibernate.internal.CoreLogging; import org.hibernate.internal.CoreMessageLogger; @@ -143,6 +144,7 @@ public class CollectionPropertyHolder extends AbstractPropertyHolder { } if ( StringHelper.isEmpty( info.getAttributeName() ) ) { + // the @Convert did not name an attribute... if ( canElementBeConverted && canKeyBeConverted ) { throw new IllegalStateException( "@Convert placed on Map attribute [" + collection.getRole() @@ -158,30 +160,46 @@ public class CollectionPropertyHolder extends AbstractPropertyHolder { // if neither, we should not be here... } else { - if ( canElementBeConverted && canKeyBeConverted ) { + // the @Convert named an attribute... + final String keyPath = removePrefix( info.getAttributeName(), "key" ); + final String elementPath = removePrefix( info.getAttributeName(), "value" ); + + if ( canElementBeConverted && canKeyBeConverted && keyPath == null && elementPath == null ) { // specified attributeName needs to have 'key.' or 'value.' prefix - if ( info.getAttributeName().startsWith( "key." ) ) { - keyAttributeConversionInfoMap.put( - info.getAttributeName().substring( 4 ), - info - ); - } - else if ( info.getAttributeName().startsWith( "value." ) ) { - elementAttributeConversionInfoMap.put( - info.getAttributeName().substring( 6 ), - info - ); - } - else { - throw new IllegalStateException( - "@Convert placed on Map attribute [" + collection.getRole() - + "] must define attributeName of 'key' or 'value'" - ); - } + throw new IllegalStateException( + "@Convert placed on Map attribute [" + collection.getRole() + + "] must define attributeName of 'key' or 'value'" + ); + } + + if ( keyPath != null ) { + keyAttributeConversionInfoMap.put( keyPath, info ); + } + else if ( elementPath != null ) { + elementAttributeConversionInfoMap.put( elementPath, info ); } } } + /** + * Check if path has the given prefix and remove it. + * + * @param path Path. + * @param prefix Prefix. + * @return Path without prefix, or null, if path did not have the prefix. + */ + private String removePrefix(String path, String prefix) { + if ( path.equals(prefix) ) { + return ""; + } + + if (path.startsWith(prefix + ".")) { + return path.substring( prefix.length() + 1 ); + } + + return null; + } + @Override protected String normalizeCompositePath(String attributeName) { return attributeName; @@ -219,8 +237,17 @@ public class CollectionPropertyHolder extends AbstractPropertyHolder { @Override protected AttributeConversionInfo locateAttributeConversionInfo(String path) { - // todo : implement - return null; + final String key = removePrefix( path, "key" ); + if ( key != null ) { + return keyAttributeConversionInfoMap.get( key ); + } + + final String element = removePrefix( path, "element" ); + if ( element != null ) { + return elementAttributeConversionInfoMap.get( element ); + } + + return elementAttributeConversionInfoMap.get( path ); } public String getClassName() { diff --git a/hibernate-core/src/test/java/org/hibernate/test/type/converter/ElementCollectionTests.java b/hibernate-core/src/test/java/org/hibernate/test/type/converter/ElementCollectionTests.java new file mode 100644 index 0000000000..895de889b6 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/type/converter/ElementCollectionTests.java @@ -0,0 +1,201 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * Copyright (c) 2015, 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.test.type.converter; + +import java.net.MalformedURLException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import javax.persistence.AttributeConverter; +import javax.persistence.CollectionTable; +import javax.persistence.Column; +import javax.persistence.Convert; +import javax.persistence.Converts; +import javax.persistence.ElementCollection; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.MapKeyColumn; +import javax.persistence.Table; + +import org.hibernate.Session; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.mapping.Collection; +import org.hibernate.mapping.IndexedCollection; +import org.hibernate.mapping.PersistentClass; +import org.hibernate.mapping.Property; +import org.hibernate.type.descriptor.converter.AttributeConverterTypeAdapter; + +import org.hibernate.testing.TestForIssue; +import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase; +import org.junit.Test; + +import static org.hibernate.testing.junit4.ExtraAssertions.assertTyping; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * Test for {@link org.hibernate.cfg.CollectionPropertyHolder}. + * + * Tests that {@link javax.persistence.AttributeConverter}s are considered correctly for {@link javax.persistence.ElementCollection}. + * + * @author Markus Heiden + * @author Steve Ebersole + */ +@TestForIssue( jiraKey = "HHH-9495" ) +public class ElementCollectionTests extends BaseNonConfigCoreFunctionalTestCase { + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { TheEntity.class }; + } + + @Test + public void testSimpleConvertUsage() throws MalformedURLException { + // first some assertions of the metamodel + PersistentClass entityBinding = metadata().getEntityBinding( TheEntity.class.getName() ); + assertNotNull( entityBinding ); + + Property setAttributeBinding = entityBinding.getProperty( "set" ); + Collection setBinding = (Collection) setAttributeBinding.getValue(); + assertTyping( AttributeConverterTypeAdapter.class, setBinding.getElement().getType() ); + + Property mapAttributeBinding = entityBinding.getProperty( "map" ); + IndexedCollection mapBinding = (IndexedCollection) mapAttributeBinding.getValue(); + assertTyping( AttributeConverterTypeAdapter.class, mapBinding.getIndex().getType() ); + assertTyping( AttributeConverterTypeAdapter.class, mapBinding.getElement().getType() ); + + // now lets try to use the model, integration-testing-style! + TheEntity entity = new TheEntity(1); + + Session s = openSession(); + s.beginTransaction(); + s.save( entity ); + s.getTransaction().commit(); + s.close(); + + s = openSession(); + s.beginTransaction(); + TheEntity retrieved = (TheEntity) s.load( TheEntity.class, 1 ); + assertEquals( 1, retrieved.getSet().size() ); + assertEquals(new ValueType("set_value"), retrieved.getSet().iterator().next()); + assertEquals(1, retrieved.getMap().size()); + assertEquals(new ValueType("map_value"), retrieved.getMap().get(new ValueType("map_key"))); + s.delete( retrieved ); + s.getTransaction().commit(); + s.close(); + } + + /** + * Non-serializable value type. + */ + public static class ValueType { + private final String value; + + public ValueType(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + @Override + public boolean equals(Object o) { + return o instanceof ValueType && + value.equals(((ValueType) o).value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + } + + /** + * Converter for {@link ValueType}. + */ + public static class ValueTypeConverter implements AttributeConverter { + @Override + public String convertToDatabaseColumn(ValueType type) { + return type.getValue(); + } + + @Override + public ValueType convertToEntityAttribute(String type) { + return new ValueType(type); + } + } + + /** + * Entity holding element collections. + */ + @Entity( name = "TheEntity" ) + @Table(name = "entity") + public static class TheEntity { + @Id + public Integer id; + + /** + * Element set with converter. + */ + @Convert( converter = ValueTypeConverter.class ) + @ElementCollection(fetch = FetchType.LAZY) + @CollectionTable(name = "entity_set", joinColumns = @JoinColumn(name = "entity_id", nullable = false)) + @Column(name = "value", nullable = false) + public Set set = new HashSet(); + + /** + * Element map with converters. + */ + @Converts({ + @Convert(attributeName = "key", converter = ValueTypeConverter.class), + @Convert(attributeName = "value", converter = ValueTypeConverter.class) + }) + @ElementCollection(fetch = FetchType.LAZY) + @CollectionTable(name = "entity_map", joinColumns = @JoinColumn(name = "entity_id", nullable = false)) + @MapKeyColumn(name = "key", nullable = false) + @Column(name = "value", nullable = false) + public Map map = new HashMap(); + + public TheEntity() { + } + + public TheEntity(Integer id) { + this.id = id; + this.set.add(new ValueType("set_value")); + this.map.put( new ValueType( "map_key" ), new ValueType( "map_value" ) ); + } + + public Set getSet() { + return set; + } + + public Map getMap() { + return map; + } + } +}