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    import javax.xml.bind.annotation.XmlElement;
015    import javax.xml.bind.annotation.XmlElementWrapper;
016    import javax.xml.bind.annotation.XmlRootElement;
017    import javax.xml.bind.annotation.XmlTransient;
018    
019    import edu.nrao.sss.util.JaxbUtility;
020    
021    /**
022     * A grouping of {@link CatalogItem catalog items}.
023     * A {@code CatalogItemGroup} is normally contained in a
024     * {@link Catalog} and is a way of collecting together
025     * items that have similar traits.
026     * <p>
027     * <b>Version Info:</b>
028     * <table style="margin-left:2em">
029     *   <tr><td>$Revision: 2232 $</td></tr>
030     *   <tr><td>$Date: 2009-04-23 13:38:46 -0600 (Thu, 23 Apr 2009) $</td></tr>
031     *   <tr><td>$Author: dharland $ (last person to modify)</td></tr>
032     * </table></p>
033     * 
034     * @author David M. Harland
035     * @since 2006-10-23
036     */
037    @XmlRootElement
038    public class CatalogItemGroup<I extends CatalogItem<I>,
039                                  G extends CatalogItemGroup<I,G,C>,
040                                  C extends Catalog<I,G,C>>
041      implements Cloneable
042    {
043      private static final String INIT_NAME = "[New Group]";
044      //private static final String NO_NAME   = "[Unamed Group]";
045    
046      //============================================================================
047      // INSTANCE VARIABLES AND CONSTRUCTORS
048      //============================================================================
049    
050      //IDENTIFICATION
051      private long   id;
052      private String name;
053    
054      //CONTAINER & CONTENTS
055      private List<I> items;
056      private C       catalog;
057    
058      //OTHER PROPERTIES
059      private boolean      nameIsLocked;
060      private List<String> notes;
061      
062      //NONPERSISTED PROPERTIES
063      private Set<CatalogItemGroupListener<I,G,C>> listeners;
064      private boolean                              suppressNotification;
065      
066      /**
067       * Creates a new group with a default name and that belongs to no catalog.
068       */
069      public CatalogItemGroup()
070      {
071        this(null, INIT_NAME);
072      }
073    
074      /**
075       * Creates a new group that belongs to {@code container}.
076       * 
077       * @param container the name of the one catalog to which this group belongs.
078       *                  This value may be <i>null</i>.
079       *                  
080       * @param nameOfGroup the name of this group.  If this value is <i>null</i>,
081       *                    a non-null default name will be used.
082       */
083      @SuppressWarnings("unchecked")
084      public CatalogItemGroup(C container, String nameOfGroup)
085      {
086        id                   = getIdOfUnidentified();
087        name                 = (nameOfGroup == null) ? INIT_NAME : nameOfGroup;
088        nameIsLocked         = false;
089        items                = new ArrayList<I>();
090        notes                = new ArrayList<String>();
091        suppressNotification = false;
092    
093        listeners = new CopyOnWriteArraySet<CatalogItemGroupListener<I,G,C>>();
094        
095        if (container != null)
096        {
097          container.addGroup((G)this);  //Sets this.catalog
098        }
099        else
100        {
101          catalog = null;
102        }
103      }
104    
105      /**
106       * Special constructor used only by Catalog for constructing its main group.
107       */
108      protected CatalogItemGroup(C container)
109      {
110        id                   = getIdOfUnidentified();
111        name                 = container.getReservedGroupName();
112        nameIsLocked         = true;
113        items                = new ArrayList<I>();
114        notes                = new ArrayList<String>();
115        suppressNotification = false;
116        catalog              = container;
117    
118        listeners = new CopyOnWriteArraySet<CatalogItemGroupListener<I,G,C>>();
119      }
120    
121      //============================================================================
122      // IDENTIFICATION
123      //============================================================================
124    
125      /**
126       * Sets the ID of this group.  If {@code id} is <i>null</i>, this group's ID
127       * ID will be set to an <i>unidentified</i> ID.
128       * 
129       * @param id a new ID for this group.
130       */
131      protected void setId(Long id)
132      {
133        this.id = (id == null) ? getIdOfUnidentified() : id;
134      }
135      
136      /**
137       * Returns a value that represents an undefined ID.
138       * Subclasses may override this method to suit their needs.
139       */
140      protected long getIdOfUnidentified()
141      {
142        return -1L;
143      }
144      
145      /**
146       * Returns this group's ID.
147       * @return this group's ID.
148       */
149      @XmlAttribute
150      public Long getId()
151      {
152        return id;
153      }
154    
155      /**
156       * Attempts to change the name of this group to {@code newName}.
157       * <p>
158       * This method exists for the use of frameworks that rely on 
159       * java-bean naming conventions and set-methods that are of
160       * type void.  The preferred method for all other clients is
161       * {@link #setNameAndConfirm(String)}, which will let the caller
162       * know whether or not the change of name was successful.
163       * If this method fails to change this group's name it does so
164       * <em>silently</em>.</p>
165       * 
166       * @param newName the new name for this group.
167       * 
168       * @see #setNameAndConfirm(String)
169       */
170      public void setName(String newName)
171      {
172        setNameAndConfirm(newName);
173      }
174    
175      /**
176       * Attempts to set the name of this group and returns <i>true</i> if
177       * this group's name was changed.
178       * 
179       * @param newName the new name for this group.
180       *
181       * @return <i>true</i> if the name of this group was changed to
182       *         {@code newName}. Reasons for a return value of <i>false</i>:
183       *         <ol>
184       *           <li>The name of this group is locked.  (This is controlled by
185       *               the containing catalog, if this group is a member of a
186       *               catalog.)</li>
187       *           <li>{@code newName} is <i>null</i>.</li>
188       *           <li>{@code newName} is the empty string <tt>""</tt>.</li>
189       *           <li>This group's name is already equal to {@code newName}.</li>
190       *           <li>This group belongs to a catalog and another group in the
191       *               catalog has a name of {@code newName}.  (The catalog is
192       *               now enforcing uniqueness of its groups' names.)</li>
193       *         </ol>
194       */
195      public boolean setNameAndConfirm(String newName)
196      {
197        boolean nameChanged;
198        
199        //Will not change name if name is locked, if it's null,
200        //or if it's same as current name
201        if (nameIsLocked || (newName == null) || newName.equals(name) || newName.equals(""))
202        {
203          nameChanged = false;
204        }
205        else //name is NOT locked, newName is NOT null, AND newName is truly new
206        {
207          if (catalog == null)
208          {
209            name = newName;
210            nameChanged = true;
211          }
212          else //group belongs to catalog
213          {
214            if (catalog.containsGroupNamed(newName))
215            {
216              nameChanged = false;
217            }
218            else //catalog has no group w/ this name
219            {
220              name = newName;
221              nameChanged = true;
222            }
223          }
224        }
225        
226        return nameChanged;
227      }
228      
229      /**
230       * If necessary, this method gives this group a new name of the form
231       * <tt><i>currentName</i> (Copy <i>#</i>)</tt>.  This is necessary
232       * only if there is another group in the client catalog with the
233       * same name, or if this group is using the reserved name.
234       * The main purpose of this method is to allow a user to add
235       * a group to a catalog that already has a group with the same
236       * name as this one.
237       * 
238       * @param client the catalog that is making the request to rename.
239       */
240      void adjustNameForCatalog(C client)
241      {
242        StringBuilder buff = new StringBuilder(this.name);
243    
244        //See if this group already has the " (Copy #)" suffix.
245        //If so, delete suffix from buffer.
246        final String suffix = " (Copy ";
247        int suffixIndex = buff.indexOf(suffix);
248        if (suffixIndex >= 0)
249          buff.delete(suffixIndex, buff.length());
250    
251        //baseName is everything in name before " (Copy #)"
252        String baseName = buff.toString();
253        
254        //copyBase is baseName and " (Copy "
255        String copyBase = baseName + suffix;
256        
257        //Search all groups (even this one, if part of client) looking for
258        //names that start with copyBase and names that are exactly baseName.
259        int copyNumber = 0;
260        for (G group : client.getGroups())
261          if (group.getName().equals(baseName) ||
262              group.getName().startsWith(copyBase))
263            copyNumber++;
264    
265        //If someone has cloned the main group it's possible that this group
266        //has the reserved name.  We'll have to change that name, and allow
267        //clients to do so, too, by unlocking the name.
268        if (baseName.equals(client.getReservedGroupName()))
269          this.nameIsLocked = false;
270    
271        if (copyNumber > 0)
272        {
273          buff.append(suffix).append(copyNumber).append(')');
274          this.name = buff.toString();
275        }
276      }
277    
278      /**
279       * Returns the name of this group.
280       * @return the name of this group.
281       */
282      public String getName()
283      {
284        return name;
285      }
286    
287      /** Restricts the ability to change this group's name. */
288      void setNameIsLocked(boolean isLocked)
289      {
290        nameIsLocked = isLocked;
291      }
292      
293      /**
294       * Returns <i>true</i> if the ability to change this group's name is
295       * restricted.
296       */
297      protected boolean getNameIsLocked()
298      {
299        return nameIsLocked;
300      }
301      
302      //============================================================================
303      // OTHER PROPERTIES
304      //============================================================================
305    
306      /**
307       * Returns a list of notes about this group.
308       * Each note is free-form text with no particular structure.
309       * <p>
310       * This method returns the list actually held by this
311       * {@code CatalogItemGroup}, so
312       * any list manipulations may be performed by first fetching the list and
313       * then operating on it.</p>
314       * 
315       * @return a list of notes about this catalog.
316       */
317      @XmlElementWrapper
318      @XmlElement(name="note")
319      public List<String> getNotes()
320      {
321        return notes;
322      }
323      
324      /** This is here for mechanisms that need setX/getX pairs, such as JAXB. */
325      void setNotes(List<String> replacementList)
326      {
327        notes = (replacementList == null) ? new ArrayList<String>()
328                                          : replacementList;
329      }
330    
331      //============================================================================
332      // CONTAINER
333      //============================================================================
334      
335      /**
336       * Sets the catalog to which this group belongs.
337       * <p>
338       * If this group is currently contained in a catalog
339       * that is not the same as the {@code newCatalog} parameter,
340       * the current catalog will be told to remove this group
341       * from its collection of groups.  If {@code newCatalog}
342       * is not <i>null</i>, it will be told to add this group
343       * to its collection.  Finally, this group's catalog
344       * will be set to {@code newCatalog}, even if it is <i>null</i>.</p>
345       * <p>
346       * The current catalog has the right to deny movement of this group
347       * to a new catalog.  If the request is denied, this group will
348       * remain with its current catalog.</p>
349       * <p>
350       * Passing this method a {@code newCatalog} of <i>null</i> has the
351       * effect of disconnecting this group from any catalog.</p>
352       * 
353       * @param newCatalog the catalog to which this group belongs.
354       * 
355       * @return <i>true</i> if {@code newCatalog} is now the catalog for this
356       *         group.  Note that this means if {@code newCatalog} is already
357       *         the catalog of this group, the return value is <i>true</i>. 
358       */
359      @SuppressWarnings("unchecked")
360      public boolean setCatalog(C newCatalog)
361      {
362        C formerCatalog = this.catalog;
363        
364        //Quick exit if this group may not be removed from its catalog
365        if ((formerCatalog != null) && !formerCatalog.allowsRemovalOf((G)this))
366          return false;
367        
368        //Quick exit if this group already belongs to newCatalog.
369        //(Intentional use of "==" here.)
370        if (formerCatalog == newCatalog)
371          return true;  //"true" that newCatalog is this group's catalog
372        
373        boolean catalogIsNewCatalog = false;
374        
375        //If newCatalog is NOT null, it will be in charge of telling
376        //formerCatalog about its loss of this group.
377        if (newCatalog != null)
378        {
379          catalogIsNewCatalog = newCatalog.addGroup((G)this);
380        }
381        //Otherwise, we must tell the former catalog ourselves
382        else //newCatalog == null
383        {
384          if (formerCatalog != null)
385            catalogIsNewCatalog = (formerCatalog.removeGroup((G)this) != null);
386        }
387        
388        //Make the change, or rollback to pre-call state
389        catalog = catalogIsNewCatalog ? newCatalog : formerCatalog;
390        
391        return catalogIsNewCatalog;
392      }
393    
394      /**
395       * Sets this group's catalog to {@code newCatalog} without
396       * contacting either the former or new catalog.  This method is
397       * used only by the Catalog class.
398       */
399      protected void simplySetCatalog(C newCatalog)
400      {
401        this.catalog = newCatalog;
402      }
403      
404      /**
405       * Returns the catalog to which this group belongs, if any.
406       * <p>
407       * This group may be one of several that belong to
408       * the same catalog.  If this group belongs to
409       * no catalog, the value returned is <i>null</i>.</p>
410       * 
411       * @return the catalog to which this group belongs, if any.
412       * 
413       * @see #hasCatalog()
414       */
415      @XmlTransient
416      public C getCatalog()
417      {
418        return catalog;
419      }
420      
421      /**
422       * Returns <i>true</i> if this group has a non-null catalog.
423       * <p>
424       * Catalog item groups should normally be contained within, and
425       * therefore have a non-null, catalog.  However, there are some
426       * situations where this method will return <i>false</i>:
427       * <ol>
428       *   <li>This group has just been created and its catalog
429       *       has not yet been set.</li>
430       *   <li>A client removed this group from its catalog and 
431       *       did not place it in a new catalog.</li>
432       *   <li>A client explicitly set this group's catalog to
433       *       <i>null</i>.</li>
434       * </ol></p>
435       * 
436       * @return <i>true</i> if this group has a non-null catalog.
437       *         Therefore a return value of <i>true</i> means that 
438       *         you can call {@link #getCatalog()} and know that
439       *         it will return a non-null object. 
440       */
441      public boolean hasCatalog()
442      {
443        return catalog != null;
444      }
445      
446      //============================================================================
447      // ADDING MEMBERS
448      //============================================================================
449    
450      /**
451       * Adds a new member to this group.
452       * <p>
453       * The new member is added to this group only if it is not <i>null</i>, and
454       * this group currently contains no member that is equal to it.
455       * (To bypass the equality restriction, use
456       * {@link #addEvenIfEqualToCurrentMember(CatalogItem)}). If this
457       * group belongs to a catalog, the catalog is also informed about the new
458       * member.</p>
459       * <p>
460       * If this group is part of a catalog, and if the catalog
461       * has an item that is equal to {@code newMember}, then that equivalent
462       * item is used in place of {@code newMember} as the new member of this
463       * group.  This ensures that an item copied from one group of a catalog
464       * to another group of the same catalog is a single item that is a
465       * member of the two groups.  The next section shows what happens to
466       * this group, its catalog (if it has one), and the value returned by
467       * this method in a few situations.</p>
468       * <p>
469       * <b><u>A. This Group Does <i>Not</i> Belong to a Catalog</u></b></p>
470       * <ol>
471       *   <li><tt>newMember</tt> is <i>null</i> or its addition to this group
472       *       fails<sup>1</sup>.<br/>
473       *       Nothing is added to this group and the value returned is <i>null</i>.
474       *       </li><br/>
475       *   <li>This group contains no members that are equal to <tt>newMember</tt>.<br/>
476       *       <tt>newMember</tt> is added to this group and returned.
477       *       </li><br/>
478       *   <li>This group contains a member that is equal to <tt>newMember</tt>.<br/>
479       *       The member equal to <tt>newMember</tt> that is already present in
480       *       this group is returned.  Nothing is added to this group.
481       *       </li>
482       * </ol>
483       * <p><b><u>B. This Group <i>Does</i> Belong to a Catalog</u></b></p>
484       * <ol>
485       *   <li><tt>newMember</tt> is <i>null</i> or its addition to this group
486       *       or its catalog fails<sup>1</sup>.<br/>
487       *       Nothing is added to this group or its catalog and the value
488       *       returned is <i>null</i>.
489       *       </li><br/>
490       *   <li>This group and its catalog contain no members that are equal to
491       *       <tt>newMember</tt>.<br/>
492       *       <tt>newMember</tt> is added to this group and to its catalog,
493       *       and is returned.
494       *       </li><br/>
495       *   <li>This group contains no members that are equal to <tt>newMember</tt>,
496       *       but its catalog does.<br/>
497       *       The member equal to <tt>newMember</tt> that is already present in
498       *       the catalog is added to this group and returned.  Nothing is added
499       *       to the catalog.
500       *       </li><br/>
501       *   <li>This group, and therefore its catalog, contains a member that
502       *       is equal to <tt>newMember</tt>.<br/>
503       *       The member equal to <tt>newMember</tt> that is already present in
504       *       this group and its is returned.
505       *       Nothing is added to this group or its catalog.
506       *       </li>
507       * </ol>
508       * <p>
509       * In general, the return value is the {@code newMember} parameter.  However,
510       * this is not always true when this group is part of a catalog.  See the
511       * description of the returned value, below.</p>
512       * <p>
513       * <sup>1</sup><i>With the exception of a programming error within this
514       * class or the Catalog class, this is not expected to happen.</i></p>
515       * 
516       * @param newMember
517       *   a prospective new member of this group.
518       * 
519       * @return
520       *   <i>null</i>, <tt>newMember</tt>, or an item that is equal to
521       *   <tt>newMember</tt>, as outlined in the situations above.
522       * 
523       * @see #addEvenIfEqualToCurrentMember(CatalogItem) 
524       */
525      public I add(I newMember)
526      {
527        I added = null;
528        
529        if (newMember != null)
530        {
531          int position = items.indexOf(newMember);
532          
533          added = position < 0 ? addNewMember(size(), newMember) : items.get(position);
534        }
535        
536        return added;
537      }
538      
539      /**
540       * Adds a new member to this group at the specified position.
541       * <p>
542       * For a full description of the behavior of this method and its return
543       * values, see {@link #add(CatalogItem)}.</p>
544       * 
545       * @param index the position at which to insert the new member.
546       * 
547       * @param newMember a prospective new member of this group.
548       * 
549       * @return the member that was actually added to, or was already a part of,
550       *         this group.
551       *         
552       * @see #addEvenIfEqualToCurrentMember(int, CatalogItem)
553       */
554      public I add(int index, I newMember)
555      {
556        I added = null;
557        
558        if (newMember != null)
559        {
560          int position = items.indexOf(newMember);
561          
562          if (position == -1) //this is, indeed, a new member of this group
563          {
564            added = addNewMember(index, newMember);
565          }
566          else //already have equal member in this group
567          {
568            added = items.get(position);
569            move(position, index);
570          }
571        }
572    
573        return added;
574      }
575    
576      /**
577       * @param newMember
578       *
579      public void addByForcingUniqueness(NameableCatalogItem<I> newMember)
580      {
581        I added = null;
582        
583        if (newMember != null)
584        {
585          int position = items.indexOf(newMember);
586          
587          //Not found in group, but might be an equal one in catalog
588          if (position == -1)
589          {
590            see if equal member is in catalog
591              if yes, change name
592              else  vanilla add
593          }
594          else //already have equal member in this group
595          {
596            need a unique name
597            if catalog != null, need unique name catalog-wied
598          }
599        }
600        
601        return added;
602      }
603      */
604      //Adds new member w/out any checks.  Don't call w/ null newMember.
605      @SuppressWarnings("unchecked")
606      private I addNewMember(int index, I newMember)
607      {
608        //Update catalog or use it to get reference to equal member
609        if (catalog != null)
610        {
611          //If the catalog has a resource that is EQUAL to newMember,
612          //replace the member sent to us with the one from the catalog.
613          if (catalog.contains(newMember))
614          {
615            newMember = catalog.getItems().get(catalog.indexOf(newMember));
616          }
617          else //!catalog.contains(newMember), so tell catalog about new item
618          {
619            catalog.addItem(newMember, (G)this);
620          }
621        }
622    
623        //Try to add the newMember to this group.
624        items.add(index, newMember);
625        tellListenersAboutNewMember(newMember, index);
626    
627        //Return the parameter, the equivalent item from the catalog, or null
628        return newMember;
629      }
630      
631      /**
632       * Adds the collection of {@code newMembers} to this group.
633       * Only non-null candidates that are not already members of
634       * this group are added.
635       * 
636       * @param newMembers a collection of prospective new members.
637       * 
638       * @return a collection of the members actually added to this group.
639       * 
640       * @see #add(CatalogItem)
641       */
642      public Collection<I> addAll(Collection<? extends I> newMembers)
643      {
644        Collection<I> addedMembers = new ArrayList<I>();
645        
646        if (newMembers != null)
647        {
648          for (I member : newMembers)
649            addedMembers.add(this.add(member));
650        }
651        
652        return addedMembers;
653      }
654      
655      /**
656       * Adds the collection of {@code newMembers} to this group at the
657       * specified position.
658       * Only non-null candidates that are not already members of
659       * this group are added.
660       * 
661       * @param index the position at which to insert the first new member.
662       *              Subsequent new members are added at successive positions.
663       * 
664       * @param newMembers a collection of prospective new members.
665       * 
666       * @return a collection of the members actually added to this group.
667       * 
668       * @see #add(CatalogItem)
669       */
670      public Collection<I> addAll(int index, Collection<? extends I> newMembers)
671      {
672        Collection<I> addedMembers = new ArrayList<I>();
673        
674        if (newMembers != null)
675        {
676          for (I member : newMembers)
677            addedMembers.add(this.add(index++, member));
678        }
679        
680        return addedMembers;
681      }
682    
683      /**
684       * Adds {@code newMember} even if this group already has a member to which it
685       * is equal.  Contrast this with {@link #add(CatalogItem)}.
686       * <p>
687       * The only times {@code newMember} is <i>not</i> added to this group are
688       * when it is <i>null</i> and when a reference to it is already contained
689       * in this group.</p>
690       *
691       * @param newMember a potential new member for this group.
692       * 
693       * @return {@code newMember}.
694       */
695      public I addEvenIfEqualToCurrentMember(I newMember)
696      {
697        return addEvenIfEqualToCurrentMember(size(), newMember);
698      }
699    
700      /**
701       * Adds {@code newMember} at the specified position
702       * even if this group already has a member to which it
703       * is equal.  Contrast this with {@link #add(int, CatalogItem)}.
704       * <p>
705       * The only times {@code newMember} is <i>not</i> added to this group are
706       * when it is <i>null</i> and when a reference to it is already contained
707       * in this group.</p>
708       * 
709       * @param index the position at which to add {@code newMember}.
710       * 
711       * @param newMember a potential new member for this group.
712       * 
713       * @return {@code newMember}.
714       */
715      @SuppressWarnings("unchecked")
716      public I addEvenIfEqualToCurrentMember(int index, I newMember)
717      {
718        //Quick exit if newMember is null
719        if (newMember == null)
720          return null;
721        
722        //Quick exit if newMember itself is already present
723        for (I currentMember : items)
724          if (currentMember == newMember)
725            return newMember;
726        
727        //We now know that newMember is not null & not already in this group.
728        //Bypass the normal members.contains(newMember) logic and force it in.
729        items.add(index, newMember);
730        tellListenersAboutNewMember(newMember, index);
731    
732        //The catalog must also be told to accept this member, even if it already
733        //has an equal-but-not-identical item.  This group and the main catalog
734        //must each hold a reference to newMember.
735        if (catalog != null)
736          catalog.addItemEvenIfEqualToCurrentItem(newMember, (G)this);
737        
738        return newMember;
739      }
740      
741      @SuppressWarnings("unchecked")
742      protected void tellListenersAboutNewMember(I newMember, int index)
743      {
744        if (!suppressNotification)
745          for (CatalogItemGroupListener<I,G,C> listener : listeners)
746            listener.memberAdded((G)this, newMember, index);
747      }
748      
749      //============================================================================
750      // REMOVING MEMBERS
751      //============================================================================
752    
753      /**
754       * Removes {@code member} from this group, if present.
755       * <p>
756       * The returned element is either <i>null</i> (if this group had no member
757       * equal to {@code member}) or is the removed member, which is the first
758       * member of this group that is equal to {@code member}.</p>
759       * 
760       * @param member the member that is to be removed from this group.
761       * 
762       * @return the removed element, or <i>null</i> if no element was removed.
763       */
764      public I remove(I member)
765      {
766        int index = indexOf(member);
767        
768        return index < 0 ? null : remove(index);
769      }
770      
771      /**
772       * Removes the member at {@code index} from this group.
773       * 
774       * @param index the index of the member to be removed.
775       * 
776       * @return the member formerly at {@code index}.
777       * 
778       * @throws IndexOutOfBoundsException  if the index is out of range
779       *           (index < 0 || index >= size()).
780       */
781       @SuppressWarnings("unchecked")
782      public I remove(int index)
783      {
784        I formerMember = items.remove(index);
785        tellListenersAboutFormerMember(formerMember, index);
786        
787        if (formerMember != null)
788        {
789          //Let catalog know about removal. It will decide whether or not to
790          //remove member entirely from itself.
791          if (catalog != null)
792            catalog.removeItem(formerMember, (G)this);
793        }
794        
795        return formerMember;
796      }
797      
798      /**
799       * Removes all elements of {@code unwantedMembers} that are contained in this
800       * group.
801       *  
802       * @param unwantedMembers the members that are to be removed from this group.
803       * 
804       * @return the former members that were removed from this group as a result
805       *         of the call.
806       */
807      public Collection<I> removeAll(Collection<? extends I> unwantedMembers)
808      {
809        Collection<I> formerMembers = new ArrayList<I>();
810        
811        if (unwantedMembers != null)
812        {
813          for (I member : unwantedMembers)
814          {
815            I formerMember = remove(member);
816            if (formerMember != null)
817              formerMembers.add(formerMember);
818          }
819        }
820        
821        return formerMembers;
822      }
823      
824      /** Removes all members from this group. */
825      @SuppressWarnings("unchecked")
826      public void clear()
827      {
828        //We clone the list of members because we will do things in this order:
829        //1. Remove the members from this group.
830        //2. Notify listeners about the removals.
831        //3. Tell the catalog about the removals, in case it needs to do anything.
832        List<I> clonedMemberList = null;
833        synchronized(this) {
834          clonedMemberList = (List<I>)((ArrayList<I>)items).clone();
835        }
836        
837        //Remove this group's members.  Do not use items for remainder of method.
838        items.clear();
839        
840        //Notify listeners.  Traversing the member list backwards ensures
841        //that the index sent to listener matches current state of list.
842        for (int m=clonedMemberList.size()-1; m >= 0; m--)
843          tellListenersAboutFormerMember(clonedMemberList.get(m), m);
844        
845        //The catalog will decide whether or not it actually removes members
846        if (catalog != null)
847          catalog.removeItems(clonedMemberList, (G)this);
848      }
849      
850      @SuppressWarnings("unchecked")
851      protected void tellListenersAboutFormerMember(I formerMember, int index)
852      {
853        if (!suppressNotification)
854          for (CatalogItemGroupListener<I,G,C> listener : listeners)
855            listener.memberRemoved((G)this, formerMember, index);
856      }
857    
858      //============================================================================
859      // MOVING MEMBERS
860      //============================================================================
861      
862      /**
863       * Moves {@code member} to an index one higher than its current index.
864       * The member will not be moved if it is not in this group, or if
865       * it is already in the highest position.
866       * <p>
867       * If {@code member} is in this group in multiple positions, the instance
868       * with the <i>highest</i> index will be moved.</p> 
869       * 
870       * @param member the member to be moved.
871       * 
872       * @return if no movement occurred, <i>null</i> is returned.  Otherwise
873       *         the member that was moved is returned.  This will be either
874       *         {@code member}, or an equal member that was already part of
875       *         this group. 
876       */
877      public I incrementIndexOf(I member)
878      {
879        I movedMember = null;
880        
881        int currentIndex = items.indexOf(member);
882        int highestIndex = items.size() - 1;
883        
884        if ((0 <= currentIndex) && (currentIndex < highestIndex))
885        {
886          suppressNotification = true;
887          
888          movedMember = remove(currentIndex);
889          items.add(currentIndex + 1, movedMember);
890          
891          suppressNotification = false;
892          tellListenersAboutMovedMember(movedMember, currentIndex, currentIndex+1);
893        }
894        
895        return movedMember;
896      }
897      
898      /**
899       * Moves {@code member} to an index one lower than its current index.
900       * The member will not be moved if it is not in this group, or if
901       * it is already in the lowest position.
902       * <p>
903       * If {@code member} is in this group in multiple positions, the instance
904       * with the <i>lowest</i> index will be moved.</p> 
905       * 
906       * @param member the member to be moved.
907       * 
908       * @return if no movement occurred, <i>null</i> is returned.  Otherwise
909       *         the member that was moved is returned.  This will be either
910       *         {@code member}, or an equal member that was already part of
911       *         this group. 
912       */
913      public I decrementIndexOf(I member)
914      {
915        I movedMember = null;
916        
917        int currentIndex = items.indexOf(member);
918        
919        if (0 < currentIndex)
920        {
921          suppressNotification = true;
922          
923          movedMember = remove(currentIndex);
924          items.add(currentIndex - 1, movedMember);
925          
926          suppressNotification = false;
927          tellListenersAboutMovedMember(movedMember, currentIndex, currentIndex-1);
928        }
929        
930        return movedMember;
931      }
932      
933      /**
934       * Moves the member at {@code index} to an index of {@code index}-plus-one.
935       * No move will occur if {@code index} is out of bounds, or if it is already
936       * the highest index of this group.
937       * 
938       * @param index the index of the member to be moved.
939       * 
940       * @return the member that was moved, or <i>null</i> if no member was moved. 
941       */
942      public I incrementIndexOfMemberAt(int index)
943      {
944        I movedMember = null;
945        
946        int highestIndex = items.size() - 1;
947        
948        if ((0 <= index) && (index < highestIndex))
949        {
950          suppressNotification = true;
951          
952          movedMember = items.remove(index);
953          items.add(index + 1, movedMember);
954          
955          suppressNotification = false;
956          tellListenersAboutMovedMember(movedMember, index, index+1);
957        }
958        
959        return movedMember;
960      }
961      
962      /**
963       * Moves the member at {@code index} to an index of {@code index}-minus-one.
964       * No move will occur if {@code index} is out of bounds, or if it is already
965       * the lowest index of this group.
966       * 
967       * @param index the index of the member to be moved.
968       * 
969       * @return the member that was moved, or <i>null</i> if no member was moved. 
970       */
971      public I decrementIndexOfMemberAt(int index)
972      {
973        I movedMember = null;
974        
975        int highestIndex = items.size() - 1;
976        
977        if ((0 < index) && (index <= highestIndex))
978        {
979          suppressNotification = true;
980          
981          movedMember = items.remove(index);
982          items.add(index - 1, movedMember);
983          
984          suppressNotification = false;
985          tellListenersAboutMovedMember(movedMember, index, index+1);
986        }
987        
988        return movedMember;
989      }
990      
991      /**
992       * Swaps the members at the given positions.
993       * <p>
994       * If the indices are equal, no action is taken.
995       * If one or both of the indices are not in the range
996       * [0,size()), the underlying {@link List} will throw
997       * an exception.</p>
998       * 
999       * @param index1 the position of the one of the members.
1000       * @param index2 the position of another of the members.
1001       * 
1002       * @return a list with the member originally at the lower index followed by
1003       *         the member originally at the higher index.  If no swap was made
1004       *         the returned list will be empty.
1005       */
1006      public List<I> swap(int index1, int index2)
1007      {
1008        List<I> swappedMembers = new ArrayList<I>(); 
1009        
1010        if (index1 != index2)
1011        {
1012          int low  = Math.min(index1, index2);
1013          int high = Math.max(index1, index2);
1014        
1015          I lowMember  = items.get(low);
1016          I highMember = items.get(high);
1017        
1018          items.remove(high);
1019          items.add(high, lowMember);
1020          items.remove(low);
1021          items.add(low, highMember);
1022          
1023          tellListenersAboutMovedMember(highMember, high, low);
1024          
1025          //The indices are incremented here because listeners will receive this
1026          //message after the one above.  So to them, it looks as if lowMember is
1027          //currently at position low+1.
1028          tellListenersAboutMovedMember(lowMember, low+1, high+1);
1029          
1030          swappedMembers.add(lowMember);
1031          swappedMembers.add(highMember);
1032        }
1033        
1034        return swappedMembers;
1035      }
1036    
1037      /**
1038       * Moves a member {@code fromIndex} {@code toIndex}.
1039       * 
1040       * @param fromIndex the current index of a member.
1041       * @param toIndex the new index of that same member.
1042       * @return the member that moved, even if it moved back to the same position.
1043       */
1044      public I move(int fromIndex, int toIndex)
1045      {
1046        suppressNotification = true;
1047        
1048        I movedMember = items.remove(fromIndex);
1049        
1050        if (movedMember != null)
1051        {
1052          int indexMax = items.size();
1053          if (toIndex > indexMax)
1054            toIndex = indexMax;
1055          
1056          items.add(toIndex, movedMember);
1057          
1058          suppressNotification = false;
1059          tellListenersAboutMovedMember(movedMember, fromIndex, toIndex);
1060        }
1061        else
1062        {
1063          suppressNotification = false;
1064        }
1065        
1066        return movedMember;
1067      }
1068      
1069      /**
1070       * Moves {@code member} {@code toIndex}.
1071       * 
1072       * @param member the member to be moved.
1073       * @param toIndex the new position for {@code member}.
1074       * @return the member that moved, or <i>null</i> if no movement occurred.
1075       *         Note that the member that moved might be {@code member} or,
1076       *         alternatively, a member <em>equal to</em> {@code member},
1077       *         if this group has such a member.
1078       */
1079      public I move(I member, int toIndex)
1080      {
1081        int fromIndex = indexOf(member);
1082        
1083        return fromIndex < 0 ? null : move(fromIndex, toIndex);
1084      }
1085      
1086      @SuppressWarnings("unchecked")
1087      protected void tellListenersAboutMovedMember(I member,
1088                                                   int fromIndex, int toIndex)
1089      {
1090        if (!suppressNotification)
1091          for (CatalogItemGroupListener<I,G,C> listener : listeners)
1092            listener.memberMoved((G)this, member, fromIndex, toIndex);
1093      }
1094      
1095      /**
1096       * Uses {@code comparator} to sort the members of this group.
1097       * @param comparator used to order this group's members
1098       */
1099      @SuppressWarnings("unchecked")
1100      public void sort(Comparator<? super I> comparator)
1101      {
1102        Collections.sort(items, comparator);
1103    
1104        if (!suppressNotification)
1105          for (CatalogItemGroupListener<I,G,C> listener : listeners)
1106            listener.membersSorted((G)this);
1107      }
1108    
1109      //============================================================================
1110      // FETCHING MEMBERS AND MEMBER INFORMATION
1111      //============================================================================
1112    
1113      /** 
1114       * Returns the number of members in this group.
1115       * @return the number of members in this group.
1116       */
1117      public int size()
1118      {
1119        return items.size();
1120      }
1121      
1122      /**
1123       * Returns the member at the given position.
1124       * 
1125       * @param index the position of the member to return.
1126       * 
1127       * @return the member at {@code index}.
1128       */
1129      public I get(int index)
1130      {
1131        return items.get(index);
1132      }
1133      
1134      /**
1135       * Returns a list of this group's members.
1136       * <p>
1137       * Note that the returned list is <i>not</i> held internally by this group.
1138       * This means that any changes made to the returned list will <i>not</i>
1139       * be reflected in this object.  The elements of the list, though, are the
1140       * actual members of this group, so changes made to them will be reflected
1141       * in this group (and in all other groups of which those items are
1142       * members).</p>
1143       * 
1144       * @return a list of this group's members.  The returned value is guaranteed
1145       *         to be non-null, but it may be an empty list.
1146       */
1147      public List<I> getAll()
1148      {
1149        return new ArrayList<I>(items);
1150      }
1151      
1152      /** Returns the internal list of members. */
1153      protected List<I> getInternalMemberList()
1154      {
1155        return items;
1156      }
1157      
1158      /**
1159       * Replaces the internal member list with {@code replacementList}.
1160       * This method is meant solely for use by frameworks (such
1161       * as JAXB or Hibernate) that rely on getX/setX pairs for
1162       * persisted properties.  It is {@code protected}, as opposed to
1163       * {@code private}, so that subclasses can create and annotate
1164       * methods that call this one.
1165       */
1166      protected void setInternalMemberList(List<I> replacementList)
1167      {
1168        items = (replacementList == null) ? new ArrayList<I>()
1169                                            : replacementList;
1170      }
1171    
1172      /**
1173       * Returns <i>true</i> if this group has no members.
1174       * @return <i>true</i> if this group has no members.
1175       */
1176      public boolean isEmpty()
1177      {
1178        return items.isEmpty();
1179      }
1180      
1181      /**
1182       * Returns <i>true</i> if {@code member} belongs to this group.
1183       * @param member a possible member of this group.
1184       * @return <i>true</i> if {@code member} belongs to this group.
1185       */
1186      public boolean contains(I member)
1187      {
1188        return items.contains(member);
1189      }
1190    
1191      /**
1192       * Returns the index in this group of the first occurrence of
1193       * {@code member}.  Indexing is zero-based.
1194       * 
1195       * @param member the member whose index is sought.
1196       * 
1197       * @return the index of the first occurrence of {@code member}.
1198       *         If {@code member} does not belong to this group,
1199       *         the returned value will be less than zero. 
1200       */
1201      public int indexOf(I member)
1202      {
1203        return member == null ? -1 : items.indexOf(member); 
1204      }
1205    
1206      /**
1207       * Returns the index in this group of the last occurrence of
1208       * {@code member}.  Indexing is zero-based.
1209       * 
1210       * @param member the member whose index is sought.
1211       * 
1212       * @return the index of the last occurrence of {@code member}.
1213       *         If {@code member} does not belong to this group,
1214       *         the returned value will be less than zero. 
1215       */
1216      public int lastIndexOf(I member)
1217      {
1218        return member == null ? -1 : items.lastIndexOf(member); 
1219      }
1220      
1221      //============================================================================
1222      // LISTENERS
1223      //============================================================================
1224    
1225      /**
1226       * Adds {@code newListener} to this group's list.
1227       * <p>
1228       * The listener will be informed whenever:
1229       * <ul>
1230       *   <li>A new member is added to this group</li>
1231       *   <li>A current member is removed from this group</li>
1232       *   <li>A current member is moved from one position
1233       *       in this group to another</li>
1234       * </ul></p>
1235       * 
1236       * @param newListener the listener to be added to this group's list.
1237       */
1238      public void addListener(CatalogItemGroupListener<I,G,C> newListener)
1239      {
1240        if (newListener != null)
1241          listeners.add(newListener);
1242      }
1243    
1244      /**
1245       * Removes {@code listener} from this group's list.
1246       * @param listener the listener to be removed from this group's list.
1247       */
1248      public void removeListener(CatalogItemGroupListener<I,G,C> listener)
1249      {
1250        listeners.remove(listener);
1251      }
1252    
1253      /** Removes all listeners from this group. */
1254      public void removeAllListeners()
1255      {
1256        listeners.clear();
1257      }
1258      
1259      //============================================================================
1260      // TEXT
1261      //============================================================================
1262      
1263      /**
1264       * Returns a text representation of this group.
1265       * @return a text representation of this group.
1266       */
1267      public String toString()
1268      {
1269        StringBuilder buff = new StringBuilder();
1270          
1271        buff.append("name=").append(name);
1272        buff.append(", id=").append(id);
1273        buff.append(", size=").append(size());
1274          
1275        return buff.toString();
1276      }
1277    
1278      /**
1279       * Returns an XML representation of this group.
1280       * @return an XML representation of this group.
1281       * @throws JAXBException if anything goes wrong during the conversion to XML.
1282       */
1283      public String toXml() throws JAXBException
1284      {
1285        return JaxbUtility.getSharedInstance().objectToXmlString(this);
1286      }
1287      
1288      /**
1289       * Writes an XML representation of this group to {@code writer}.
1290       * @param writer the device to which XML is written.
1291       * @throws JAXBException if anything goes wrong during the conversion to XML.
1292       */
1293      public void writeAsXmlTo(Writer writer) throws JAXBException
1294      {
1295        JaxbUtility.getSharedInstance().writeObjectAsXmlTo(writer, this, null);
1296      }
1297    
1298      //============================================================================
1299      // 
1300      //============================================================================
1301      
1302      /**
1303       * Clones this group and adds it to the same catalog to which it belongs.
1304       * If this group belongs to no catalog, this method behaves the same as
1305       * {@link #clone()}.
1306       * <p>
1307       * <b><u>Special Notes on Cloning Methodology</u></b>
1308       * <ol>
1309       *   <li>The clone's ID property will be set to a
1310       *       {@link #getIdOfUnidentified() value} that indicates
1311       *       that this group is currently unidentified.</li>
1312       *   <li>The clone will have no listeners.</li>
1313       * </ol></p>
1314       * <p>
1315       * If anything goes wrong during the cloning procedure,
1316       * a {@code RuntimeException} will be thrown.</p>
1317       *  
1318       * @return a clone of this group that belongs to the same catalog.
1319       * 
1320       * @see #clone()
1321       */
1322      @SuppressWarnings("unchecked")
1323      public G cloneIntoSameCatalog()
1324      {
1325        //Quick exit for null catalog
1326        if (this.catalog == null)
1327          return clone();
1328        
1329        //The code from here down knows that the clone's catalog is NOT NULL.
1330        G clonedGroup = null;
1331    
1332        try
1333        {
1334          //This line takes care of the primitive fields properly
1335          //(This is the line that requires suppression of unchecked conversions.)
1336          clonedGroup = (G)super.clone();
1337          
1338          //We do NOT want the clone to have the same ID as the original.
1339          //The ID is here for the persistence layer; it is in charge of
1340          //setting IDs.  To help it, we put the clone's ID in the uninitialized
1341          //state.
1342          clonedGroup.id = getIdOfUnidentified();
1343          
1344          adjustNameForCatalog(this.catalog);
1345          
1346          //Clear the listeners.
1347          clonedGroup.listeners =
1348            new CopyOnWriteArraySet<CatalogItemGroupListener<I,G,C>>();
1349    
1350          //Set up the catalog, which we know is NOT null.
1351          clonedGroup.simplySetCatalog(this.catalog);
1352          this.catalog.simplyAddGroup(clonedGroup);
1353    
1354          //Create new member list & populate w/ references, not clones.
1355          //Remember: the catalog is the main holder of the items; the groups'
1356          //items are merely references to those in the catalog.  We can do
1357          //this ONLY because we know we're cloning this group into the same
1358          //non-null catalog.
1359          clonedGroup.items = new ArrayList<I>();
1360          for (I originalMember : this.items)
1361          {
1362            clonedGroup.add(originalMember);
1363          }
1364        }
1365        catch (Exception ex)
1366        {
1367          throw new RuntimeException(ex);
1368        }
1369        
1370        return clonedGroup;
1371      }
1372    
1373      /**
1374       * Returns a group that is a copy of this one.
1375       * The clone is a deep clone; all the members in the returned group are
1376       * copies of those in this group.
1377       * <p>
1378       * <b><u>Special Notes on Cloning Methodology</u></b>
1379       * <ol>
1380       *   <li>The clone's ID property will be set to a
1381       *       {@link #getIdOfUnidentified() value} that indicates
1382       *       that this group is currently unidentified.</li>
1383       *   <li>The clone will belong to no catalog.  That is, its catalog
1384       *       property will be <i>null</i>.</li>
1385       *   <li>The clone will have no listeners.</li>
1386       * </ol></p>
1387       * <p>
1388       * If anything goes wrong during the cloning procedure,
1389       * a {@code RuntimeException} will be thrown.</p>
1390       * 
1391       * @see #cloneIntoSameCatalog()
1392       */
1393      @Override
1394      public G clone()
1395      {
1396        G clonedGroup = cloneAllButMembers();
1397    
1398        for (I originalMember : this.items)
1399          clonedGroup.add(originalMember.clone());
1400    
1401        return clonedGroup;
1402      }
1403    
1404      /**
1405       * Clones this group but does not add any members to the new group.
1406       * @return a partial clone of this group.  The returned clone has
1407       *         no members.
1408       */
1409      protected G cloneAllExceptMembers()
1410      {
1411        return cloneAllButMembers();
1412      }
1413    
1414      //Clones this group except for 
1415      @SuppressWarnings("unchecked")
1416      private G cloneAllButMembers()
1417      {
1418        G clonedGroup = null;
1419    
1420        try
1421        {
1422          //This line takes care of the primitive fields properly.
1423          //(This is the line that requires suppression of unchecked conversions.)
1424          clonedGroup = (G)super.clone();
1425          
1426          //We do NOT want the clone to have the same ID as the original.
1427          //The ID is here for the persistence layer; it is in charge of
1428          //setting IDs.  To help it, we put the clone's ID in the uninitialized
1429          //state.
1430          clonedGroup.id = getIdOfUnidentified();
1431          
1432          //Clone list, but it's OK to share refs to the same immutable strings.
1433          clonedGroup.notes = new ArrayList<String>(this.notes);
1434           
1435          //Set the catalog to null.
1436          clonedGroup.simplySetCatalog(null);
1437          
1438          //Clear the listeners.
1439          clonedGroup.listeners =
1440            new CopyOnWriteArraySet<CatalogItemGroupListener<I,G,C>>();
1441    
1442          //Make new member list, but leave list empty.
1443          clonedGroup.items = new ArrayList<I>();
1444        }
1445        catch (Exception ex)
1446        {
1447          throw new RuntimeException(ex);
1448        }
1449        
1450        return clonedGroup;
1451      }
1452      
1453      /**
1454       * Copies <i>some</i> properties of {@code otherGroup} into this one.
1455       * <p>
1456       * This method is used by the catalog's clone method to help copy
1457       * its main group, which is <i>not</i> created via the normal cloning
1458       * process.  Subclasses should override this method and call
1459       * <tt>super.copyPropertiesFrom(otherGroup)</tt>.  Failure to do so
1460       * will result in some properties of a cloned catalog from matching
1461       * that of the original.</p>
1462       * <p>
1463       * What is <i>not</i> copied:</p>
1464       * <ul>
1465       *   <li>id</li>
1466       *   <li>catalog reference</li>
1467       *   <li>listeners</li>
1468       *   <li>items</li>
1469       * </ul>
1470       * <p>
1471       * Subclasses should copy only those new properties that it
1472       * creates.  This list will be similar, if not identical, to
1473       * the properties copied in the subclass's clone method.</p>
1474       * <p>
1475       * The fact that this method has become necessary probably means
1476       * that the way we're handling setup of a catalog's main group
1477       * could probably be done better.  One reason we don't just
1478       * clone the main group is the main group uses a special
1479       * non-public constructor.  A reworking of the main group creation
1480       * and cloning code should be done later on.</p>
1481       * 
1482       * @param otherGroup a source of property values.
1483       */
1484      protected void copyPropertiesFrom(G otherGroup)
1485      {
1486        //Clone list, but it's OK to share refs to the same immutable strings.
1487        this.notes = new ArrayList<String>(otherGroup.notes);
1488      }
1489      
1490      /**
1491       * Returns <i>true</i> if {@code o} is equal to this group in all respects
1492       * with the possible exception of the ordering of the members.
1493       */
1494      @SuppressWarnings("unchecked")
1495      public boolean equalsWithoutRespectToOrder(Object o)
1496      {
1497        //Quick exit if o is this
1498        if (o == this)
1499          return true;
1500        
1501        //Quick exit if clearly unequal
1502        if (clearlyNotEqualTo(o))
1503          return false;
1504        
1505        CatalogItemGroup other = (CatalogItemGroup)o;
1506        
1507        //Return true if members are same, even if they're in diff orders
1508        int count = this.size();
1509        for (int m=0; m < count; m++)
1510        {
1511          if (!this.items.contains(other.items.get(m)))
1512            return false;
1513    
1514          if (!other.items.contains(this.items.get(m)))
1515            return false;
1516        }
1517        
1518        return true;
1519      }
1520      
1521      /** Returns <i>true</i> if {@code o} is equal to this group. */
1522      @SuppressWarnings("unchecked")
1523      @Override
1524      public boolean equals(Object o)
1525      {
1526        //Quick exit if o is this
1527        if (o == this)
1528          return true;
1529        
1530        //Quick exit if clearly unequal
1531        if (clearlyNotEqualTo(o))
1532          return false;
1533        
1534        CatalogItemGroup other = (CatalogItemGroup)o;
1535    
1536        //Members must be equal and in same order
1537        return this.items.equals(other.items);
1538      }
1539      
1540      @SuppressWarnings("unchecked")
1541      protected boolean clearlyNotEqualTo(Object o)
1542      {
1543        //Quick exit if o is null
1544        if (o == null)
1545          return true;
1546        
1547        //Quick exit if classes are different
1548        if (!o.getClass().equals(this.getClass()))
1549          return true;
1550        
1551        CatalogItemGroup other = (CatalogItemGroup)o;
1552        
1553        //NOTE: Absence of ID and catalog is intentional
1554        
1555        //Compare the names and size
1556        if (!this.name.equals(other.name) ||
1557            (this.nameIsLocked != other.nameIsLocked) ||
1558            (this.size() != other.size()))
1559          return true;
1560        
1561        return false;
1562      }
1563    
1564      /** Returns a hash code value for this group. */
1565      public int hashCode()
1566      {
1567        //Taken from the Effective Java book by Joshua Bloch.
1568        //The constants 17 & 37 are arbitrary & carry no meaning.
1569        int result = 17;
1570        
1571        //NOTE: Keep this method in synch w/ equals
1572        
1573        result = 37 * result + name.hashCode();
1574        result = 37 * result + new Boolean(nameIsLocked).hashCode();
1575        result = 37 * result + items.hashCode();
1576        
1577        return result;
1578      }
1579    }