1 /*
2 * Hibernate, Relational Persistence for Idiomatic Java
3 *
4 * Copyright (c) 2008, Red Hat Middleware LLC or third-party contributors as
5 * indicated by the @author tags or express copyright attribution
6 * statements applied by the authors. All third-party contributions are
7 * distributed under license by Red Hat Middleware LLC.
8 *
9 * This copyrighted material is made available to anyone wishing to use, modify,
10 * copy, or redistribute it subject to the terms and conditions of the GNU
11 * Lesser General Public License, as published by the Free Software Foundation.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
15 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
16 * for more details.
17 *
18 * You should have received a copy of the GNU Lesser General Public License
19 * along with this distribution; if not, write to:
20 * Free Software Foundation, Inc.
21 * 51 Franklin Street, Fifth Floor
22 * Boston, MA 02110-1301 USA
23 *
24 */
25 package org.hibernate.type;
26
27 import java.io.Serializable;
28 import java.sql.PreparedStatement;
29 import java.sql.ResultSet;
30 import java.sql.SQLException;
31 import java.util.ArrayList;
32 import java.util.Collection;
33 import java.util.Iterator;
34 import java.util.List;
35 import java.util.Map;
36
37 import org.dom4j.Element;
38 import org.dom4j.Node;
39
40 import org.hibernate.EntityMode;
41 import org.hibernate.Hibernate;
42 import org.hibernate.HibernateException;
43 import org.hibernate.MappingException;
44 import org.hibernate.collection.PersistentCollection;
45 import org.hibernate.engine.CollectionKey;
46 import org.hibernate.engine.EntityEntry;
47 import org.hibernate.engine.Mapping;
48 import org.hibernate.engine.PersistenceContext;
49 import org.hibernate.engine.SessionFactoryImplementor;
50 import org.hibernate.engine.SessionImplementor;
51 import org.hibernate.persister.collection.CollectionPersister;
52 import org.hibernate.persister.collection.QueryableCollection;
53 import org.hibernate.persister.entity.EntityPersister;
54 import org.hibernate.persister.entity.Joinable;
55 import org.hibernate.proxy.HibernateProxy;
56 import org.hibernate.proxy.LazyInitializer;
57 import org.hibernate.util.ArrayHelper;
58 import org.hibernate.util.MarkerObject;
59
60 /**
61 * A type that handles Hibernate <tt>PersistentCollection</tt>s (including arrays).
62 *
63 * @author Gavin King
64 */
65 public abstract class CollectionType extends AbstractType implements AssociationType {
66
67 private static final Object NOT_NULL_COLLECTION = new MarkerObject( "NOT NULL COLLECTION" );
68 public static final Object UNFETCHED_COLLECTION = new MarkerObject( "UNFETCHED COLLECTION" );
69
70 private final String role;
71 private final String foreignKeyPropertyName;
72 private final boolean isEmbeddedInXML;
73
74 public CollectionType(String role, String foreignKeyPropertyName, boolean isEmbeddedInXML) {
75 this.role = role;
76 this.foreignKeyPropertyName = foreignKeyPropertyName;
77 this.isEmbeddedInXML = isEmbeddedInXML;
78 }
79
80 public boolean isEmbeddedInXML() {
81 return isEmbeddedInXML;
82 }
83
84 public String getRole() {
85 return role;
86 }
87
88 public Object indexOf(Object collection, Object element) {
89 throw new UnsupportedOperationException( "generic collections don't have indexes" );
90 }
91
92 public boolean contains(Object collection, Object childObject, SessionImplementor session) {
93 // we do not have to worry about queued additions to uninitialized
94 // collections, since they can only occur for inverse collections!
95 Iterator elems = getElementsIterator( collection, session );
96 while ( elems.hasNext() ) {
97 Object element = elems.next();
98 // worrying about proxies is perhaps a little bit of overkill here...
99 if ( element instanceof HibernateProxy ) {
100 LazyInitializer li = ( (HibernateProxy) element ).getHibernateLazyInitializer();
101 if ( !li.isUninitialized() ) element = li.getImplementation();
102 }
103 if ( element == childObject ) return true;
104 }
105 return false;
106 }
107
108 public boolean isCollectionType() {
109 return true;
110 }
111
112 public final boolean isEqual(Object x, Object y, EntityMode entityMode) {
113 return x == y
114 || ( x instanceof PersistentCollection && ( (PersistentCollection) x ).isWrapper( y ) )
115 || ( y instanceof PersistentCollection && ( (PersistentCollection) y ).isWrapper( x ) );
116 }
117
118 public int compare(Object x, Object y, EntityMode entityMode) {
119 return 0; // collections cannot be compared
120 }
121
122 public int getHashCode(Object x, EntityMode entityMode) {
123 throw new UnsupportedOperationException( "cannot perform lookups on collections" );
124 }
125
126 /**
127 * Instantiate an uninitialized collection wrapper or holder. Callers MUST add the holder to the
128 * persistence context!
129 *
130 * @param session The session from which the request is originating.
131 * @param persister The underlying collection persister (metadata)
132 * @param key The owner key.
133 * @return The instantiated collection.
134 */
135 public abstract PersistentCollection instantiate(SessionImplementor session, CollectionPersister persister, Serializable key);
136
137 public Object nullSafeGet(ResultSet rs, String name, SessionImplementor session, Object owner) throws SQLException {
138 return nullSafeGet( rs, new String[] { name }, session, owner );
139 }
140
141 public Object nullSafeGet(ResultSet rs, String[] name, SessionImplementor session, Object owner)
142 throws HibernateException, SQLException {
143 return resolve( null, session, owner );
144 }
145
146 public final void nullSafeSet(PreparedStatement st, Object value, int index, boolean[] settable,
147 SessionImplementor session) throws HibernateException, SQLException {
148 //NOOP
149 }
150
151 public void nullSafeSet(PreparedStatement st, Object value, int index,
152 SessionImplementor session) throws HibernateException, SQLException {
153 }
154
155 public int[] sqlTypes(Mapping session) throws MappingException {
156 return ArrayHelper.EMPTY_INT_ARRAY;
157 }
158
159 public int getColumnSpan(Mapping session) throws MappingException {
160 return 0;
161 }
162
163 public String toLoggableString(Object value, SessionFactoryImplementor factory)
164 throws HibernateException {
165 if ( value == null ) {
166 return "null";
167 }
168 else if ( !Hibernate.isInitialized( value ) ) {
169 return "<uninitialized>";
170 }
171 else {
172 return renderLoggableString( value, factory );
173 }
174 }
175
176 protected String renderLoggableString(Object value, SessionFactoryImplementor factory)
177 throws HibernateException {
178 if ( Element.class.isInstance( value ) ) {
179 // for DOM4J "collections" only
180 // TODO: it would be better if this was done at the higher level by Printer
181 return ( ( Element ) value ).asXML();
182 }
183 else {
184 List list = new ArrayList();
185 Type elemType = getElementType( factory );
186 Iterator iter = getElementsIterator( value );
187 while ( iter.hasNext() ) {
188 list.add( elemType.toLoggableString( iter.next(), factory ) );
189 }
190 return list.toString();
191 }
192 }
193
194 public Object deepCopy(Object value, EntityMode entityMode, SessionFactoryImplementor factory)
195 throws HibernateException {
196 return value;
197 }
198
199 public String getName() {
200 return getReturnedClass().getName() + '(' + getRole() + ')';
201 }
202
203 /**
204 * Get an iterator over the element set of the collection, which may not yet be wrapped
205 *
206 * @param collection The collection to be iterated
207 * @param session The session from which the request is originating.
208 * @return The iterator.
209 */
210 public Iterator getElementsIterator(Object collection, SessionImplementor session) {
211 if ( session.getEntityMode()==EntityMode.DOM4J ) {
212 final SessionFactoryImplementor factory = session.getFactory();
213 final CollectionPersister persister = factory.getCollectionPersister( getRole() );
214 final Type elementType = persister.getElementType();
215
216 List elements = ( (Element) collection ).elements( persister.getElementNodeName() );
217 ArrayList results = new ArrayList();
218 for ( int i=0; i<elements.size(); i++ ) {
219 Element value = (Element) elements.get(i);
220 results.add( elementType.fromXMLNode( value, factory ) );
221 }
222 return results.iterator();
223 }
224 else {
225 return getElementsIterator(collection);
226 }
227 }
228
229 /**
230 * Get an iterator over the element set of the collection in POJO mode
231 *
232 * @param collection The collection to be iterated
233 * @return The iterator.
234 */
235 protected Iterator getElementsIterator(Object collection) {
236 return ( (Collection) collection ).iterator();
237 }
238
239 public boolean isMutable() {
240 return false;
241 }
242
243 public Serializable disassemble(Object value, SessionImplementor session, Object owner)
244 throws HibernateException {
245 //remember the uk value
246
247 //This solution would allow us to eliminate the owner arg to disassemble(), but
248 //what if the collection was null, and then later had elements added? seems unsafe
249 //session.getPersistenceContext().getCollectionEntry( (PersistentCollection) value ).getKey();
250
251 final Serializable key = getKeyOfOwner(owner, session);
252 if (key==null) {
253 return null;
254 }
255 else {
256 return getPersister(session)
257 .getKeyType()
258 .disassemble( key, session, owner );
259 }
260 }
261
262 public Object assemble(Serializable cached, SessionImplementor session, Object owner)
263 throws HibernateException {
264 //we must use the "remembered" uk value, since it is
265 //not available from the EntityEntry during assembly
266 if (cached==null) {
267 return null;
268 }
269 else {
270 final Serializable key = (Serializable) getPersister(session)
271 .getKeyType()
272 .assemble( cached, session, owner);
273 return resolveKey( key, session, owner );
274 }
275 }
276
277 /**
278 * Is the owning entity versioned?
279 *
280 * @param session The session from which the request is originating.
281 * @return True if the collection owner is versioned; false otherwise.
282 * @throws org.hibernate.MappingException Indicates our persister could not be located.
283 */
284 private boolean isOwnerVersioned(SessionImplementor session) throws MappingException {
285 return getPersister( session ).getOwnerEntityPersister().isVersioned();
286 }
287
288 /**
289 * Get our underlying collection persister (using the session to access the
290 * factory).
291 *
292 * @param session The session from which the request is originating.
293 * @return The underlying collection persister
294 */
295 private CollectionPersister getPersister(SessionImplementor session) {
296 return session.getFactory().getCollectionPersister( role );
297 }
298
299 public boolean isDirty(Object old, Object current, SessionImplementor session)
300 throws HibernateException {
301
302 // collections don't dirty an unversioned parent entity
303
304 // TODO: I don't really like this implementation; it would be better if
305 // this was handled by searchForDirtyCollections()
306 return isOwnerVersioned( session ) && super.isDirty( old, current, session );
307 // return false;
308
309 }
310
311 public boolean isDirty(Object old, Object current, boolean[] checkable, SessionImplementor session)
312 throws HibernateException {
313 return isDirty(old, current, session);
314 }
315
316 /**
317 * Wrap the naked collection instance in a wrapper, or instantiate a
318 * holder. Callers <b>MUST</b> add the holder to the persistence context!
319 *
320 * @param session The session from which the request is originating.
321 * @param collection The bare collection to be wrapped.
322 * @return The wrapped collection.
323 */
324 public abstract PersistentCollection wrap(SessionImplementor session, Object collection);
325
326 /**
327 * Note: return true because this type is castable to <tt>AssociationType</tt>. Not because
328 * all collections are associations.
329 */
330 public boolean isAssociationType() {
331 return true;
332 }
333
334 public ForeignKeyDirection getForeignKeyDirection() {
335 return ForeignKeyDirection.FOREIGN_KEY_TO_PARENT;
336 }
337
338 /**
339 * Get the key value from the owning entity instance, usually the identifier, but might be some
340 * other unique key, in the case of property-ref
341 *
342 * @param owner The collection owner
343 * @param session The session from which the request is originating.
344 * @return The collection owner's key
345 */
346 public Serializable getKeyOfOwner(Object owner, SessionImplementor session) {
347
348 EntityEntry entityEntry = session.getPersistenceContext().getEntry( owner );
349 if ( entityEntry == null ) return null; // This just handles a particular case of component
350 // projection, perhaps get rid of it and throw an exception
351
352 if ( foreignKeyPropertyName == null ) {
353 return entityEntry.getId();
354 }
355 else {
356 // TODO: at the point where we are resolving collection references, we don't
357 // know if the uk value has been resolved (depends if it was earlier or
358 // later in the mapping document) - now, we could try and use e.getStatus()
359 // to decide to semiResolve(), trouble is that initializeEntity() reuses
360 // the same array for resolved and hydrated values
361 Object id;
362 if ( entityEntry.getLoadedState() != null ) {
363 id = entityEntry.getLoadedValue( foreignKeyPropertyName );
364 }
365 else {
366 id = entityEntry.getPersister().getPropertyValue( owner, foreignKeyPropertyName, session.getEntityMode() );
367 }
368
369 // NOTE VERY HACKISH WORKAROUND!!
370 // TODO: Fix this so it will work for non-POJO entity mode
371 Type keyType = getPersister( session ).getKeyType();
372 if ( !keyType.getReturnedClass().isInstance( id ) ) {
373 id = (Serializable) keyType.semiResolve(
374 entityEntry.getLoadedValue( foreignKeyPropertyName ),
375 session,
376 owner
377 );
378 }
379
380 return (Serializable) id;
381 }
382 }
383
384 /**
385 * Get the id value from the owning entity key, usually the same as the key, but might be some
386 * other property, in the case of property-ref
387 *
388 * @param key The collection owner key
389 * @param session The session from which the request is originating.
390 * @return The collection owner's id, if it can be obtained from the key;
391 * otherwise, null is returned
392 */
393 public Serializable getIdOfOwnerOrNull(Serializable key, SessionImplementor session) {
394 Serializable ownerId = null;
395 if ( foreignKeyPropertyName == null ) {
396 ownerId = key;
397 }
398 else {
399 Type keyType = getPersister( session ).getKeyType();
400 EntityPersister ownerPersister = getPersister( session ).getOwnerEntityPersister();
401 // TODO: Fix this so it will work for non-POJO entity mode
402 Class ownerMappedClass = ownerPersister.getMappedClass( session.getEntityMode() );
403 if ( ownerMappedClass.isAssignableFrom( keyType.getReturnedClass() ) &&
404 keyType.getReturnedClass().isInstance( key ) ) {
405 // the key is the owning entity itself, so get the ID from the key
406 ownerId = ownerPersister.getIdentifier( key, session.getEntityMode() );
407 }
408 else {
409 // TODO: check if key contains the owner ID
410 }
411 }
412 return ownerId;
413 }
414
415 public Object hydrate(ResultSet rs, String[] name, SessionImplementor session, Object owner) {
416 // can't just return null here, since that would
417 // cause an owning component to become null
418 return NOT_NULL_COLLECTION;
419 }
420
421 public Object resolve(Object value, SessionImplementor session, Object owner)
422 throws HibernateException {
423
424 return resolveKey( getKeyOfOwner( owner, session ), session, owner );
425 }
426
427 private Object resolveKey(Serializable key, SessionImplementor session, Object owner) {
428 // if (key==null) throw new AssertionFailure("owner identifier unknown when re-assembling
429 // collection reference");
430 return key == null ? null : // TODO: can this case really occur??
431 getCollection( key, session, owner );
432 }
433
434 public Object semiResolve(Object value, SessionImplementor session, Object owner)
435 throws HibernateException {
436 throw new UnsupportedOperationException(
437 "collection mappings may not form part of a property-ref" );
438 }
439
440 public boolean isArrayType() {
441 return false;
442 }
443
444 public boolean useLHSPrimaryKey() {
445 return foreignKeyPropertyName == null;
446 }
447
448 public String getRHSUniqueKeyPropertyName() {
449 return null;
450 }
451
452 public Joinable getAssociatedJoinable(SessionFactoryImplementor factory)
453 throws MappingException {
454 return (Joinable) factory.getCollectionPersister( role );
455 }
456
457 public boolean isModified(Object old, Object current, boolean[] checkable, SessionImplementor session) throws HibernateException {
458 return false;
459 }
460
461 public String getAssociatedEntityName(SessionFactoryImplementor factory)
462 throws MappingException {
463 try {
464
465 QueryableCollection collectionPersister = (QueryableCollection) factory
466 .getCollectionPersister( role );
467
468 if ( !collectionPersister.getElementType().isEntityType() ) {
469 throw new MappingException(
470 "collection was not an association: " +
471 collectionPersister.getRole()
472 );
473 }
474
475 return collectionPersister.getElementPersister().getEntityName();
476
477 }
478 catch (ClassCastException cce) {
479 throw new MappingException( "collection role is not queryable " + role );
480 }
481 }
482
483 /**
484 * Replace the elements of a collection with the elements of another collection.
485 *
486 * @param original The 'source' of the replacement elements (where we copy from)
487 * @param target The target of the replacement elements (where we copy to)
488 * @param owner The owner of the collection being merged
489 * @param copyCache The map of elements already replaced.
490 * @param session The session from which the merge event originated.
491 * @return The merged collection.
492 */
493 public Object replaceElements(
494 Object original,
495 Object target,
496 Object owner,
497 Map copyCache,
498 SessionImplementor session) {
499 // TODO: does not work for EntityMode.DOM4J yet!
500 java.util.Collection result = ( java.util.Collection ) target;
501 result.clear();
502
503 // copy elements into newly empty target collection
504 Type elemType = getElementType( session.getFactory() );
505 Iterator iter = ( (java.util.Collection) original ).iterator();
506 while ( iter.hasNext() ) {
507 result.add( elemType.replace( iter.next(), null, session, owner, copyCache ) );
508 }
509
510 // if the original is a PersistentCollection, and that original
511 // was not flagged as dirty, then reset the target's dirty flag
512 // here after the copy operation.
513 // </p>
514 // One thing to be careful of here is a "bare" original collection
515 // in which case we should never ever ever reset the dirty flag
516 // on the target because we simply do not know...
517 if ( original instanceof PersistentCollection ) {
518 if ( result instanceof PersistentCollection ) {
519 if ( ! ( ( PersistentCollection ) original ).isDirty() ) {
520 ( ( PersistentCollection ) result ).clearDirty();
521 }
522 }
523 }
524
525 return result;
526 }
527
528 /**
529 * Instantiate a new "underlying" collection exhibiting the same capacity
530 * charactersitcs and the passed "original".
531 *
532 * @param original The original collection.
533 * @return The newly instantiated collection.
534 */
535 protected Object instantiateResult(Object original) {
536 // by default just use an unanticipated capacity since we don't
537 // know how to extract the capacity to use from original here...
538 return instantiate( -1 );
539 }
540
541 /**
542 * Instantiate an empty instance of the "underlying" collection (not a wrapper),
543 * but with the given anticipated size (i.e. accounting for initial capacity
544 * and perhaps load factor).
545 *
546 * @param anticipatedSize The anticipated size of the instaniated collection
547 * after we are done populating it.
548 * @return A newly instantiated collection to be wrapped.
549 */
550 public abstract Object instantiate(int anticipatedSize);
551
552 /**
553 * {@inheritDoc}
554 */
555 public Object replace(
556 final Object original,
557 final Object target,
558 final SessionImplementor session,
559 final Object owner,
560 final Map copyCache) throws HibernateException {
561 if ( original == null ) {
562 return null;
563 }
564 if ( !Hibernate.isInitialized( original ) ) {
565 return target;
566 }
567
568 // for a null target, or a target which is the same as the original, we
569 // need to put the merged elements in a new collection
570 Object result = target == null || target == original ? instantiateResult( original ) : target;
571
572 //for arrays, replaceElements() may return a different reference, since
573 //the array length might not match
574 result = replaceElements( original, result, owner, copyCache, session );
575
576 if ( original == target ) {
577 // get the elements back into the target making sure to handle dirty flag
578 boolean wasClean = PersistentCollection.class.isInstance( target ) && !( ( PersistentCollection ) target ).isDirty();
579 //TODO: this is a little inefficient, don't need to do a whole
580 // deep replaceElements() call
581 replaceElements( result, target, owner, copyCache, session );
582 if ( wasClean ) {
583 ( ( PersistentCollection ) target ).clearDirty();
584 }
585 result = target;
586 }
587
588 return result;
589 }
590
591 /**
592 * Get the Hibernate type of the collection elements
593 *
594 * @param factory The session factory.
595 * @return The type of the collection elements
596 * @throws MappingException Indicates the underlying persister could not be located.
597 */
598 public final Type getElementType(SessionFactoryImplementor factory) throws MappingException {
599 return factory.getCollectionPersister( getRole() ).getElementType();
600 }
601
602 public String toString() {
603 return getClass().getName() + '(' + getRole() + ')';
604 }
605
606 public String getOnCondition(String alias, SessionFactoryImplementor factory, Map enabledFilters)
607 throws MappingException {
608 return getAssociatedJoinable( factory ).filterFragment( alias, enabledFilters );
609 }
610
611 /**
612 * instantiate a collection wrapper (called when loading an object)
613 *
614 * @param key The collection owner key
615 * @param session The session from which the request is originating.
616 * @param owner The collection owner
617 * @return The collection
618 */
619 public Object getCollection(Serializable key, SessionImplementor session, Object owner) {
620
621 CollectionPersister persister = getPersister( session );
622 final PersistenceContext persistenceContext = session.getPersistenceContext();
623 final EntityMode entityMode = session.getEntityMode();
624
625 if (entityMode==EntityMode.DOM4J && !isEmbeddedInXML) {
626 return UNFETCHED_COLLECTION;
627 }
628
629 // check if collection is currently being loaded
630 PersistentCollection collection = persistenceContext.getLoadContexts().locateLoadingCollection( persister, key );
631
632 if ( collection == null ) {
633
634 // check if it is already completely loaded, but unowned
635 collection = persistenceContext.useUnownedCollection( new CollectionKey(persister, key, entityMode) );
636
637 if ( collection == null ) {
638 // create a new collection wrapper, to be initialized later
639 collection = instantiate( session, persister, key );
640 collection.setOwner(owner);
641
642 persistenceContext.addUninitializedCollection( persister, collection, key );
643
644 // some collections are not lazy:
645 if ( initializeImmediately( entityMode ) ) {
646 session.initializeCollection( collection, false );
647 }
648 else if ( !persister.isLazy() ) {
649 persistenceContext.addNonLazyCollection( collection );
650 }
651
652 if ( hasHolder( entityMode ) ) {
653 session.getPersistenceContext().addCollectionHolder( collection );
654 }
655
656 }
657
658 }
659
660 collection.setOwner(owner);
661
662 return collection.getValue();
663 }
664
665 public boolean hasHolder(EntityMode entityMode) {
666 return entityMode == EntityMode.DOM4J;
667 }
668
669 protected boolean initializeImmediately(EntityMode entityMode) {
670 return entityMode == EntityMode.DOM4J;
671 }
672
673 public String getLHSPropertyName() {
674 return foreignKeyPropertyName;
675 }
676
677 public boolean isXMLElement() {
678 return true;
679 }
680
681 public Object fromXMLNode(Node xml, Mapping factory) throws HibernateException {
682 return xml;
683 }
684
685 public void setToXMLNode(Node node, Object value, SessionFactoryImplementor factory)
686 throws HibernateException {
687 if ( !isEmbeddedInXML ) {
688 node.detach();
689 }
690 else {
691 replaceNode( node, (Element) value );
692 }
693 }
694
695 /**
696 * We always need to dirty check the collection because we sometimes
697 * need to incremement version number of owner and also because of
698 * how assemble/disassemble is implemented for uks
699 */
700 public boolean isAlwaysDirtyChecked() {
701 return true;
702 }
703
704 public boolean[] toColumnNullness(Object value, Mapping mapping) {
705 return ArrayHelper.EMPTY_BOOLEAN_ARRAY;
706 }
707 }