HHH-9241 - Allow declaring non-java.util Collection interfaces

This commit is contained in:
thymepuns 2014-06-12 14:59:07 -07:00 committed by Guillaume Smet
parent bc6289e0b8
commit 5b76256e96
10 changed files with 992 additions and 50 deletions

View File

@ -33,9 +33,21 @@ public @interface CollectionType {
*/
String type();
/**
* Specifies the class to use the semantics of.
*
* For example, specifying {@link java.util.Set} will use Set semantics.
*
* When not specified, will be inferred from the interfaces on the property
* as long as it extends a standard {@link java.util.Collection} or {@link java.util.Map}.
*
* @return the class to use the semantics of.
*/
Class<?> semantics() default void.class;
/**
* Specifies configuration information for the type. Note that if the named type is a
* {@link org.hibernate.usertype.UserCollectionType}, it must also implement
* {@link org.hibernate.usertype.UserCollectionType}, it must also implement
* {@link org.hibernate.usertype.ParameterizedType} in order to receive these values.
*/
Parameter[] parameters() default {};

View File

@ -6,9 +6,12 @@
*/
package org.hibernate.cfg.annotations;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
@ -29,6 +32,7 @@ import javax.persistence.OneToMany;
import org.hibernate.AnnotationException;
import org.hibernate.FetchMode;
import org.hibernate.HibernateException;
import org.hibernate.MappingException;
import org.hibernate.annotations.BatchSize;
import org.hibernate.annotations.Cache;
@ -88,6 +92,7 @@ import org.hibernate.criterion.Junction;
import org.hibernate.engine.config.spi.ConfigurationService;
import org.hibernate.engine.spi.ExecuteUpdateResultCheckStyle;
import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.internal.util.ReflectHelper;
import org.hibernate.internal.util.StringHelper;
import org.hibernate.internal.util.config.ConfigurationHelper;
import org.hibernate.mapping.Any;
@ -119,6 +124,14 @@ import static org.hibernate.cfg.BinderHelper.toAliasTableMap;
public abstract class CollectionBinder {
private static final CoreMessageLogger LOG = Logger.getMessageLogger(CoreMessageLogger.class, CollectionBinder.class.getName());
private static final List<Class<?>> INFERRED_CLASS_PRIORITY = Collections.unmodifiableList( Arrays.asList(
java.util.List.class,
java.util.SortedSet.class,
java.util.Set.class,
java.util.SortedMap.class,
java.util.Map.class,
java.util.Collection.class) );
private MetadataBuildingContext buildingContext;
protected Collection collection;
@ -256,6 +269,7 @@ public abstract class CollectionBinder {
boolean isHibernateExtensionMapping,
MetadataBuildingContext buildingContext) {
final CollectionBinder result;
if ( property.isArray() ) {
if ( property.getElementClass().isPrimitive() ) {
result = new PrimitiveArrayBinder();
@ -266,62 +280,39 @@ public abstract class CollectionBinder {
}
else if ( property.isCollection() ) {
//TODO consider using an XClass
Class returnedClass = property.getCollectionClass();
if ( java.util.Set.class.equals( returnedClass ) ) {
if ( property.isAnnotationPresent( CollectionId.class ) ) {
throw new AnnotationException( "Set do not support @CollectionId: "
+ StringHelper.qualify( entityName, property.getName() ) );
}
result = new SetBinder( false );
final Class<?> returnedClass = property.getCollectionClass();
final CollectionBinder basicBinder = getBinderFromBasicCollectionType(
returnedClass,
property,
entityName,
isIndexed
);
if ( basicBinder != null ) {
result = basicBinder;
}
else if ( java.util.SortedSet.class.equals( returnedClass ) ) {
if ( property.isAnnotationPresent( CollectionId.class ) ) {
throw new AnnotationException( "Set do not support @CollectionId: "
+ StringHelper.qualify( entityName, property.getName() ) );
}
result = new SetBinder( true );
}
else if ( java.util.Map.class.equals( returnedClass ) ) {
if ( property.isAnnotationPresent( CollectionId.class ) ) {
throw new AnnotationException( "Map do not support @CollectionId: "
+ StringHelper.qualify( entityName, property.getName() ) );
}
result = new MapBinder( false );
}
else if ( java.util.SortedMap.class.equals( returnedClass ) ) {
if ( property.isAnnotationPresent( CollectionId.class ) ) {
throw new AnnotationException( "Map do not support @CollectionId: "
+ StringHelper.qualify( entityName, property.getName() ) );
}
result = new MapBinder( true );
}
else if ( java.util.Collection.class.equals( returnedClass ) ) {
if ( property.isAnnotationPresent( CollectionId.class ) ) {
result = new IdBagBinder();
else if ( property.isAnnotationPresent( CollectionType.class ) ) {
Class<?> semanticsClass = property.getAnnotation( CollectionType.class ).semantics();
if ( semanticsClass != void.class ) {
result = getBinderFromBasicCollectionType( semanticsClass, property, entityName, isIndexed );
}
else {
result = new BagBinder();
}
}
else if ( java.util.List.class.equals( returnedClass ) ) {
if ( isIndexed ) {
if ( property.isAnnotationPresent( CollectionId.class ) ) {
throw new AnnotationException(
"List do not support @CollectionId and @OrderColumn (or @IndexColumn) at the same time: "
+ StringHelper.qualify( entityName, property.getName() ) );
}
result = new ListBinder();
}
else if ( property.isAnnotationPresent( CollectionId.class ) ) {
result = new IdBagBinder();
}
else {
result = new BagBinder();
final Class<?> inferredClass = inferCollectionClassFromSubclass( returnedClass );
result = inferredClass != null ? getBinderFromBasicCollectionType(
inferredClass,
property,
entityName,
isIndexed
) : null;
}
}
else {
result = null;
}
if ( result == null ) {
throw new AnnotationException(
returnedClass.getName() + " collection not yet supported: "
returnedClass.getName() + " collection type not supported for property: "
+ StringHelper.qualify( entityName, property.getName() )
);
}
@ -354,6 +345,72 @@ public abstract class CollectionBinder {
return result;
}
private static CollectionBinder getBinderFromBasicCollectionType(Class<?> clazz, XProperty property,
String entityName, boolean isIndexed) {
if ( java.util.Set.class.equals( clazz) ) {
if ( property.isAnnotationPresent( CollectionId.class) ) {
throw new AnnotationException("Set do not support @CollectionId: "
+ StringHelper.qualify( entityName, property.getName() ) );
}
return new SetBinder( false );
}
else if ( java.util.SortedSet.class.equals( clazz ) ) {
if ( property.isAnnotationPresent( CollectionId.class ) ) {
throw new AnnotationException( "Set do not support @CollectionId: "
+ StringHelper.qualify( entityName, property.getName() ) );
}
return new SetBinder( true );
}
else if ( java.util.Map.class.equals( clazz ) ) {
if ( property.isAnnotationPresent( CollectionId.class ) ) {
throw new AnnotationException( "Map do not support @CollectionId: "
+ StringHelper.qualify( entityName, property.getName() ) );
}
return new MapBinder( false );
}
else if ( java.util.SortedMap.class.equals( clazz ) ) {
if ( property.isAnnotationPresent( CollectionId.class ) ) {
throw new AnnotationException( "Map do not support @CollectionId: "
+ StringHelper.qualify( entityName, property.getName() ) );
}
return new MapBinder( true );
}
else if ( java.util.Collection.class.equals( clazz ) ) {
if ( property.isAnnotationPresent( CollectionId.class ) ) {
return new IdBagBinder();
}
else {
return new BagBinder();
}
}
else if ( java.util.List.class.equals( clazz ) ) {
if ( isIndexed ) {
if ( property.isAnnotationPresent( CollectionId.class ) ) {
throw new AnnotationException(
"List do not support @CollectionId and @OrderColumn (or @IndexColumn) at the same time: "
+ StringHelper.qualify( entityName, property.getName() ) );
}
return new ListBinder();
}
else if ( property.isAnnotationPresent( CollectionId.class ) ) {
return new IdBagBinder();
}
else {
return new BagBinder();
}
}
return null;
}
private static Class<?> inferCollectionClassFromSubclass(Class<?> clazz) {
for ( Class<?> priorityClass : INFERRED_CLASS_PRIORITY ) {
if ( priorityClass.isAssignableFrom( clazz ) ) {
return priorityClass;
}
}
return null;
}
public void setMappedBy(String mappedBy) {
this.mappedBy = mappedBy;
}

View File

@ -0,0 +1,54 @@
package org.hibernate.test.collection.custom.declaredtype;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
/**
* @author Gavin King
* @author Steve Ebersole
*/
@Entity
public class Email {
private Long id;
private String address;
Email() {
}
public Email(String address) {
this.address = address;
}
@Id
@GeneratedValue( strategy = GenerationType.AUTO )
public Long getId() {
return id;
}
private void setId(Long id) {
this.id = id;
}
public String getAddress() {
return address;
}
public void setAddress(String type) {
this.address = type;
}
@Override
public boolean equals(Object that) {
if ( !(that instanceof Email ) ) return false;
Email p = (Email) that;
return this.address.equals(p.address);
}
@Override
public int hashCode() {
return address.hashCode();
}
}

View File

@ -0,0 +1,34 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* Copyright (c) ${year}, 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.collection.custom.declaredtype;
/**
* @author Steve Ebersole
*/
public class UserCollectionTypeAnnotationsVariantTest extends UserCollectionTypeTest {
@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class[] { User.class, Email.class };
}
}

View File

@ -0,0 +1,34 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* Copyright (c) ${year}, 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.collection.custom.declaredtype;
/**
* @author Steve Ebersole
*/
public class UserCollectionTypeHbmVariantTest extends UserCollectionTypeTest {
@Override
public String[] getMappings() {
return new String[] { "collection/custom/declaredtype/UserPermissions.hbm.xml" };
}
}

View File

@ -0,0 +1,225 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* Copyright (c) 2011, 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.collection.custom.declaredtype;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToMany;
import javax.persistence.OrderColumn;
import javax.persistence.Table;
import javax.persistence.Transient;
import org.hibernate.Hibernate;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.hibernate.annotations.CollectionType;
import org.hibernate.annotations.TypeDef;
import org.hibernate.collection.internal.PersistentList;
import org.hibernate.collection.spi.PersistentCollection;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.persister.collection.CollectionPersister;
import org.hibernate.usertype.UserCollectionType;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
/**
* @author Max Rydahl Andersen
* @author David Weinberg
*/
public abstract class UserCollectionTypeTest extends BaseCoreFunctionalTestCase {
@Override
protected String getCacheConcurrencyStrategy() {
return "nonstrict-read-write";
}
@Test
public void testBasicOperation() {
Session s = openSession();
Transaction t = s.beginTransaction();
User u = new User("max");
u.getEmailAddresses().add( new Email("max@hibernate.org") );
u.getEmailAddresses().add( new Email("max.andersen@jboss.com") );
s.persist(u);
t.commit();
s.close();
s = openSession();
t = s.beginTransaction();
User u2 = (User) s.createCriteria(User.class).uniqueResult();
assertTrue( Hibernate.isInitialized( u2.getEmailAddresses() ) );
assertEquals( u2.getEmailAddresses().size(), 2 );
assertNotNull( u2.getEmailAddresses().head());
t.commit();
s.close();
}
/**
* A custom collection class that implements a simple method just for illustration.
* We extend a java.util.Collection class which is required for annotations-based entities, but not xml-based.
*
* @author David Weinberg
*/
public static class HeadList<X> extends ArrayList<X> implements IHeadList<X> {
@Override
public X head() {
return isEmpty() ? null : get( 0 );
}
}
public static class HeadListType implements UserCollectionType {
@Override
public PersistentCollection instantiate(SharedSessionContractImplementor session, CollectionPersister persister) throws HibernateException {
return new PersistentHeadList(session);
}
@Override
public PersistentCollection wrap(SharedSessionContractImplementor session, Object collection) {
return new PersistentHeadList( session, (IHeadList) collection );
}
public Iterator getElementsIterator(Object collection) {
return ( (IHeadList) collection ).iterator();
}
public boolean contains(Object collection, Object entity) {
return ( (IHeadList) collection ).contains(entity);
}
public Object indexOf(Object collection, Object entity) {
int l = ( (IHeadList) collection ).indexOf(entity);
if(l<0) {
return null;
} else {
return l;
}
}
@Override
public Object replaceElements(
Object original,
Object target,
CollectionPersister persister,
Object owner,
Map copyCache,
SharedSessionContractImplementor session) throws HibernateException {
IHeadList result = (IHeadList) target;
result.clear();
result.addAll( (HeadList) original );
return result;
}
public Object instantiate(int anticipatedSize) {
return new HeadList();
}
}
public interface IHeadList<X> extends List<X> {
X head();
}
public static class PersistentHeadList extends PersistentList implements IHeadList {
public PersistentHeadList(SharedSessionContractImplementor session) {
super(session);
}
public PersistentHeadList(SharedSessionContractImplementor session, IHeadList list) {
super(session, list);
}
@Override
public Object head() {
return ((IHeadList) list).head();
}
}
/**
* @author Gavin King
* @author Steve Ebersole
*/
@Entity
@Table(name = "UC_BSC_USER")
@TypeDef( name = "HeadListType", typeClass = HeadListType.class )
public static class User {
private String userName;
private IHeadList<Email> emailAddresses = new HeadList<Email>();
private Map sessionData = new HashMap();
User() {
}
public User(String name) {
userName = name;
}
@Id
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
@OneToMany( fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true )
@CollectionType( type = "HeadListType" )
@JoinColumn( name = "userName" )
@OrderColumn( name = "displayOrder" )
public IHeadList<Email> getEmailAddresses() { //can declare a custom interface type
return emailAddresses;
}
public void setEmailAddresses(IHeadList<Email> emailAddresses) {
this.emailAddresses = emailAddresses;
}
@Transient
public Map getSessionData() {
return sessionData;
}
public void setSessionData(Map sessionData) {
this.sessionData = sessionData;
}
}
}

View File

@ -0,0 +1,30 @@
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<!--
This mapping is a basic example of how to write a UserCollectionType.
-->
<hibernate-mapping package="org.hibernate.test.collection.custom.declaredtype">
<import class="Permission"/>
<class name="org.hibernate.test.collection.custom.declaredtype.UserCollectionTypeTest$User" table="UC_BSC_USER">
<id name="userName"/>
<list name="emailAddresses" fetch="join" cascade="all, delete-orphan" collection-type="org.hibernate.test.collection.custom.declaredtype.UserCollectionTypeTest$HeadListType">
<key column="userName"/>
<list-index column="displayOrder" base="1"/>
<one-to-many class="Email"/>
</list>
</class>
<class name="Email">
<id name="id">
<generator class="native"/>
</id>
<property name="address"/>
</class>
</hibernate-mapping>

View File

@ -0,0 +1,77 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* Copyright (c) ${year}, 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.collection.custom.declaredtype;
import org.hibernate.annotations.CollectionType;
import javax.persistence.*;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
/**
* @author Gavin King
* @author Steve Ebersole
*/
@Entity
@Table(name = "UC_BSC_USER")
public class UserWithUnimplementedCollection {
private String userName;
private AtomicReference<Email> emailAddresses = new AtomicReference<Email>();
private Map sessionData = new HashMap();
UserWithUnimplementedCollection() {
}
public UserWithUnimplementedCollection(String name) {
userName = name;
}
@Id
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
@OneToMany( fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true )
@CollectionType( type = "org.hibernate.test.collection.custom.declaredtype.UserCollectionTypeTest.HeadListType" )
@JoinColumn( name = "userName" )
@OrderColumn( name = "displayOrder" )
public AtomicReference<Email> getEmailAddresses() { //can declare a custom interface type
return emailAddresses;
}
public void setEmailAddresses(AtomicReference<Email> emailAddresses) {
this.emailAddresses = emailAddresses;
}
@Transient
public Map getSessionData() {
return sessionData;
}
public void setSessionData(Map sessionData) {
this.sessionData = sessionData;
}
}

View File

@ -0,0 +1,69 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* Copyright (c) 2011, 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.collection.custom.declaredtype;
import org.hibernate.AnnotationException;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
/**
* @author Max Rydahl Andersen
* @author David Weinberg Negative test when specifying a type that can't be mapped as a collection
*/
public class UserWithUnimplementedCollectionTest extends BaseCoreFunctionalTestCase {
@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class[]{ UserWithUnimplementedCollection.class, Email.class };
}
@Override
protected String getCacheConcurrencyStrategy() {
return "nonstrict-read-write";
}
@Override
protected void buildSessionFactory() {
try {
super.buildSessionFactory();
fail( "Expected exception" );
}
catch (Exception e) {
assertTrue( e instanceof AnnotationException );
assertEquals(
"Illegal attempt to map a non collection as a @OneToMany, @ManyToMany or @CollectionOfElements: org.hibernate.test.collection.custom.declaredtype.UserWithUnimplementedCollection.emailAddresses",
e.getMessage() );
}
}
@Test
public void testSessionFactoryFailsToBeCreated() {
}
}

View File

@ -0,0 +1,350 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* Copyright (c) 2011, 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.collection.custom.declaredtype.explicitsemantics;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import java.util.Spliterator;
import java.util.Spliterators;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToMany;
import javax.persistence.OrderColumn;
import javax.persistence.Table;
import javax.persistence.Transient;
import org.hibernate.Hibernate;
import org.hibernate.HibernateException;
import org.hibernate.annotations.CollectionType;
import org.hibernate.annotations.TypeDef;
import org.hibernate.collection.internal.PersistentSet;
import org.hibernate.collection.spi.PersistentCollection;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.persister.collection.CollectionPersister;
import org.hibernate.usertype.UserCollectionType;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import org.junit.Test;
import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
/**
* @author Max Rydahl Andersen
* @author David Weinberg
*/
public class UserCollectionTypeTest extends BaseCoreFunctionalTestCase {
@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class[]{ User.class, Email.class };
}
@Override
protected String getCacheConcurrencyStrategy() {
return "nonstrict-read-write";
}
@Test
public void testBasicOperation() {
doInHibernate( this::sessionFactory, session -> {
User u = new User( "max" );
u.getEmailAddresses().add( new Email( "max@hibernate.org" ) );
u.getEmailAddresses().add( new Email( "max.andersen@jboss.com" ) );
session.persist( u );
} );
doInHibernate( this::sessionFactory, session -> {
User u2 = (User) session.createCriteria( User.class ).uniqueResult();
assertTrue( Hibernate.isInitialized( u2.getEmailAddresses() ) );
assertEquals( u2.getEmailAddresses().size(), 2 );
assertNotNull( u2.getEmailAddresses().head() );
} );
}
/**
* @author Gavin King
* @author Steve Ebersole
*/
@Entity(name = "Email")
public static class Email {
private Long id;
private String address;
Email() {
}
public Email(String address) {
this.address = address;
}
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
public Long getId() {
return id;
}
private void setId(Long id) {
this.id = id;
}
public String getAddress() {
return address;
}
public void setAddress(String type) {
this.address = type;
}
@Override
public boolean equals(Object that) {
if ( !( that instanceof Email ) )
return false;
Email p = (Email) that;
return this.address.equals( p.address );
}
@Override
public int hashCode() {
return address.hashCode();
}
}
@Entity(name = "User")
@Table(name = "UC_BSC_USER")
@TypeDef(name = "HeadSetListType", typeClass = HeadSetListType.class)
public static class User {
private String userName;
private IHeadSetList<Email> emailAddresses = new HeadSetList<Email>();
private Map sessionData = new HashMap();
User() {
}
public User(String name) {
userName = name;
}
@Id
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
@OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true)
@CollectionType(type = "HeadSetListType", semantics = Set.class)
@JoinColumn(name = "userName")
@OrderColumn(name = "displayOrder")
public IHeadSetList<Email> getEmailAddresses() { // can declare a custom interface type
return emailAddresses;
}
public void setEmailAddresses(IHeadSetList<Email> emailAddresses) {
this.emailAddresses = emailAddresses;
}
@Transient
public Map getSessionData() {
return sessionData;
}
public void setSessionData(Map sessionData) {
this.sessionData = sessionData;
}
}
public static class HeadSetListType implements UserCollectionType {
@Override
public PersistentCollection instantiate(SharedSessionContractImplementor session, CollectionPersister persister)
throws HibernateException {
return new PersistentHeadList( session );
}
@Override
public PersistentCollection wrap(SharedSessionContractImplementor session, Object collection) {
return new PersistentHeadList( session, (IHeadSetList) collection );
}
@Override
public Iterator getElementsIterator(Object collection) {
return ( (IHeadSetList) collection ).iterator();
}
@Override
public boolean contains(Object collection, Object entity) {
return ( (IHeadSetList) collection ).contains( entity );
}
@Override
public Object indexOf(Object collection, Object entity) {
int l = ( (IHeadSetList) collection ).indexOf( entity );
if ( l < 0 ) {
return null;
}
else {
return l;
}
}
@Override
public Object replaceElements(
Object original,
Object target,
CollectionPersister persister,
Object owner,
Map copyCache,
SharedSessionContractImplementor session) throws HibernateException {
IHeadSetList result = (IHeadSetList) target;
result.clear();
result.addAll( (HeadSetList) original );
return result;
}
@Override
public Object instantiate(int anticipatedSize) {
return new HeadSetList();
}
}
public interface IHeadSetList<X> extends Set<X>, List<X> {
X head();
@Override
default Spliterator<X> spliterator() {
return Spliterators.spliterator( this, Spliterator.DISTINCT );
}
}
/**
* A custom collection class that has both List and Set interfaces, but only really implements set for persistence
* (e.g. extends PersistentSet). Without setting the semantics on the CollectionType annotation, List semantics
* would be inferred, and that would not match the implemented methods in PersistentSet and would fail. HeadSetList
* is very much a toy collection type.
*
* @author David Weinberg
*/
public static class HeadSetList<X> extends ArrayList<X> implements IHeadSetList<X> {
@Override
public X head() {
return isEmpty() ? null : get( 0 );
}
}
public static class PersistentHeadList extends PersistentSet implements IHeadSetList {
public PersistentHeadList(SharedSessionContractImplementor session) {
super( session );
}
public PersistentHeadList(SharedSessionContractImplementor session, IHeadSetList list) {
super( session, list );
}
@Override
public Object head() {
return ( (IHeadSetList) set ).head();
}
@Override
public boolean addAll(int index, Collection c) {
return set.addAll( c );
}
@Override
public Object get(int index) {
Iterator iterator = iterator();
Object next = null;
for ( int i = 0; i <= index; i++ ) {
next = iterator.next();
}
return next;
}
@Override
public Object set(int index, Object element) {
remove( index );
return add( element );
}
@Override
public void add(int index, Object element) {
add( element );
}
@Override
public Object remove(int index) {
return remove( get( index ) );
}
@Override
public int indexOf(Object o) {
throw new UnsupportedOperationException( "Toy class" );
}
@Override
public int lastIndexOf(Object o) {
throw new UnsupportedOperationException( "Toy class" );
}
@Override
public ListIterator listIterator() {
throw new UnsupportedOperationException( "Toy class" );
}
@Override
public ListIterator listIterator(int index) {
throw new UnsupportedOperationException( "Toy class" );
}
@Override
public List subList(int fromIndex, int toIndex) {
throw new UnsupportedOperationException( "Toy class" );
}
}
}