001    package edu.nrao.sss.catalog;
002    
003    import java.io.Writer;
004    import java.util.ArrayList;
005    import java.util.Collection;
006    import java.util.Collections;
007    import java.util.Comparator;
008    import java.util.List;
009    import java.util.Set;
010    import java.util.concurrent.CopyOnWriteArraySet;
011    
012    import javax.xml.bind.JAXBException;
013    import javax.xml.bind.annotation.XmlAttribute;
014    
015    import edu.nrao.sss.util.Identifiable;
016    import edu.nrao.sss.util.JaxbUtility;
017    
018    /**
019     * A generic catalog. In addition to holding a collection of catalog
020     * {@link CatalogItem items}, this catalog supports the notion of
021     * {@link CatalogItemGroup groups} of catalog items.
022     * <p>
023     * <b>Version Info:</b>
024     * <table style="margin-left:2em">
025     *   <tr><td>$Revision: 2204 $</td></tr>
026     *   <tr><td>$Date: 2009-04-16 11:58:16 -0600 (Thu, 16 Apr 2009) $</td></tr>
027     *   <tr><td>$Author: dharland $ (last person to modify)</td></tr>
028     * </table></p>
029     * 
030     * @author David M. Harland
031     * @since 2006-10-23
032     */
033    public abstract class Catalog<I extends CatalogItem<I>,
034                                  G extends CatalogItemGroup<I,G,C>,
035                                  C extends Catalog<I,G,C>>
036      implements Cloneable, CatalogItemGroupListener<I,G,C>
037    {
038      private static final String INIT_NAME = "[New Catalog]";
039      private static final String NO_NAME   = "[Unnamed Catalog]";
040      private static final String DEFAULT_RESERVED_GROUP_NAME = "All Items";
041    
042      //============================================================================
043      // INSTANCE VARIABLES AND CONSTRUCTORS
044      //============================================================================
045    
046      //IDENTIFICATION
047      private long   id;
048      private String name;
049    
050      //CONTENTS
051      private G       mainGroup;  //Master list of items
052      private List<G> groups;     //Does NOT contain mainGroup
053    
054      //OTHER
055      private String reservedGroupName;
056    
057      //NONPERSISTED PROPERTIES
058      private Set<CatalogListener<I,G,C>> listeners;
059      private boolean                     suppressNotification;
060    
061      /** Helps create a new catalog with a default name. */
062      public Catalog()
063      {
064        this(INIT_NAME);
065      }
066      
067      /**
068       * Helps create a new catalog with the given name.
069       * 
070       * @param nameOfCatalog the name of this catalog.  If this value is
071       *                      <i>null</i>, this catalog will be given a
072       *                      default name.
073       */
074      public Catalog(String nameOfCatalog)
075      {
076        id   = getIdOfUnidentified();
077        name = (nameOfCatalog == null) ? NO_NAME : nameOfCatalog;
078        
079        reservedGroupName = DEFAULT_RESERVED_GROUP_NAME;
080        
081        groups    = new ArrayList<G>();
082        mainGroup = createMainGroup();
083        
084        listeners            = new CopyOnWriteArraySet<CatalogListener<I,G,C>>();
085        suppressNotification = false;
086        
087        mainGroup.addListener(this);
088      }
089    
090      /**
091       * Creates and returns a new group to use as the holder of items for this
092       * catalog.
093       * @return a new group to use as this catalog's main group.
094       */
095      abstract protected G createMainGroup();
096      
097      /**
098       * Returns a group of the same type of items used by this catalog.
099       * @return a group of the same type of items used by this catalog.
100       */
101      abstract public G createGroup();
102      
103      //============================================================================
104      // IDENTIFICATION
105      //============================================================================
106    
107      /**
108       * Sets the ID of this catalog.  If {@code id} is <i>null</i>, this catalog's
109       * ID will be set to an <i>unidentified</i> ID.
110       * 
111       * @param id a new ID for this catalog.
112       */
113      protected void setId(Long id)
114      {
115        this.id = (id == null) ? getIdOfUnidentified() : id;
116      }
117      
118      /**
119       * Returns a value that represents an undefined ID.
120       * Subclasses may override this method to suit their needs.
121       */
122      protected long getIdOfUnidentified()
123      {
124        return -1L;
125      }
126    
127      /**
128       * Resets this catalog's ID, and the IDs of all its groups,
129       * to a value that represents the unidentified state.
130       * Note that it does nothing to the IDs of its items, because
131       * {@link CatalogItem} does not implement the {@link Identifiable}
132       * interface.  Subclasses of this catalog whose items are
133       * <tt>Identifiable</tt> should override this method, call
134       * <tt>super.clearId()</tt>, then clear the IDs of their items.
135       * <p>
136       * This method is useful for preparing a catalog for storage in a database.
137       * The ID property (as of now, though this may change in the future) is
138       * used by our persistence mechanism to identify objects.  If you are
139       * persisting this catalog for the first time, you may need to call
140       * this method before performing a save.  This is especially true if
141       * you have created this source from XML, as the XML unmarshalling
142       * brings along the ID property.</p> 
143       */
144      public void clearId()
145      {
146        this.id = getIdOfUnidentified();
147        
148        for (G group : this.groups)
149          group.setId(null);
150      }
151      
152      /**
153       * Returns this catalog's ID.
154       * @return this catalog's ID.
155       */
156      @XmlAttribute
157      public Long getId()
158      {
159        return id;
160      }
161    
162      /**
163       * Sets the name of this catalog.
164       * <p>
165       * If {@code newName} is <i>null</i> or the empty string
166       * (<tt>""</tt>), the request to change the name will be
167       * denied and the current name will remain in place.</p>
168       * 
169       * @param newName the new name for this catalog.
170       */
171      public void setName(String newName)
172      {
173        if (newName != name && newName.length() > 0)
174          name = newName;
175      }
176      
177      /**
178       * Returns the name of this catalog.
179       * @return the name of this catalog.
180       */
181      public String getName()
182      {
183        return name;
184      }
185      
186      //============================================================================
187      // OTHER PROPERTIES
188      //============================================================================
189    
190      /**
191       * Returns a list of notes about this catalog.
192       * Each note is free-form text with no particular structure.
193       * <p>
194       * This method returns the list actually held by this {@code Catalog}, so
195       * any list manipulations may be performed by first fetching the list and
196       * then operating on it.</p>
197       * 
198       * @return a list of notes about this catalog.
199       */
200      public List<String> getNotes()
201      {
202        return mainGroup.getNotes();
203      }
204      
205      /** This is here for mechanisms that need setX/getX pairs, such as JAXB. */
206      @SuppressWarnings("unused")
207      private void setNotes(List<String> replacementList)
208      {
209        mainGroup.setNotes(replacementList);
210      }
211    
212      //============================================================================
213      // ADDING ITEMS & GROUPS 
214      //============================================================================
215      //----------------------------------------------------------------------------
216      // Items
217      //----------------------------------------------------------------------------
218    
219      /**
220       * Adds a new item to this catalog.  The new item is added only if this
221       * catalog holds no item to which it is equal.
222       * 
223       * @param newItem a new item for this catalog.
224       * 
225       * @return the item that was added to, or was already a part of,
226       *         this catalog.  This will be either {@code newItem} if this
227       *         catalog contains no item equal to {@code newItem}, or the
228       *         first item in this catalog that is equal to {@code newItem}. 
229       */
230      public I addItem(I newItem)
231      {
232        return mainGroup.add(newItem);
233      }
234      
235      /**
236       * Adds a new item to this catalog at the specified position.
237       * The new item is added only if this
238       * catalog holds no item to which it is equal.
239       * 
240       * @param index the position at which to insert the new item.
241       * 
242       * @param newItem a prospective new item in this catalog.
243       * 
244       * @return the item that was added to, or was already a part of,
245       *         this catalog.  This will be either {@code newItem} if this
246       *         catalog contains no item equal to {@code newItem}, or the
247       *         first item in this catalog that is equal to {@code newItem}.
248       */
249      public I addItem(int index, I newItem)
250      {
251        return mainGroup.add(index, newItem);
252      }
253      
254      /** Adds a new item to this catalog. */
255      I addItem(I newItem, G caller)
256      {
257        return (caller == mainGroup) ? newItem : addItem(newItem);
258      }
259      
260      /**
261       * Adds {@code newItems} to this catalog.
262       * Only non-null elements of {@code newItems} that are not
263       * already part of this catalog are classified as new items.
264       * 
265       * @param newItems new items in this catalog.
266       * 
267       * @return the collection of items that were added to this catalog.
268       */
269      public Collection<I> addItems(Collection<? extends I> newItems)
270      {
271        Collection<I> addedItems = new ArrayList<I>();
272        
273        if (newItems != null)
274        {
275          for (I newItem : newItems)
276          {
277            if ((newItem != null) && !contains(newItem))
278            {
279              addedItems.add(this.addItem(newItem));
280            }
281          }
282        }
283        
284        return addedItems;
285      }
286      
287      /**
288       * Adds {@code newItems} to this catalog at the specified position.
289       * Only non-null elements of {@code newItems} that are not
290       * already part of this catalog are classified as new items.
291       * 
292       * @param index the position at which to insert the first new item.
293       *              Subsequent new items are added at successive positions.
294       * 
295       * @param newItems new items in this catalog.
296       * 
297       * @return the collection of items that were added to this catalog.
298       */
299      public Collection<I> addItems(int index, Collection<? extends I> newItems)
300      {
301        Collection<I> addedItems = new ArrayList<I>();
302        
303        if (newItems != null)
304        {
305          for (I newItem : newItems)
306          {
307            if ((newItem != null) && !contains(newItem))
308            {
309              addedItems.add(this.addItem(index++, newItem));
310            }
311          }
312        }
313        
314        return addedItems;
315      }
316      
317      /**
318       * Adds {@code newItem} even if this catalog already has an item to which
319       * it is equal.  Contrast this with {@link #addItem(CatalogItem)}.
320       * <p>
321       * The only times {@code newItem} is <i>not</i> added to this catalog are
322       * when it is <i>null</i> and when a reference to it is already contained
323       * in this catalog.</p>
324       * 
325       * @param newItem a potential new item for this catalog.
326       * 
327       * @return {@code newItem}.
328       */
329      public I addItemEvenIfEqualToCurrentItem(I newItem)
330      {
331        return mainGroup.addEvenIfEqualToCurrentMember(newItem);
332      }
333      
334      /**
335       * Adds {@code newItem} even if this catalog already has an item to which
336       * it is equal.  Contrast this with {@link #addItem(int, CatalogItem)}.
337       * <p>
338       * The only times {@code newItem} is <i>not</i> added to this catalog are
339       * when it is <i>null</i> and when a reference to it is already contained
340       * in this catalog.</p>
341       * 
342       * @param index the position at which to add {@code newItem}.
343       * 
344       * @param newItem a potential new item for this catalog.
345       * 
346       * @return {@code newItem}.
347       */
348      public I addItemEvenIfEqualToCurrentItem(int index, I newItem)
349      {
350        return mainGroup.addEvenIfEqualToCurrentMember(index, newItem);
351      }
352      
353      /** Forces the addition of a new item. */
354      I addItemEvenIfEqualToCurrentItem(I newItem, G caller)
355      {
356        return (caller == mainGroup) ? newItem
357                                     : addItemEvenIfEqualToCurrentItem(newItem);
358      }
359    
360      //----------------------------------------------------------------------------
361      // Groups
362      //----------------------------------------------------------------------------
363    
364      /**
365       * Adds a new group to this catalog.
366       * All of the new group's members are also added to this catalog.
367       * <p>
368       * This catalog is now enforcing uniqueness for the names of its groups.
369       * If {@code newGroup} has the same name as a group that is already in
370       * this catalog, a <b>side effect</b> of this {@code add} method is
371       * that {@code newGroup}'s name will be changed to
372       * <tt>newGroup.getName() + " (Copy <i>#</i>)"</tt>, where "#" is the
373       * n<sup>th</sup> duplication of this name.</p>
374       * 
375       * @param newGroup a new group of items for this catalog.
376       * 
377       * @return <i>true</i> if this catalog changed as a result of the call.
378       *         Reasons for a return value of <i>false</i>:
379       *         <ol>
380       *           <li>{@code newGroup} is <i>null</i></li>
381       *           <li>{@code newGroup} is already a part of this catalog</li>
382       *         </ol>
383       */
384      public boolean addGroup(G newGroup)
385      {
386        return addGroup(groups.size(), newGroup);
387      }
388      
389      /**
390       * Adds a new group to this catalog at the specified position.
391       * All of the new group's members are also added to this catalog.
392       * <p>
393       * This catalog is now enforcing uniqueness for the names of its groups.
394       * If {@code newGroup} has the same name as a group that is already in
395       * this catalog, a <b>side effect</b> of this {@code add} method is
396       * that {@code newGroup}'s name will be changed to
397       * <tt>newGroup.getName() + " (Copy <i>#</i>)"</tt>, where "#" is the
398       * n<sup>th</sup> duplication of this name.</p>
399       * 
400       * @param index the position at which to insert the new group.
401       * 
402       * @param newGroup a new group of items for this catalog.
403       * 
404       * @return <i>true</i> if this catalog changed as a result of the call.
405       *         Reasons for a return value of <i>false</i>:
406       *         <ol>
407       *           <li>{@code newGroup} is <i>null</i></li>
408       *           <li>{@code newGroup} is already a part of this catalog</li>
409       *         </ol>
410       */
411      @SuppressWarnings("unchecked")
412      public boolean addGroup(int index, G newGroup)
413      {
414        //Quick exit if newGroup is null or is already in this catalog
415        if ((newGroup == null) || (newGroup.getCatalog() == this))
416          return false;
417        
418        //Remove newGroup from its former catalog
419        C formerCatalog = newGroup.getCatalog();
420        if (formerCatalog != null)
421          formerCatalog.groups.remove(newGroup);
422        
423        //Tell newGroup it now belongs to this catalog
424        newGroup.simplySetCatalog((C)this);
425    
426        //Add the newGroup's members to this catalog.  If this catalog already
427        //contains EQUAL members, replace the corresponding member of the group
428        //with a reference to the equal member in this catalog.
429        if (newGroup != mainGroup)  //Redundant?
430        {
431          //addAll(newGroup.getMembers());
432          List<I> newMemberList   =  newGroup.getInternalMemberList();
433          List<I> catalogItemList = mainGroup.getInternalMemberList();
434          
435          int memberCount = newMemberList.size();
436          for (int i=0; i < memberCount; i++)
437          {
438            I newItem = newMemberList.get(i);
439            
440            int itemIndex = catalogItemList.indexOf(newItem);
441            
442            //If catalog already contains item that is EQUAL, but not identical,
443            //to newItem, update the group with the item from the catalog. 
444            if (itemIndex >= 0)
445            {
446              I catalogItem = catalogItemList.get(itemIndex);
447              if (newItem != catalogItem) //but newItem.equals(catalogItem)
448              {
449                newMemberList.remove(i);
450                newMemberList.add(i, catalogItem);
451              }
452            }
453            //Otherwise, add the new item to this catalog.
454            else
455            {
456              mainGroup.add(newItem);
457            }
458          }
459        }
460    
461        //The new group's name might already be in use by this catalog and
462        //could even be the reserved name.  Change it, if necessary.
463        newGroup.adjustNameForCatalog((C)this);
464        
465        //Add newGroup to our collection
466        groups.add(index, newGroup);
467        tellListenersAboutNewGroup(newGroup, index);
468    
469        return true;
470      }
471      
472      /**
473       * Adds {@code newGroups} to this catalog.
474       * <p>
475       * This is a convenience method that is the equivalent of iterating over
476       * {@code newGroups} and calling {@link #addGroup(CatalogItemGroup)}.</p>
477       * 
478       * @param newGroups new groups in this catalog.
479       * 
480       * @return <i>true</i> if one or more groups were added to this catalog.
481       */
482      public boolean addGroups(Collection<? extends G> newGroups)
483      {
484        boolean added = false;
485        
486        if (newGroups != null)
487        {
488          for (G newGroup : newGroups)
489            added |= addGroup(newGroup);
490        }
491        
492        return added;
493      }
494      
495      /**
496       * Adds {@code newGroups} to this catalog at the specified position.
497       * <p>
498       * This is a convenience method that is the equivalent of iterating over
499       * {@code newGroups} and calling {@link #addGroup(int, CatalogItemGroup)}.</p>
500       * 
501       * @param index the position at which to insert the first new group.
502       *              Subsequent new groups are added at successive positions.
503       * 
504       * @param newGroups new groups in this catalog.
505       * 
506       * @return <i>true</i> if one or more groups were added to this catalog.
507       */
508      public boolean addGroups(int index, Collection<? extends G> newGroups)
509      {
510        boolean added = false;
511        
512        if (newGroups != null)
513        {
514          for (G newGroup : newGroups)
515            added |= addGroup(index++, newGroup);
516        }
517        
518        return added;
519      }
520    
521      /**
522       * Adds newGroup directly to this catalog's list of groups
523       * without also adding its sources and lookup tables, and
524       * without notifying listeners.
525       * This method is used only by the CatalogItemGroup class and
526       * should be used only if you absolutely know what you are doing.
527       */
528      void simplyAddGroup(G newGroup)
529      {
530        if ((newGroup != null) && !groups.contains(newGroup))
531          groups.add(newGroup);
532      }
533      
534      @SuppressWarnings("unchecked")
535      protected void tellListenersAboutNewGroup(G newGroup, int index)
536      {
537        if (!suppressNotification)
538          for (CatalogListener<I,G,C> listener : listeners)
539            listener.groupAdded((C)this, newGroup, index);
540      }
541    
542      //============================================================================
543      // REMOVING ITEMS & GROUPS 
544      //============================================================================
545      
546      //Be aware: while the add-item and move-item methods simply delegate to the
547      //mainGroup, for removal of items, the mainGroup calls back into this
548      //catalog.  This is done so that any items removed from mainGroup may
549      //also be removed from all other groups in this catalog.  One consequence
550      //of this is that the group's remove methods are not called, and this
551      //bypasses the group listeners.  To remedy this, we notify the listeners
552      //here.  (Note that this catalog is likely the only listener to the
553      //main group, but our code here should not rely on that.)
554      
555      /** Removes an item from this catalog, if the caller is the main group. */
556      I removeItem(I item, G caller)
557      {
558        return (caller == mainGroup) ? removeItem(item) : null;
559      }
560    
561      /**
562       * Removes {@code item} from this catalog, if present.
563       * The {@code item}
564       * is also removed from every group of this catalog in which it is present.
565       * 
566       * @param item the item that is to be removed from this catalog.
567       * 
568       * @return {@code item} if it had been an item in this catalog,
569       *         <i>null</i> if it had not.
570       */
571      public I removeItem(I item)
572      {
573        List<I> items = mainGroup.getInternalMemberList();
574    
575        I formerItem = items.contains(item) ? item : null;
576        
577        if (formerItem != null)
578        {
579          //Remove the item from all groups
580          for (G group : groups)
581            group.remove(formerItem);
582          
583          int index = items.indexOf(formerItem);
584          items.remove(formerItem);
585          memberRemoved(mainGroup, formerItem, index);
586        }
587        
588        return formerItem;
589      }
590      
591      /**
592       * Removes the item found at {@code index}.
593       * @param index the index of the item to be removed.
594       * @return the removed item, or <i>null</i> if no item was removed.
595       */
596      public I removeItem(int index)
597      {
598        return removeItem(mainGroup.get(index));
599      }
600    
601      /**
602       * Returns <i>true</i> if this catalog will allow removal of {@code group}.
603       * @param group the group to be removed.
604       * @return <i>true</i> if this catalog will allow removal of {@code group}.
605       */
606      boolean allowsRemovalOf(G group)
607      {
608        return group != mainGroup;
609      }
610      
611      /**
612       * Removes {@code group} from this catalog.
613       * Note that the items held by {@code group} are <i>not</i> removed
614       * from this catalog.
615       * 
616       * @param group the group that is to be removed from this catalog.
617       * 
618       * @return {@code group} if it had belonged to this catalog and was
619       *         able to be removed, <i>null</i> otherwise.
620       */
621      public G removeGroup(G group)
622      {
623        //Quick exit if group is null, does not belong to this catalog,
624        //or is not permitted to leave
625        if ((group == null) || (group.getCatalog() != this) ||
626            !this.allowsRemovalOf(group))
627          return null;
628        
629        //Tell group that it belongs to no catalog
630        group.simplySetCatalog(null);
631        
632        //Remove the group from our collection
633        int     index   = groups.indexOf(group);
634        boolean removed = groups.remove(group);
635        if (removed)
636          tellListenersAboutFormerGroup(group, index);
637    
638        return group;
639      }
640    
641      /**
642       * Removes the group at {@code index} from this catalog.
643       * Note that the items held by {@code group} are <i>not</i> removed
644       * from this catalog.
645       * 
646       * @param index the index of the group to be removed.
647       * 
648       * @return the group found at {@code index} if it was
649       *         able to be removed, <i>null</i> otherwise.
650       */
651      public G removeGroup(int index)
652      {
653        return removeGroup(groups.get(index));
654      }
655      
656      /** Removes items from this catalog, if the caller is the main group. */
657      boolean removeItems(Collection<? extends I> items, G caller)
658      {
659        return (caller == mainGroup) ? removeItems(items) : false;
660      }
661    
662      /**
663       * Removes all elements of {@code unwantedItems} that are contained in this
664       * catalog. 
665       * @param unwantedItems the items that are to be removed from this catalog.
666       * @return <i>true</i> if this catalog changed as a result of the call.
667       */
668      boolean removeItems(Collection<? extends I> unwantedItems)
669      {
670        boolean result = false;
671        
672        if (unwantedItems != null)
673        {
674          for (G group : groups)
675            group.removeAll(unwantedItems);
676          
677          List<I> myItems = mainGroup.getInternalMemberList();
678          for (I unwantedItem : unwantedItems)
679          {
680            int index = myItems.indexOf(unwantedItem);
681            if (index >= 0)
682            {
683              result |= myItems.remove(unwantedItem);
684              memberRemoved(mainGroup, unwantedItem, index);
685            }
686          }
687        }
688        
689        return result;
690      }
691    
692      /**
693       * Removes all groups from this catalog.
694       * No catalog items are removed as a result of the call.
695       *
696       * @see #removeAllItems()
697       * @see #clear()
698       */
699      @SuppressWarnings("unchecked")
700      public void removeAllGroups()
701      {
702        for (G group : groups)
703          group.simplySetCatalog(null);
704        
705        List<G> clonedGroupList = null;
706        synchronized(this) {
707          clonedGroupList = (List<G>)((ArrayList<G>)groups).clone();
708        }
709    
710        //Remove all client-made groups; we NEVER remove the main group
711        groups.clear();
712        
713        //Notify listeners.  Traversing the group list backwards ensures
714        //that the index sent to listener matches current state of list.
715        for (int g=clonedGroupList.size()-1; g >= 0; g--)
716          tellListenersAboutFormerGroup(clonedGroupList.get(g), g);
717      }
718      
719      /**
720       * Removes all items from this catalog and its groups,
721       * but does not remove the groups themselves.
722       * @see #removeAllGroups()
723       * @see #clear()
724       */
725      @SuppressWarnings("unchecked")
726      public void removeAllItems()
727      {
728        for (G group : groups)
729          group.clear();
730    
731        List<I> myItems = mainGroup.getInternalMemberList();
732        
733        List<I> clonedItemList = null;
734        synchronized(this) {
735          clonedItemList = (List<I>)((ArrayList<I>)myItems).clone();
736        }
737        
738        myItems.clear();
739        
740        //Notify listeners.  Traversing the member list backwards ensures
741        //that the index sent to listener matches current state of list.
742        for (int i=clonedItemList.size()-1; i >= 0; i--)
743          memberRemoved(mainGroup, clonedItemList.get(i), i);
744      }
745      
746      /**
747       * Removes all groups <i>and</i> all items from this catalog.
748       * @see #removeAllItems()
749       * @see #removeAllGroups()
750       */
751      public void clear()
752      {
753        removeAllGroups();
754        removeAllItems();
755      }
756      
757      @SuppressWarnings("unchecked")
758      protected void tellListenersAboutFormerGroup(G formerGroup, int index)
759      {
760        if (!suppressNotification)
761          for (CatalogListener<I,G,C> listener : listeners)
762            listener.groupRemoved((C)this, formerGroup, index);
763      }
764    
765      //============================================================================
766      // MOVING ITEMS & GROUPS 
767      //============================================================================
768      //----------------------------------------------------------------------------
769      // Items 
770      //----------------------------------------------------------------------------
771    
772      /**
773       * Moves {@code item} to an index one higher than its current index.
774       * The item will not be moved if it is not in this catalog, or if
775       * it is already in the highest position.
776       * <p>
777       * If {@code item} is in this catalog in multiple positions, the instance
778       * with the <i>highest</i> index will be moved.</p> 
779       * 
780       * @param item the item to be moved.
781       * 
782       * @return if no movement occurred, <i>null</i> is returned.  Otherwise
783       *         the item that was moved is returned.  This will be either
784       *         {@code item}, or an equal item that was already part of
785       *         this catalog. 
786       */
787      public I incrementIndexOfItem(I item)
788      {
789        return mainGroup.incrementIndexOf(item);
790      }
791    
792      /**
793       * Moves {@code item} to an index one lower than its current index.
794       * The item will not be moved if it is not in this catalog, or if
795       * it is already in the lowest position.
796       * <p>
797       * If {@code item} is in this catalog in multiple positions, the instance
798       * with the <i>lowest</i> index will be moved.</p> 
799       * 
800       * @param item the item to be moved.
801       * 
802       * @return if no movement occurred, <i>null</i> is returned.  Otherwise
803       *         the item that was moved is returned.  This will be either
804       *         {@code item}, or an equal item that was already part of
805       *         this catalog. 
806       */
807      public I decrementIndexOfItem(I item)
808      {
809        return mainGroup.decrementIndexOf(item);
810      }
811    
812      /**
813       * Moves the item at {@code index} to an index of {@code index}-plus-one.
814       * No move will occur if {@code index} is out of bounds, or if it is already
815       * the highest index of this catalog.
816       * 
817       * @param index the index of the item to be moved.
818       * 
819       * @return the item that was moved, or <i>null</i> if no item was moved. 
820       */
821      public I incrementIndexOfItemAt(int index)
822      {
823        return mainGroup.incrementIndexOfMemberAt(index);
824      }
825    
826      /**
827       * Moves the item at {@code index} to an index of {@code index}-minus-one.
828       * No move will occur if {@code index} is out of bounds, or if it is already
829       * the lowest index of this catalog.
830       * 
831       * @param index the index of the item to be moved.
832       * 
833       * @return the item that was moved, or <i>null</i> if no item was moved. 
834       */
835      public I decrementIndexOfItemAt(int index)
836      {
837        return mainGroup.decrementIndexOfMemberAt(index);
838      }
839    
840      /**
841       * Swaps the items at the given positions.
842       * <p>
843       * If the indices are equal, no action is taken.
844       * If one or both of the indices are not in the range
845       * [0,size()), the underlying {@link List} will throw
846       * an exception.</p>
847       * 
848       * @param index1 the position of the one of the items.
849       * @param index2 the position of another of the items.
850       * 
851       * @return a list with the item originally at the lower index followed by
852       *         the item originally at the higher index.  If no swap was made
853       *         the returned list will be empty.
854       */
855      public List<I> swapItems(int index1, int index2)
856      {
857        return mainGroup.swap(index1, index2);
858      }
859    
860      /**
861       * Moves an item {@code fromIndex} {@code toIndex}.
862       * 
863       * @param fromIndex the current index of an item.
864       * @param toIndex the new index of that same item.
865       * @return the item that moved.
866       */
867      public I moveItem(int fromIndex, int toIndex)
868      {
869        return mainGroup.move(fromIndex, toIndex);
870      }
871    
872      /**
873       * Moves {@code item} {@code toIndex}.
874       * 
875       * @param item the item to be moved.
876       * @param toIndex the new position for {@code item}.
877       * @return the item that moved, or <i>null</i> if no movement occurred.
878       *         Note that the item that moved might be {@code item} or,
879       *         alternatively, an item <em>equal to</em> {@code item},
880       *         if this catalog has such an item.
881       */
882      public I moveItem(I item, int toIndex)
883      {
884        return mainGroup.move(item, toIndex);
885      }
886    
887      /**
888       * Uses {@code comparator} to sort the items of this catalog.
889       * @param comparator used to order this catalog's items.
890       */
891      public void sort(Comparator<? super I> comparator)
892      {
893        mainGroup.sort(comparator);
894      }
895      
896      //----------------------------------------------------------------------------
897      // Groups
898      //----------------------------------------------------------------------------
899      
900      /**
901       * Moves {@code group} to an index one higher than its current index.
902       * The group will not be moved if it is not in this catalog, or if
903       * it is already in the highest position.
904       * <p>
905       * If {@code group} is in this catalog in multiple positions, the instance
906       * with the <i>highest</i> index will be moved.  (It is not generally
907       * possible for a group to occupy multiple positions in a catalog.) Note
908       * that the special group that holds all items of this catalog may
909       * not be moved.</p> 
910       * 
911       * @param group the group to be moved.
912       * 
913       * @return if no movement occurred, <i>null</i> is returned.  Otherwise
914       *         the group that was moved is returned.  This will be either
915       *         {@code group}, or an equal group that was already part of
916       *         this catalog. 
917       */
918      public G incrementIndexOfGroup(G group)
919      {
920        G movedGroup = null;
921        
922        int currentIndex = groups.indexOf(group);
923        int highestIndex = groups.size() - 1;
924        
925        if ((0 <= currentIndex) && (currentIndex < highestIndex))
926        {
927          suppressNotification = true;
928          
929          movedGroup = removeGroup(currentIndex);
930          groups.add(currentIndex + 1, movedGroup);
931          
932          suppressNotification = false;
933          tellListenersAboutMovedGroup(movedGroup, currentIndex, currentIndex+1);
934        }
935        
936        return movedGroup;
937      }
938    
939      /**
940       * Moves {@code group} to an index one lower than its current index.
941       * The group will not be moved if it is not in this catalog, or if
942       * it is already in the lowest position.
943       * <p>
944       * If {@code group} is in this catalog in multiple positions, the instance
945       * with the <i>lowest</i> index will be moved.  (It is not generally
946       * possible for a group to occupy multiple positions in a catalog.) Note
947       * that the special group that holds all items of this catalog may
948       * not be moved.</p> 
949       * 
950       * @param group the group to be moved.
951       * 
952       * @return if no movement occurred, <i>null</i> is returned.  Otherwise
953       *         the group that was moved is returned.  This will be either
954       *         {@code group}, or an equal group that was already part of
955       *         this catalog. 
956       */
957      public G decrementIndexOfGroup(G group)
958      {
959        G movedGroup = null;
960        
961        int currentIndex = groups.indexOf(group);
962        
963        if (0 < currentIndex)
964        {
965          suppressNotification = true;
966          
967          movedGroup = removeGroup(currentIndex);
968          groups.add(currentIndex - 1, movedGroup);
969          
970          suppressNotification = false;
971          tellListenersAboutMovedGroup(movedGroup, currentIndex, currentIndex-1);
972        }
973        
974        return movedGroup;
975      }
976    
977      /**
978       * Moves the group at {@code index} to an index of {@code index}-plus-one.
979       * No move will occur if {@code index} is out of bounds, or if it is already
980       * the highest index of this catalog.
981       * 
982       * @param index the index of the group to be moved.
983       * 
984       * @return the group that was moved, or <i>null</i> if no group was moved.
985       */
986      public G incrementIndexOfGroupAt(int index)
987      {
988        G movedGroup = null;
989        
990        int highestIndex = groups.size() - 1;
991        
992        if ((0 <= index) && (index < highestIndex))
993        {
994          movedGroup = groups.remove(index);
995          groups.add(index + 1, movedGroup);
996          
997          suppressNotification = false;
998          tellListenersAboutMovedGroup(movedGroup, index, index+1);
999        }
1000        
1001        return movedGroup;
1002      }
1003    
1004      /**
1005       * Moves the group at {@code index} to an index of {@code index}-minus-one.
1006       * No move will occur if {@code index} is out of bounds, or if it is already
1007       * the lowest index of this catalog.
1008       * 
1009       * @param index the index of the group to be moved.
1010       * 
1011       * @return the group that was moved, or <i>null</i> if no group was moved. 
1012       */
1013      public G decrementIndexOfGroupAt(int index)
1014      {
1015        G movedGroup = null;
1016        
1017        int highestIndex = groups.size() - 1;
1018        
1019        if ((0 < index) && (index <= highestIndex))
1020        {
1021          movedGroup = groups.remove(index);
1022          groups.add(index - 1, movedGroup);
1023          
1024          suppressNotification = false;
1025          tellListenersAboutMovedGroup(movedGroup, index, index+1);
1026        }
1027        
1028        return movedGroup;
1029      }
1030    
1031      /**
1032       * Swaps the groups at the given positions.
1033       * <p>
1034       * If the indices are equal, no action is taken.
1035       * If one or both of the indices are not in the range
1036       * [0,size()), the underlying {@link List} will throw
1037       * an exception.</p>
1038       * 
1039       * @param index1 the position of the one of the groups.
1040       * @param index2 the position of another of the groups.
1041       * 
1042       * @return a list with the group originally at the lower index followed by
1043       *         the group originally at the higher index.  If no swap was made
1044       *         the returned list will be empty.
1045      */
1046      public List<G> swapGroups(int index1, int index2)
1047      {
1048        List<G> swappedGroups = new ArrayList<G>(); 
1049    
1050        //No swap if indices are same
1051        if (index1 != index2)
1052        {
1053          int low  = Math.min(index1, index2);
1054          int high = Math.max(index1, index2);
1055        
1056          G lowGroup  = groups.get(low);
1057          G highGroup = groups.get(high);
1058          
1059          groups.remove(high);
1060          groups.add(high, lowGroup);
1061          groups.remove(low);
1062          groups.add(low, highGroup);
1063          
1064          tellListenersAboutMovedGroup(highGroup, high, low);
1065          
1066          //The indices are incremented here because listeners will receive this
1067          //message after the one above.  So to them, it looks as if lowGroup is
1068          //currently at position low+1.
1069          tellListenersAboutMovedGroup(lowGroup, low+1, high+1);
1070    
1071          swappedGroups.add(lowGroup);
1072          swappedGroups.add(highGroup);
1073        }
1074        
1075        return swappedGroups;
1076      }
1077    
1078      /**
1079       * Moves a group {@code fromIndex} {@code toIndex}.
1080       * 
1081       * @param fromIndex the current index of a group.
1082       * @param toIndex the new index of that same group.
1083       * @return the group that moved, even if it moved back to the same position.
1084      */
1085      public G moveGroup(int fromIndex, int toIndex)
1086      {
1087        G movedGroup = null;
1088        
1089        movedGroup = groups.remove(fromIndex);
1090          
1091        if (movedGroup != null)
1092        {
1093          int indexMax = groups.size();
1094          if (toIndex > indexMax)
1095            toIndex = indexMax;
1096          
1097          groups.add(toIndex, movedGroup);
1098          
1099          suppressNotification = false;
1100          tellListenersAboutMovedGroup(movedGroup, fromIndex, toIndex);
1101        }
1102    
1103        return movedGroup;
1104      }
1105    
1106      /**
1107       * Moves {@code group} {@code toIndex}.
1108       * 
1109       * @param group the group to be moved.
1110       * @param toIndex the new position for {@code group}.
1111       * @return the group that moved, or <i>null</i> if no movement occurred.
1112       */
1113      public G moveGroup(G group, int toIndex)
1114      {
1115        int fromIndex = groups.indexOf(group);
1116        
1117        return (fromIndex < 0) ? null : moveGroup(fromIndex, toIndex);
1118      }
1119      
1120      /**
1121       * Uses {@code comparator} to sort the members of this group.
1122       * @param comparator used to order this group's members
1123       */
1124      @SuppressWarnings("unchecked")
1125      public void sortGroups(Comparator<? super G> comparator)
1126      {
1127        Collections.sort(groups, comparator);
1128    
1129        if (!suppressNotification)
1130          for (CatalogListener<I,G,C> listener : listeners)
1131            listener.groupsSorted((C)this);
1132      }
1133    
1134      @SuppressWarnings("unchecked")
1135      protected void tellListenersAboutMovedGroup(G group,
1136                                                  int fromIndex, int toIndex)
1137      {
1138        if (!suppressNotification)
1139          for (CatalogListener<I,G,C> listener : listeners)
1140            listener.groupMoved((C)this, group, fromIndex, toIndex);
1141      }
1142    
1143      //============================================================================
1144      // FETCHING ITEMS & GROUPS
1145      //============================================================================
1146      //----------------------------------------------------------------------------
1147      // Items
1148      //----------------------------------------------------------------------------
1149    
1150      /** 
1151       * Returns the number of items in this catalog.
1152       * (A group is <i>not</i> considered to be an item.)
1153       * 
1154       * @return the number of items in this catalog.
1155       */
1156      public int size()
1157      {
1158        return mainGroup.size();
1159      }
1160      
1161      /**
1162       * Returns the item at the given position.
1163       * 
1164       * @param index the position of the item to return.
1165       * 
1166       * @return the item at {@code index}.
1167       */
1168      public I getItem(int index)
1169      {
1170        return mainGroup.get(index);
1171      }
1172    
1173      /**
1174       * Returns a list of this catalog's items.
1175       * (A group is <i>not</i> considered to be an item.)
1176       * <p>
1177       * Note that the returned list is <i>not</i> held internally by this catalog.
1178       * This means that any changes made to the returned list will <i>not</i>
1179       * be reflected in this object.  The elements of the list, though, are the
1180       * actual items of this catalog, so changes made to them will be reflected
1181       * in this catalog.</p>
1182       * 
1183       * @return a list of this catalog's items.  The returned value is guaranteed
1184       *         to be non-null, but it may be an empty list.
1185       */
1186      public List<I> getItems()
1187      {
1188        return mainGroup.getAll();
1189      }
1190    
1191      /**
1192       * Returns the internal item list.
1193       * This method is meant solely for use by frameworks (such
1194       * as JAXB or Hibernate) that rely on getX/setX pairs for
1195       * persisted properties.  It is {@code protected}, as opposed to
1196       * {@code private}, so that subclasses can create and annotate
1197       * methods that call this one.
1198       */
1199      protected List<I> getInternalItemList()
1200      {
1201        return mainGroup.getInternalMemberList();
1202      }
1203      
1204      /**
1205       * Replaces the internal item list with {@code replacementList}.
1206       * This method is meant solely for use by frameworks (such
1207       * as JAXB or Hibernate) that rely on getX/setX pairs for
1208       * persisted properties.  It is {@code protected}, as opposed to
1209       * {@code private}, so that subclasses can create and annotate
1210       * methods that call this one.
1211       */
1212      protected void setInternalItemList(List<I> replacementList)
1213      {
1214        mainGroup.setInternalMemberList(replacementList);
1215      }
1216    
1217      /**
1218       * Returns <i>true</i> if this catalog holds {@code item}.
1219       * @param item a possible item in this catalog.
1220       * @return <i>true</i> if this catalog holds {@code item}.
1221       */
1222      public boolean contains(I item)
1223      {
1224        return mainGroup.contains(item);
1225      }
1226      
1227      /**
1228       * Returns the index of the first occurence of {@code item} in this catalog.
1229       * See {@link java.util.List#indexOf(Object)} for more details.
1230       * @param item a potential item of this catalog
1231       * @return the index of the first occurence of {@code item} in this catalog,
1232       *         or -1 if this catalog does not contain {@code item}.
1233       */
1234      public int indexOf(I item)
1235      {
1236        return mainGroup.getInternalMemberList().indexOf(item);
1237      }
1238      
1239      /**
1240       * Returns the index of the lasst occurence of {@code item} in this catalog.
1241       * See {@link java.util.List#lastIndexOf(Object)} for more details.
1242       * @param item a potential item of this catalog
1243       * @return the index of the last occurence of {@code item} in this catalog,
1244       *         or -1 if this catalog does not contain {@code item}.
1245       */
1246      public int lastIndexOf(I item)
1247      {
1248        return mainGroup.getInternalMemberList().lastIndexOf(item);
1249      }
1250    
1251      //----------------------------------------------------------------------------
1252      // Groups
1253      //----------------------------------------------------------------------------
1254    
1255      /**
1256       * Returns the group at the given position.
1257       * 
1258       * @param index the position of the group to return.
1259       * 
1260       * @return the group at {@code index}.
1261       */
1262      public G getGroup(int index)
1263      {
1264        return groups.get(index);
1265      }
1266      
1267      /**
1268       * Returns a group with the given name, or <i>null</i> if this catalog
1269       * has no such group.
1270       * 
1271       * @param groupName
1272       *   the name of group potentially in this catalog.
1273       *   
1274       * @return
1275       *   a group with the given name, or <i>null</i> if this catalog
1276       *   has no such group.
1277       */
1278      public G getGroup(String groupName)
1279      {
1280        for (G group : groups)
1281          if (group.getName().equals(groupName))
1282            return group;
1283        
1284        return null;
1285      }
1286      
1287      /**
1288       * Returns a list of this catalog's groups.
1289       * <p>
1290       * Note that the returned list is <i>not</i> held internally by this catalog.
1291       * This means that any changes made to the returned list will <i>not</i>
1292       * be reflected in this object.  The elements of the list, though, are the
1293       * actual groups of this catalog, so changes made to them will be reflected
1294       * in this catalog.</p>
1295       * 
1296       * @return a list of this catalog's groups.  The returned value is guaranteed
1297       *         to be non-null, but it may be an empty list.
1298       */
1299      public List<G> getGroups()
1300      {
1301        return new ArrayList<G>(groups);
1302      }
1303    
1304      /**
1305       * Returns a list of groups that contain {@code item}.
1306       * @param item an item of this catalog that may be in one or more groups.
1307       * @return a list of groups that contain {@code item}.
1308       */
1309      public List<G> getGroupsThatContain(I item)
1310      {
1311        List<G> result = new ArrayList<G>();
1312        
1313        for (G group : groups)
1314          if (group.getInternalMemberList().contains(item))
1315            result.add(group);
1316        
1317        return result;
1318      }
1319      
1320      /**
1321       * Returns <i>true</i> if this catalog contains a group with the
1322       * given {@code name}.
1323       * @param name potential name of a group contained by this catalog.
1324       * @return <i>true</i> if this catalog has a group named {@code name}.
1325       */
1326      public boolean containsGroupNamed(String name)
1327      {
1328        //See if name is the special main-group name
1329        boolean contains = getReservedGroupName().equals(name);
1330        
1331        //If not, see if any of the regular groups have that name
1332        if (!contains)
1333        {
1334          for (G group : groups)
1335          {
1336            if (group.getName().equals(name))
1337            {
1338              contains = true;
1339              break;
1340            }
1341          }
1342        }
1343        
1344        return contains;
1345      }
1346    
1347      /**
1348       * Returns the internal group list.
1349       * This method is meant solely for use by frameworks (such
1350       * as JAXB or Hibernate) that rely on getX/setX pairs for
1351       * persisted properties.  It is {@code protected}, as opposed to
1352       * {@code private}, so that subclasses can create and annotate
1353       * methods that call this one.
1354       */
1355      protected List<G> getInternalGroupList()
1356      {
1357        return groups;
1358      }
1359      
1360      /**
1361       * Replaces the internal group list with {@code replacementList}.
1362       * This method is meant solely for use by frameworks (such
1363       * as JAXB or Hibernate) that rely on getX/setX pairs for
1364       * persisted properties.  It is {@code protected}, as opposed to
1365       * {@code private}, so that subclasses can create and annotate
1366       * methods that call this one.
1367       */
1368      @SuppressWarnings("unchecked")
1369      protected void setInternalGroupList(List<G> replacementList)
1370      {
1371        groups = (replacementList == null) ? new ArrayList<G>()
1372                                           : replacementList;
1373        for (G group : groups)
1374          group.simplySetCatalog((C)this);
1375      }
1376    
1377      /**
1378       * Returns the name that this catalog uses for its special group.
1379       * The special group is the internal group that holds all the items
1380       * of this catalog.  No other group may use this name.
1381       * 
1382       * @return the name that this catalog uses for its special group.
1383       */
1384      public String getReservedGroupName()
1385      {
1386        return reservedGroupName;
1387      }
1388      
1389      /**
1390       * Sets the name that this catalog will treat as a reserved name.
1391       * The reserved name is used for the special group that holds all
1392       * the items in this catalog.
1393       * 
1394       * @param reservedName the name this catalog will use for its special
1395       *                     group.  If this value is <i>null</i>, this catalog
1396       *                     will use a default name in its place.
1397       */
1398      protected void setReservedGroupName(String reservedName)
1399      {
1400        reservedGroupName = (reservedName == null) ? DEFAULT_RESERVED_GROUP_NAME
1401                                                   : reservedName;
1402      }
1403      
1404      //============================================================================
1405      // LISTENERS
1406      //============================================================================
1407      //----------------------------------------------------------------------------
1408      // Adding & Removing
1409      //----------------------------------------------------------------------------
1410    
1411      /**
1412       * Adds {@code newListener} to this catalog list.
1413       * <p>
1414       * The listener will be informed whenever:
1415       * <ul>
1416       *   <li>A new item or group is added to this catalog</li>
1417       *   <li>A current item or group is removed from this catalog</li>
1418       *   <li>A current item or group is moved from one position
1419       *       in this catalog to another</li>
1420       * </ul></p>
1421       * 
1422       * @param newListener the listener to be added to this catalog's list.
1423       */
1424      public void addListener(CatalogListener<I,G,C> newListener)
1425      {
1426        if (newListener != null)
1427          listeners.add(newListener);
1428      }
1429    
1430      /**
1431       * Removes {@code listener} from this catalog's list.
1432       * @param listener the listener to be removed from this catalog's list.
1433       */
1434      public void removeListener(CatalogListener<I,G,C> listener)
1435      {
1436        listeners.remove(listener);
1437      }
1438    
1439      /** Removes all listeners from this catalog. */
1440      public void removeAllListeners()
1441      {
1442        listeners.clear();
1443      }
1444    
1445      //----------------------------------------------------------------------------
1446      // Listening to Main Group
1447      //----------------------------------------------------------------------------
1448    
1449      @SuppressWarnings("unchecked")
1450      public void memberAdded(G group, I newMember, int index)
1451      {
1452        //Catalog is allowed to listen only to its own mainGroup
1453        if ((group == mainGroup) && !suppressNotification)
1454        {
1455          for (CatalogListener<I,G,C> listener : listeners)
1456            listener.itemAdded((C)this, newMember, index);
1457        }
1458      }
1459      
1460      @SuppressWarnings("unchecked")
1461      public void memberRemoved(G group, I formerMember, int index)
1462      {
1463        //Catalog is allowed to listen only to its own mainGroup
1464        if ((group == mainGroup) && !suppressNotification)
1465        {
1466          for (CatalogListener<I,G,C> listener : listeners)
1467            listener.itemRemoved((C)this, formerMember, index);
1468        }
1469      }
1470      
1471      @SuppressWarnings("unchecked")
1472      public void memberMoved(G group, I member, int fromIndex, int toIndex)
1473      {
1474        //Catalog is allowed to listen only to its own mainGroup
1475        if ((group == mainGroup) && !suppressNotification)
1476        {
1477          for (CatalogListener<I,G,C> listener : listeners)
1478            listener.itemMoved((C)this, member, fromIndex, toIndex);
1479        }
1480      }
1481      
1482      @SuppressWarnings("unchecked")
1483      public void memberReplaced(G group, I formerMember, I newMember, int index)
1484      {
1485        //Catalog is allowed to listen only to its own mainGroup
1486        if ((group == mainGroup) && !suppressNotification)
1487        {
1488          for (CatalogListener<I,G,C> listener : listeners)
1489            listener.itemReplaced((C)this, formerMember, newMember, index);
1490        }
1491      }
1492      
1493      @SuppressWarnings("unchecked")
1494      public void membersSorted(G group)
1495      {
1496        //Catalog is allowed to listen only to its own mainGroup
1497        if ((group == mainGroup) && !suppressNotification)
1498        {
1499          for (CatalogListener<I,G,C> listener : listeners)
1500            listener.itemsSorted((C)this);
1501        }
1502      }
1503    
1504      //============================================================================
1505      // TEXT
1506      //============================================================================
1507      
1508      /**
1509       * Returns a text representation of this catalog.
1510       * @return a text representation of this catalog.
1511       */
1512      public String toString()
1513      {
1514        StringBuilder buff = new StringBuilder();
1515          
1516        buff.append("name=").append(name);
1517        buff.append(", id=").append(id);
1518        buff.append(", items=").append(size());
1519        buff.append(", groups=").append(groups.size());
1520          
1521        return buff.toString();
1522      }
1523    
1524      /**
1525       * Returns an XML representation of this catalog.
1526       * @return an XML representation of this catalog.
1527       * @throws JAXBException if anything goes wrong during the conversion to XML.
1528       */
1529      public String toXml() throws JAXBException
1530      {
1531        return JaxbUtility.getSharedInstance().objectToXmlString(this);
1532      }
1533      
1534      /**
1535       * Writes an XML representation of this catalog to {@code writer}.
1536       * @param writer the device to which XML is written.
1537       * @throws JAXBException if anything goes wrong during the conversion to XML.
1538       */
1539      public void writeAsXmlTo(Writer writer) throws JAXBException
1540      {
1541        JaxbUtility.getSharedInstance().writeObjectAsXmlTo(writer, this, null);
1542      }
1543    
1544      //============================================================================
1545      // TRANSFORMATION TO OTHER FORMS
1546      //============================================================================
1547    
1548      /**
1549       * Creates and returns a group that has the same items in the same order
1550       * as this catalog.
1551       * @return a group representation of this catalog.
1552       */
1553      public G toGroup()
1554      {
1555        G group = createGroup();
1556        
1557        //Replaced deactivated line below w/ code that uses the internal
1558        //member list directly.  Did this because group.addAll will
1559        //weed out duplicate entries, but if the mainGroup actually
1560        //has duplicate entries, we need to expose that.
1561      //group.addAll(mainGroup.getAll());
1562        group.getInternalMemberList().addAll(mainGroup.getInternalMemberList());
1563        
1564        group.setName(getName());
1565        
1566        return group;
1567      }
1568      
1569      //============================================================================
1570      // 
1571      //============================================================================
1572      
1573      /**
1574       *  Returns a catalog that is a copy of this one.
1575       *  <p>
1576       *  The returned catalog has items and groups with the same properties
1577       *  as those of this catalog, but those items and groups are new and
1578       *  distinct from those held herein.</p>
1579       *  <p>
1580       *  If anything goes wrong during the cloning procedure,
1581       *  a {@code RuntimeException} will be thrown.</p>
1582       */
1583      @SuppressWarnings("unchecked")
1584      public C clone()
1585      {
1586        C clone = null;
1587    
1588        try
1589        {
1590          //This line takes care of the primitive fields properly
1591          //(This is the line that requires suppression of unchecked conversions.)
1592          clone = (C)super.clone();
1593          
1594          //We do NOT want the clone to have the same ID as the original.
1595          //The ID is here for the persistence layer; it is in charge of
1596          //setting IDs.  To help it, we put the clone's ID in the uninitialized
1597          //state.
1598          clone.id = getIdOfUnidentified();
1599          
1600          //Clear the listeners.
1601          clone.listeners = new CopyOnWriteArraySet<CatalogListener<I,G,C>>();
1602          
1603          //Create a new main group and clone items individually.
1604          clone.mainGroup = clone.createMainGroup();
1605          for (I thisItem : this.mainGroup.getInternalMemberList())
1606            clone.mainGroup.add(thisItem.clone());
1607          
1608          //Copies the notes and any properties created by subclasses.
1609          clone.mainGroup.copyPropertiesFrom(this.mainGroup);
1610          
1611          //Create a new list of groups and add clones of groups.
1612          clone.groups = new ArrayList<G>();
1613          for (G thisGroup : this.groups)
1614            clone.addGroup(thisGroup.clone());
1615        }
1616        catch (Exception ex)
1617        {
1618          throw new RuntimeException(ex);
1619        }
1620        
1621        return clone;
1622      }
1623    
1624      /** Returns <i>true</i> if {@code o} is equal to this catalog. */
1625      @SuppressWarnings("unchecked")
1626      @Override
1627      public boolean equals(Object o)
1628      {
1629        //Quick exit if o is null
1630        if (o == null)
1631          return false;
1632        
1633        //Quick exit if o is this
1634        if (o == this)
1635          return true;
1636        
1637        //Quick exit if classes are different
1638        if (!o.getClass().equals(this.getClass()))
1639          return false;
1640        
1641        Catalog other = (Catalog)o;
1642        
1643        //NOTE: Absence of ID and reservedGroupName is intentional
1644        
1645        //Items & groups must be equal and in same order
1646        return this.name.equals(other.name) &&
1647               this.mainGroup.equals(other.mainGroup) &&
1648               this.groups.equals(other.groups);
1649      }
1650    
1651      /** Returns a hash code value for this catalog. */
1652      public int hashCode()
1653      {
1654        //Taken from the Effective Java book by Joshua Bloch.
1655        //The constants 17 & 37 are arbitrary & carry no meaning.
1656        int result = 17;
1657        
1658        //NOTE: Keep this method in synch w/ equals
1659        
1660        result = 37 * result + name.hashCode();
1661        result = 37 * result + mainGroup.hashCode();
1662        result = 37 * result + groups.hashCode();
1663        
1664        return result;
1665      }
1666    }