001: //$Id: AbstractPersistentCollection.java 11301 2007-03-19 20:43:46Z steve.ebersole@jboss.com $
002: package org.hibernate.collection;
003:
004: import java.io.Serializable;
005: import java.util.ArrayList;
006: import java.util.Collection;
007: import java.util.HashSet;
008: import java.util.Iterator;
009: import java.util.List;
010: import java.util.ListIterator;
011:
012: import org.hibernate.AssertionFailure;
013: import org.hibernate.HibernateException;
014: import org.hibernate.LazyInitializationException;
015: import org.hibernate.engine.CollectionEntry;
016: import org.hibernate.engine.ForeignKeys;
017: import org.hibernate.engine.SessionImplementor;
018: import org.hibernate.engine.TypedValue;
019: import org.hibernate.persister.collection.CollectionPersister;
020: import org.hibernate.pretty.MessageHelper;
021: import org.hibernate.type.Type;
022: import org.hibernate.util.CollectionHelper;
023: import org.hibernate.util.EmptyIterator;
024: import org.hibernate.util.MarkerObject;
025:
026: /**
027: * Base class implementing <tt>PersistentCollection</tt>
028: * @see PersistentCollection
029: * @author Gavin King
030: */
031: public abstract class AbstractPersistentCollection implements
032: Serializable, PersistentCollection {
033:
034: private transient SessionImplementor session;
035: private boolean initialized;
036: private transient List operationQueue;
037: private transient boolean directlyAccessible;
038: private transient boolean initializing;
039: private Object owner;
040: private int cachedSize = -1;
041:
042: private String role;
043: private Serializable key;
044: // collections detect changes made via their public interface and mark
045: // themselves as dirty as a performance optimization
046: private boolean dirty;
047: private Serializable storedSnapshot;
048:
049: public final String getRole() {
050: return role;
051: }
052:
053: public final Serializable getKey() {
054: return key;
055: }
056:
057: public final boolean isUnreferenced() {
058: return role == null;
059: }
060:
061: public final boolean isDirty() {
062: return dirty;
063: }
064:
065: public final void clearDirty() {
066: dirty = false;
067: }
068:
069: public final void dirty() {
070: dirty = true;
071: }
072:
073: public final Serializable getStoredSnapshot() {
074: return storedSnapshot;
075: }
076:
077: //Careful: these methods do not initialize the collection.
078: /**
079: * Is the initialized collection empty?
080: */
081: public abstract boolean empty();
082:
083: /**
084: * Called by any read-only method of the collection interface
085: */
086: protected final void read() {
087: initialize(false);
088: }
089:
090: /**
091: * Called by the <tt>size()</tt> method
092: */
093: protected boolean readSize() {
094: if (!initialized) {
095: if (cachedSize != -1 && !hasQueuedOperations()) {
096: return true;
097: } else {
098: throwLazyInitializationExceptionIfNotConnected();
099: CollectionEntry entry = session.getPersistenceContext()
100: .getCollectionEntry(this );
101: CollectionPersister persister = entry
102: .getLoadedPersister();
103: if (persister.isExtraLazy()) {
104: if (hasQueuedOperations()) {
105: session.flush();
106: }
107: cachedSize = persister.getSize(
108: entry.getLoadedKey(), session);
109: return true;
110: }
111: }
112: }
113: read();
114: return false;
115: }
116:
117: protected Boolean readIndexExistence(Object index) {
118: if (!initialized) {
119: throwLazyInitializationExceptionIfNotConnected();
120: CollectionEntry entry = session.getPersistenceContext()
121: .getCollectionEntry(this );
122: CollectionPersister persister = entry.getLoadedPersister();
123: if (persister.isExtraLazy()) {
124: if (hasQueuedOperations()) {
125: session.flush();
126: }
127: return new Boolean(persister.indexExists(entry
128: .getLoadedKey(), index, session));
129: }
130: }
131: read();
132: return null;
133:
134: }
135:
136: protected Boolean readElementExistence(Object element) {
137: if (!initialized) {
138: throwLazyInitializationExceptionIfNotConnected();
139: CollectionEntry entry = session.getPersistenceContext()
140: .getCollectionEntry(this );
141: CollectionPersister persister = entry.getLoadedPersister();
142: if (persister.isExtraLazy()) {
143: if (hasQueuedOperations()) {
144: session.flush();
145: }
146: return new Boolean(persister.elementExists(entry
147: .getLoadedKey(), element, session));
148: }
149: }
150: read();
151: return null;
152:
153: }
154:
155: protected static final Object UNKNOWN = new MarkerObject("UNKNOWN");
156:
157: protected Object readElementByIndex(Object index) {
158: if (!initialized) {
159: throwLazyInitializationExceptionIfNotConnected();
160: CollectionEntry entry = session.getPersistenceContext()
161: .getCollectionEntry(this );
162: CollectionPersister persister = entry.getLoadedPersister();
163: if (persister.isExtraLazy()) {
164: if (hasQueuedOperations()) {
165: session.flush();
166: }
167: return persister.getElementByIndex(
168: entry.getLoadedKey(), index, session, owner);
169: }
170: }
171: read();
172: return UNKNOWN;
173:
174: }
175:
176: protected int getCachedSize() {
177: return cachedSize;
178: }
179:
180: /**
181: * Is the collection currently connected to an open session?
182: */
183: private final boolean isConnectedToSession() {
184: return session != null
185: && session.isOpen()
186: && session.getPersistenceContext().containsCollection(
187: this );
188: }
189:
190: /**
191: * Called by any writer method of the collection interface
192: */
193: protected final void write() {
194: initialize(true);
195: dirty();
196: }
197:
198: /**
199: * Is this collection in a state that would allow us to
200: * "queue" operations?
201: */
202: protected boolean isOperationQueueEnabled() {
203: return !initialized && isConnectedToSession()
204: && isInverseCollection();
205: }
206:
207: /**
208: * Is this collection in a state that would allow us to
209: * "queue" puts? This is a special case, because of orphan
210: * delete.
211: */
212: protected boolean isPutQueueEnabled() {
213: return !initialized && isConnectedToSession()
214: && isInverseOneToManyOrNoOrphanDelete();
215: }
216:
217: /**
218: * Is this collection in a state that would allow us to
219: * "queue" clear? This is a special case, because of orphan
220: * delete.
221: */
222: protected boolean isClearQueueEnabled() {
223: return !initialized && isConnectedToSession()
224: && isInverseCollectionNoOrphanDelete();
225: }
226:
227: /**
228: * Is this the "inverse" end of a bidirectional association?
229: */
230: private boolean isInverseCollection() {
231: CollectionEntry ce = session.getPersistenceContext()
232: .getCollectionEntry(this );
233: return ce != null && ce.getLoadedPersister().isInverse();
234: }
235:
236: /**
237: * Is this the "inverse" end of a bidirectional association with
238: * no orphan delete enabled?
239: */
240: private boolean isInverseCollectionNoOrphanDelete() {
241: CollectionEntry ce = session.getPersistenceContext()
242: .getCollectionEntry(this );
243: return ce != null && ce.getLoadedPersister().isInverse()
244: && !ce.getLoadedPersister().hasOrphanDelete();
245: }
246:
247: /**
248: * Is this the "inverse" end of a bidirectional one-to-many, or
249: * of a collection with no orphan delete?
250: */
251: private boolean isInverseOneToManyOrNoOrphanDelete() {
252: CollectionEntry ce = session.getPersistenceContext()
253: .getCollectionEntry(this );
254: return ce != null
255: && ce.getLoadedPersister().isInverse()
256: && (ce.getLoadedPersister().isOneToMany() || !ce
257: .getLoadedPersister().hasOrphanDelete());
258: }
259:
260: /**
261: * Queue an addition
262: */
263: protected final void queueOperation(Object element) {
264: if (operationQueue == null)
265: operationQueue = new ArrayList(10);
266: operationQueue.add(element);
267: dirty = true; //needed so that we remove this collection from the second-level cache
268: }
269:
270: /**
271: * After reading all existing elements from the database,
272: * add the queued elements to the underlying collection.
273: */
274: protected final void performQueuedOperations() {
275: for (int i = 0; i < operationQueue.size(); i++) {
276: ((DelayedOperation) operationQueue.get(i)).operate();
277: }
278: }
279:
280: /**
281: * After flushing, re-init snapshot state.
282: */
283: public void setSnapshot(Serializable key, String role,
284: Serializable snapshot) {
285: this .key = key;
286: this .role = role;
287: this .storedSnapshot = snapshot;
288: }
289:
290: /**
291: * After flushing, clear any "queued" additions, since the
292: * database state is now synchronized with the memory state.
293: */
294: public void postAction() {
295: operationQueue = null;
296: cachedSize = -1;
297: clearDirty();
298: }
299:
300: /**
301: * Not called by Hibernate, but used by non-JDK serialization,
302: * eg. SOAP libraries.
303: */
304: public AbstractPersistentCollection() {
305: }
306:
307: protected AbstractPersistentCollection(SessionImplementor session) {
308: this .session = session;
309: }
310:
311: /**
312: * return the user-visible collection (or array) instance
313: */
314: public Object getValue() {
315: return this ;
316: }
317:
318: /**
319: * Called just before reading any rows from the JDBC result set
320: */
321: public void beginRead() {
322: // override on some subclasses
323: initializing = true;
324: }
325:
326: /**
327: * Called after reading all rows from the JDBC result set
328: */
329: public boolean endRead() {
330: //override on some subclasses
331: return afterInitialize();
332: }
333:
334: public boolean afterInitialize() {
335: setInitialized();
336: //do this bit after setting initialized to true or it will recurse
337: if (operationQueue != null) {
338: performQueuedOperations();
339: operationQueue = null;
340: cachedSize = -1;
341: return false;
342: } else {
343: return true;
344: }
345: }
346:
347: /**
348: * Initialize the collection, if possible, wrapping any exceptions
349: * in a runtime exception
350: * @param writing currently obsolete
351: * @throws LazyInitializationException if we cannot initialize
352: */
353: protected final void initialize(boolean writing) {
354: if (!initialized) {
355: if (initializing) {
356: throw new LazyInitializationException(
357: "illegal access to loading collection");
358: }
359: throwLazyInitializationExceptionIfNotConnected();
360: session.initializeCollection(this , writing);
361: }
362: }
363:
364: private void throwLazyInitializationExceptionIfNotConnected() {
365: if (!isConnectedToSession()) {
366: throwLazyInitializationException("no session or session was closed");
367: }
368: if (!session.isConnected()) {
369: throwLazyInitializationException("session is disconnected");
370: }
371: }
372:
373: private void throwLazyInitializationException(String message) {
374: throw new LazyInitializationException(
375: "failed to lazily initialize a collection"
376: + (role == null ? "" : " of role: " + role)
377: + ", " + message);
378: }
379:
380: protected final void setInitialized() {
381: this .initializing = false;
382: this .initialized = true;
383: }
384:
385: protected final void setDirectlyAccessible(
386: boolean directlyAccessible) {
387: this .directlyAccessible = directlyAccessible;
388: }
389:
390: /**
391: * Could the application possibly have a direct reference to
392: * the underlying collection implementation?
393: */
394: public boolean isDirectlyAccessible() {
395: return directlyAccessible;
396: }
397:
398: /**
399: * Disassociate this collection from the given session.
400: * @return true if this was currently associated with the given session
401: */
402: public final boolean unsetSession(SessionImplementor currentSession) {
403: if (currentSession == this .session) {
404: this .session = null;
405: return true;
406: } else {
407: return false;
408: }
409: }
410:
411: /**
412: * Associate the collection with the given session.
413: * @return false if the collection was already associated with the session
414: * @throws HibernateException if the collection was already associated
415: * with another open session
416: */
417: public final boolean setCurrentSession(SessionImplementor session)
418: throws HibernateException {
419: if (session == this .session) {
420: return false;
421: } else {
422: if (isConnectedToSession()) {
423: CollectionEntry ce = session.getPersistenceContext()
424: .getCollectionEntry(this );
425: if (ce == null) {
426: throw new HibernateException(
427: "Illegal attempt to associate a collection with two open sessions");
428: } else {
429: throw new HibernateException(
430: "Illegal attempt to associate a collection with two open sessions: "
431: + MessageHelper
432: .collectionInfoString(
433: ce
434: .getLoadedPersister(),
435: ce.getLoadedKey(),
436: session
437: .getFactory()));
438: }
439: } else {
440: this .session = session;
441: return true;
442: }
443: }
444: }
445:
446: /**
447: * Do we need to completely recreate this collection when it changes?
448: */
449: public boolean needsRecreate(CollectionPersister persister) {
450: return false;
451: }
452:
453: /**
454: * To be called internally by the session, forcing
455: * immediate initialization.
456: */
457: public final void forceInitialization() throws HibernateException {
458: if (!initialized) {
459: if (initializing) {
460: throw new AssertionFailure(
461: "force initialize loading collection");
462: }
463: if (session == null) {
464: throw new HibernateException(
465: "collection is not associated with any session");
466: }
467: if (!session.isConnected()) {
468: throw new HibernateException("disconnected session");
469: }
470: session.initializeCollection(this , false);
471: }
472: }
473:
474: /**
475: * Get the current snapshot from the session
476: */
477: protected final Serializable getSnapshot() {
478: return session.getPersistenceContext().getSnapshot(this );
479: }
480:
481: /**
482: * Is this instance initialized?
483: */
484: public final boolean wasInitialized() {
485: return initialized;
486: }
487:
488: public boolean isRowUpdatePossible() {
489: return true;
490: }
491:
492: /**
493: * Does this instance have any "queued" additions?
494: */
495: public final boolean hasQueuedOperations() {
496: return operationQueue != null;
497: }
498:
499: /**
500: * Iterate the "queued" additions
501: */
502: public final Iterator queuedAdditionIterator() {
503: if (hasQueuedOperations()) {
504: return new Iterator() {
505: int i = 0;
506:
507: public Object next() {
508: return ((DelayedOperation) operationQueue.get(i++))
509: .getAddedInstance();
510: }
511:
512: public boolean hasNext() {
513: return i < operationQueue.size();
514: }
515:
516: public void remove() {
517: throw new UnsupportedOperationException();
518: }
519: };
520: } else {
521: return EmptyIterator.INSTANCE;
522: }
523: }
524:
525: /**
526: * Iterate the "queued" additions
527: */
528: public final Collection getQueuedOrphans(String entityName) {
529: if (hasQueuedOperations()) {
530: Collection additions = new ArrayList(operationQueue.size());
531: Collection removals = new ArrayList(operationQueue.size());
532: for (int i = 0; i < operationQueue.size(); i++) {
533: DelayedOperation op = (DelayedOperation) operationQueue
534: .get(i);
535: additions.add(op.getAddedInstance());
536: removals.add(op.getOrphan());
537: }
538: return getOrphans(removals, additions, entityName, session);
539: } else {
540: return CollectionHelper.EMPTY_COLLECTION;
541: }
542: }
543:
544: /**
545: * Called before inserting rows, to ensure that any surrogate keys
546: * are fully generated
547: */
548: public void preInsert(CollectionPersister persister)
549: throws HibernateException {
550: }
551:
552: /**
553: * Called after inserting a row, to fetch the natively generated id
554: */
555: public void afterRowInsert(CollectionPersister persister,
556: Object entry, int i) throws HibernateException {
557: }
558:
559: /**
560: * get all "orphaned" elements
561: */
562: public abstract Collection getOrphans(Serializable snapshot,
563: String entityName) throws HibernateException;
564:
565: /**
566: * Get the current session
567: */
568: public final SessionImplementor getSession() {
569: return session;
570: }
571:
572: final class IteratorProxy implements Iterator {
573: private final Iterator iter;
574:
575: IteratorProxy(Iterator iter) {
576: this .iter = iter;
577: }
578:
579: public boolean hasNext() {
580: return iter.hasNext();
581: }
582:
583: public Object next() {
584: return iter.next();
585: }
586:
587: public void remove() {
588: write();
589: iter.remove();
590: }
591:
592: }
593:
594: final class ListIteratorProxy implements ListIterator {
595: private final ListIterator iter;
596:
597: ListIteratorProxy(ListIterator iter) {
598: this .iter = iter;
599: }
600:
601: public void add(Object o) {
602: write();
603: iter.add(o);
604: }
605:
606: public boolean hasNext() {
607: return iter.hasNext();
608: }
609:
610: public boolean hasPrevious() {
611: return iter.hasPrevious();
612: }
613:
614: public Object next() {
615: return iter.next();
616: }
617:
618: public int nextIndex() {
619: return iter.nextIndex();
620: }
621:
622: public Object previous() {
623: return iter.previous();
624: }
625:
626: public int previousIndex() {
627: return iter.previousIndex();
628: }
629:
630: public void remove() {
631: write();
632: iter.remove();
633: }
634:
635: public void set(Object o) {
636: write();
637: iter.set(o);
638: }
639:
640: }
641:
642: class SetProxy implements java.util.Set {
643:
644: final Collection set;
645:
646: SetProxy(Collection set) {
647: this .set = set;
648: }
649:
650: public boolean add(Object o) {
651: write();
652: return set.add(o);
653: }
654:
655: public boolean addAll(Collection c) {
656: write();
657: return set.addAll(c);
658: }
659:
660: public void clear() {
661: write();
662: set.clear();
663: }
664:
665: public boolean contains(Object o) {
666: return set.contains(o);
667: }
668:
669: public boolean containsAll(Collection c) {
670: return set.containsAll(c);
671: }
672:
673: public boolean isEmpty() {
674: return set.isEmpty();
675: }
676:
677: public Iterator iterator() {
678: return new IteratorProxy(set.iterator());
679: }
680:
681: public boolean remove(Object o) {
682: write();
683: return set.remove(o);
684: }
685:
686: public boolean removeAll(Collection c) {
687: write();
688: return set.removeAll(c);
689: }
690:
691: public boolean retainAll(Collection c) {
692: write();
693: return set.retainAll(c);
694: }
695:
696: public int size() {
697: return set.size();
698: }
699:
700: public Object[] toArray() {
701: return set.toArray();
702: }
703:
704: public Object[] toArray(Object[] array) {
705: return set.toArray(array);
706: }
707:
708: }
709:
710: final class ListProxy implements java.util.List {
711:
712: private final java.util.List list;
713:
714: ListProxy(java.util.List list) {
715: this .list = list;
716: }
717:
718: public void add(int index, Object value) {
719: write();
720: list.add(index, value);
721: }
722:
723: /**
724: * @see java.util.Collection#add(Object)
725: */
726: public boolean add(Object o) {
727: write();
728: return list.add(o);
729: }
730:
731: /**
732: * @see java.util.Collection#addAll(Collection)
733: */
734: public boolean addAll(Collection c) {
735: write();
736: return list.addAll(c);
737: }
738:
739: /**
740: * @see java.util.List#addAll(int, Collection)
741: */
742: public boolean addAll(int i, Collection c) {
743: write();
744: return list.addAll(i, c);
745: }
746:
747: /**
748: * @see java.util.Collection#clear()
749: */
750: public void clear() {
751: write();
752: list.clear();
753: }
754:
755: /**
756: * @see java.util.Collection#contains(Object)
757: */
758: public boolean contains(Object o) {
759: return list.contains(o);
760: }
761:
762: /**
763: * @see java.util.Collection#containsAll(Collection)
764: */
765: public boolean containsAll(Collection c) {
766: return list.containsAll(c);
767: }
768:
769: /**
770: * @see java.util.List#get(int)
771: */
772: public Object get(int i) {
773: return list.get(i);
774: }
775:
776: /**
777: * @see java.util.List#indexOf(Object)
778: */
779: public int indexOf(Object o) {
780: return list.indexOf(o);
781: }
782:
783: /**
784: * @see java.util.Collection#isEmpty()
785: */
786: public boolean isEmpty() {
787: return list.isEmpty();
788: }
789:
790: /**
791: * @see java.util.Collection#iterator()
792: */
793: public Iterator iterator() {
794: return new IteratorProxy(list.iterator());
795: }
796:
797: /**
798: * @see java.util.List#lastIndexOf(Object)
799: */
800: public int lastIndexOf(Object o) {
801: return list.lastIndexOf(o);
802: }
803:
804: /**
805: * @see java.util.List#listIterator()
806: */
807: public ListIterator listIterator() {
808: return new ListIteratorProxy(list.listIterator());
809: }
810:
811: /**
812: * @see java.util.List#listIterator(int)
813: */
814: public ListIterator listIterator(int i) {
815: return new ListIteratorProxy(list.listIterator(i));
816: }
817:
818: /**
819: * @see java.util.List#remove(int)
820: */
821: public Object remove(int i) {
822: write();
823: return list.remove(i);
824: }
825:
826: /**
827: * @see java.util.Collection#remove(Object)
828: */
829: public boolean remove(Object o) {
830: write();
831: return list.remove(o);
832: }
833:
834: /**
835: * @see java.util.Collection#removeAll(Collection)
836: */
837: public boolean removeAll(Collection c) {
838: write();
839: return list.removeAll(c);
840: }
841:
842: /**
843: * @see java.util.Collection#retainAll(Collection)
844: */
845: public boolean retainAll(Collection c) {
846: write();
847: return list.retainAll(c);
848: }
849:
850: /**
851: * @see java.util.List#set(int, Object)
852: */
853: public Object set(int i, Object o) {
854: write();
855: return list.set(i, o);
856: }
857:
858: /**
859: * @see java.util.Collection#size()
860: */
861: public int size() {
862: return list.size();
863: }
864:
865: /**
866: * @see java.util.List#subList(int, int)
867: */
868: public List subList(int i, int j) {
869: return list.subList(i, j);
870: }
871:
872: /**
873: * @see java.util.Collection#toArray()
874: */
875: public Object[] toArray() {
876: return list.toArray();
877: }
878:
879: /**
880: * @see java.util.Collection#toArray(Object[])
881: */
882: public Object[] toArray(Object[] array) {
883: return list.toArray(array);
884: }
885:
886: }
887:
888: protected interface DelayedOperation {
889: public void operate();
890:
891: public Object getAddedInstance();
892:
893: public Object getOrphan();
894: }
895:
896: /**
897: * Given a collection of entity instances that used to
898: * belong to the collection, and a collection of instances
899: * that currently belong, return a collection of orphans
900: */
901: protected static Collection getOrphans(Collection oldElements,
902: Collection currentElements, String entityName,
903: SessionImplementor session) throws HibernateException {
904:
905: // short-circuit(s)
906: if (currentElements.size() == 0)
907: return oldElements; // no new elements, the old list contains only Orphans
908: if (oldElements.size() == 0)
909: return oldElements; // no old elements, so no Orphans neither
910:
911: Type idType = session.getFactory().getEntityPersister(
912: entityName).getIdentifierType();
913:
914: // create the collection holding the Orphans
915: Collection res = new ArrayList();
916:
917: // collect EntityIdentifier(s) of the *current* elements - add them into a HashSet for fast access
918: java.util.Set currentIds = new HashSet();
919: for (Iterator it = currentElements.iterator(); it.hasNext();) {
920: Object current = it.next();
921: if (current != null
922: && ForeignKeys.isNotTransient(entityName, current,
923: null, session)) {
924: Serializable currentId = ForeignKeys
925: .getEntityIdentifierIfNotUnsaved(entityName,
926: current, session);
927: currentIds.add(new TypedValue(idType, currentId,
928: session.getEntityMode()));
929: }
930: }
931:
932: // iterate over the *old* list
933: for (Iterator it = oldElements.iterator(); it.hasNext();) {
934: Object old = it.next();
935: Serializable oldId = ForeignKeys
936: .getEntityIdentifierIfNotUnsaved(entityName, old,
937: session);
938: if (!currentIds.contains(new TypedValue(idType, oldId,
939: session.getEntityMode()))) {
940: res.add(old);
941: }
942: }
943:
944: return res;
945: }
946:
947: static void identityRemove(Collection list, Object object,
948: String entityName, SessionImplementor session)
949: throws HibernateException {
950:
951: if (object != null
952: && ForeignKeys.isNotTransient(entityName, object, null,
953: session)) {
954:
955: Type idType = session.getFactory().getEntityPersister(
956: entityName).getIdentifierType();
957:
958: Serializable idOfCurrent = ForeignKeys
959: .getEntityIdentifierIfNotUnsaved(entityName,
960: object, session);
961: Iterator iter = list.iterator();
962: while (iter.hasNext()) {
963: Serializable idOfOld = ForeignKeys
964: .getEntityIdentifierIfNotUnsaved(entityName,
965: iter.next(), session);
966: if (idType.isEqual(idOfCurrent, idOfOld, session
967: .getEntityMode(), session.getFactory())) {
968: iter.remove();
969: break;
970: }
971: }
972:
973: }
974: }
975:
976: public Object getIdentifier(Object entry, int i) {
977: throw new UnsupportedOperationException();
978: }
979:
980: public Object getOwner() {
981: return owner;
982: }
983:
984: public void setOwner(Object owner) {
985: this.owner = owner;
986: }
987:
988: }
|