001    package edu.nrao.sss.model.source;
002    
003    import java.io.FileNotFoundException;
004    import java.io.Reader;
005    import java.util.ArrayList;
006    import java.util.Arrays;
007    import java.util.Collection;
008    import java.util.Comparator;
009    import java.util.Date;
010    import java.util.HashMap;
011    import java.util.List;
012    import java.util.Map;
013    
014    import javax.xml.bind.JAXBException;
015    import javax.xml.bind.annotation.XmlIDREF;
016    import javax.xml.bind.annotation.XmlRootElement;
017    import javax.xml.bind.annotation.XmlType;
018    import javax.xml.stream.XMLStreamException;
019    
020    import edu.nrao.sss.catalog.CatalogItemGroup;
021    import edu.nrao.sss.model.RepositoryException;
022    import edu.nrao.sss.util.Filter;
023    import edu.nrao.sss.util.Identifiable;
024    import edu.nrao.sss.util.JaxbUtility;
025    
026    /**
027     * A collection of {@link Source}s and {@link SourceLookupTable}s.
028     * A {@code SourceGroup} is normally contained in a
029     * {@link SourceCatalog} and is a way of categorizing sources that
030     * have similar traits.
031     * <p>
032     * <b>Version Info:</b>
033     * <table style="margin-left:2em">
034     *   <tr><td>$Revision: 2313 $</td></tr>
035     *   <tr><td>$Date: 2009-05-20 15:00:52 -0600 (Wed, 20 May 2009) $</td></tr>
036     *   <tr><td>$Author: btruitt $</td></tr>
037     * </table></p>
038     *  
039     * @author David M. Harland
040     * @since 2006-09-15
041     */
042    @XmlRootElement
043    @XmlType(propOrder={"source", "sourceTable"})
044    public class SourceGroup
045      extends CatalogItemGroup<SourceCatalogEntry, SourceGroup, SourceCatalog>
046      implements Identifiable, Cloneable, SourceProvider
047    {
048      private static final boolean        CHECK_FOR_NON_NULL_CATALOG = true;
049      private static final boolean DO_NOT_CHECK_FOR_NON_NULL_CATALOG = false; 
050      
051      //NON-PERSISTED PROPERTIES
052      private SourceTableListener tableListener;
053      
054      /**
055       * Creates a new group that has a default name and that belongs to no catalog.
056       */
057      public SourceGroup()
058      {
059        this(null, null);
060      }
061      
062      /**
063       * Creates a new group that belongs to {@code container}.
064       * 
065       * @param container the name of the one catalog to which this group belongs.
066       *                  This value may be <i>null</i>.
067       *                  
068       * @param nameOfGroup the name of this group.  If this value is <i>null</i>,
069       *                    a non-null default name will be used.
070       */
071      public SourceGroup(SourceCatalog container, String nameOfGroup)
072      {
073        super(container, nameOfGroup);
074        
075        if (container == null)
076          tableListener = new Listener(this, CHECK_FOR_NON_NULL_CATALOG);
077      }
078    
079      /**
080       * Special constructor used only by SourceCatalog for constructing its main
081       * group.
082       */
083      SourceGroup(SourceCatalog container)
084      {
085        super(container);
086    
087        tableListener = new Listener(this, DO_NOT_CHECK_FOR_NON_NULL_CATALOG);
088      }
089    
090      /** Returns {@link Identifiable#UNIDENTIFIED}. */
091      @Override
092      protected long getIdOfUnidentified()
093      {
094        return Identifiable.UNIDENTIFIED;
095      }
096    
097      //============================================================================
098      // CONTAINER
099      //============================================================================
100      
101      /**
102       * Sets the catalog to which this source group belongs.
103       * See {@link CatalogItemGroup#setCatalog(edu.nrao.sss.catalog.Catalog)}
104       * for more details.
105       * 
106       * @param newCatalog the catalog to which this group belongs.
107       * 
108       * @return <i>true</i> if {@code newCatalog} is the new catalog for this
109       *         group.  Note that this means if {@code newCatalog} is already
110       *         the catalog of this group, the return value is <i>true</i>. 
111       */
112      @Override
113      public boolean setCatalog(SourceCatalog newCatalog)
114      {
115        //Quick exit if this group already belongs to newCatalog.
116        //(Intentional use of "==" here.)
117        if (getCatalog() == newCatalog)
118          return true;  //"true" that newCatalog is this group's catalog
119    
120        boolean haveNewCatalog = super.setCatalog(newCatalog);
121        
122        //Change listener ONLY if newCatalog is our catalog.
123        if (haveNewCatalog)
124        {
125          tableListener =
126            (newCatalog == null) ? new Listener(this, CHECK_FOR_NON_NULL_CATALOG)
127                                 : newCatalog.getTableListener();
128            
129          synchronizeTablesWithListener();
130        }
131        
132        return haveNewCatalog;
133      }
134      
135      /**
136       * Sets this group's catalog to {@code newCatalog} without
137       * contacting either the former or new catalog.  This method is
138       * used only by the SourceCatalog class.
139       */
140      @Override
141      protected void simplySetCatalog(SourceCatalog newCatalog)
142      {
143        super.simplySetCatalog(newCatalog);
144        
145        tableListener =
146          (getCatalog() == null) ? new Listener(this, CHECK_FOR_NON_NULL_CATALOG)
147                                 : newCatalog.getTableListener();
148    
149        synchronizeTablesWithListener();
150      }
151      
152      //============================================================================
153      // ADDING MEMBERS
154      //============================================================================
155    
156      /**
157       * Adds a new member to this source group.
158       * <p>
159       * If this group is not part of a catalog, and if the {@code newMember} is
160       * accepted into the group, this group will hold a reference to
161       * {@code newMember}.  However, if this group is part of a catalog, and
162       * if the catalog holds an entry that is <i>equal</i> to {@code newMember},
163       * this group will hold a reference to the equivalent entry in the
164       * catalog.</p>
165       * 
166       * @param newMember a new member of this source group.
167       * 
168       * @return <i>true</i> if this group changed as a result of the call.
169       *         Reasons for a return value of <i>false</i>:
170       *         <ol>
171       *           <li>{@code newMember} is <i>null</i></li>
172       *           <li>{@code newMember} is already a member of this group</li>
173       *         </ol>
174       */
175      @Override
176      public SourceCatalogEntry add(SourceCatalogEntry newMember)
177      {
178        SourceCatalogEntry addedMember = super.add(newMember);
179    
180        //Special logic for handling listeners of lookup tables
181        if (addedMember instanceof SourceLookupTable)
182        {
183          SourceLookupTable newTable = (SourceLookupTable)addedMember;
184    
185          newTable.setListener(tableListener);
186          
187          addSourcesFrom(newTable);
188        }
189        
190        return addedMember;
191      }
192    
193      //Adds sources from table to this group, or this group's catalog, or
194      //replaces the table's source with a reference to an equal source
195      //that is already in this group, or this group's catalog.
196      private void addSourcesFrom(SourceLookupTable table)
197      {
198        //Redirect & quick exit if this group is in a catalog
199        if (getCatalog() != null)
200        {
201          ((SourceCatalog)getCatalog()).addSourcesFrom(table);
202          return;
203        }
204        
205        //CATALOG IS NULL FROM HERE DOWN
206        
207        //Get a list of the current sources from this group
208        List<SourceCatalogEntry> currentSources = getInternalMemberList();
209        
210        //For each source in the table, either replace the table's source
211        //with an EQUAL source from our current entries, or add the new
212        //source to our list of entries.
213        for (Date key : table.getKeySet())
214        {
215          Source tableSource = table.get(key);
216    
217          int index = currentSources.indexOf(tableSource);
218    
219          if (index >= 0)
220          {
221            //Group has EQUAL source.  If it is not IDENTICAL source,
222            //update the table so that it refers to the existing source.
223            Source currentMember = (Source)currentSources.get(index);
224            if (tableSource != currentMember)
225              table.put(key, currentMember);
226          }
227          else
228          {
229            //This group has no equal source, so add it to this group.
230            add(tableSource);
231          }
232        }
233      }
234    
235      //============================================================================
236      // REMOVING MEMBERS
237      //============================================================================
238    
239      @Override
240      public SourceCatalogEntry remove(SourceCatalogEntry member)
241      {
242        SourceCatalogEntry removedMember = super.remove(member);
243        
244        if (removedMember != null)
245          finishRemovingMember(removedMember);
246        
247        return removedMember;
248      }
249    
250      @Override
251      public SourceCatalogEntry remove(int index)
252      {
253        SourceCatalogEntry removedMember = super.remove(index);
254        
255        if (removedMember != null)
256          finishRemovingMember(removedMember);
257        
258        return removedMember;
259      }
260      
261      //Deals with removal of tables entries within tables.
262      private void finishRemovingMember(SourceCatalogEntry removedMember)
263      {
264        if (getCatalog() == null)
265        {
266          //If we're removing a table, and we're not part of a catalog,
267          //we need to remove our listener from the table.
268          //Note: do NOT remove the table's sources from this group.  Those
269          //sources may have been put there directly, not via the add of
270          //the table.
271          if (removedMember instanceof SourceLookupTable)
272          {
273            ((SourceLookupTable)removedMember).removeListener(tableListener);
274          }
275          //If we're removing a source, and we're not part of a catalog,
276          //we need to remove the source from every table in this group.
277          //Note: even though we remove sources from tables, we do NOT
278          //remove even an empty table from the group here.
279          else if (removedMember instanceof Source)
280          {
281            try
282            {
283              for (SourceLookupTable table : getSourceTables())
284                table.removeValue((Source)removedMember);
285            }
286            catch (RepositoryException ex)
287            {
288              //Can't happen, but just in case...
289              throw new RuntimeException("PROGRAMMER ERROR", ex);
290            }
291          }
292        }
293      }
294      
295      /**
296       * Removes all sources from this group.
297       * @return a list of all sources that were removed from this group.
298       */
299      public List<Source> removeAllSources()
300      {
301        List<Source> removedSources = new ArrayList<Source>();
302        
303        for (SourceCatalogEntry member : getAll())
304          if (member instanceof Source)
305            removedSources.add((Source)remove(member));
306        
307        return removedSources;
308      }
309      
310      /**
311       * Removes all source tables from this group.
312       * @return a list of all source tables that were removed from this group.
313       */
314      public List<SourceLookupTable> removeAllSourceTables()
315      {
316        List<SourceLookupTable> removedTables = new ArrayList<SourceLookupTable>();
317        
318        for (SourceCatalogEntry member : getAll())
319          if (member instanceof SourceLookupTable)
320            removedTables.add((SourceLookupTable)remove(member));
321        
322        return removedTables;
323      }
324      
325      //============================================================================
326      // FETCHING MEMBERS AND MEMBER INFORMATION
327      //============================================================================
328      //----------------------------------------------------------------------------
329      // SourceProvider Interface
330      //----------------------------------------------------------------------------
331      
332      /**
333       * Returns a set of all sources held by this group.
334       * <p>
335       * If this group is contained in a catalog, it may have references to
336       * sources that are not members of this group.  This happens when the
337       * group contains {@code SourceLookupTable}s; the sources in those tables
338       * need not be in this group.  In this situation, those sources are
339       * <i>not</i> present in the returned set.  Only sources held directly
340       * by this group are returned.</p>
341       * <p>
342       * Note that the returned set is <i>not</i> held internally by this gorup.
343       * This means that any changes made to the set after calling this method
344       * will <i>not</i> be reflected in this object.  The sources themselves,
345       * however, are the actual sources held in this group, so changes made to
346       * them <i>will</i> be reflected in this object.  Furthermore, if this
347       * group is part of a catalog, then the catalog, and perhaps other groups,
348       * will be referring to these same source instances, so changes made to
349       * the sources in the returned set will be reflected in all those other
350       * containers.</p>
351       *  
352       * @return a set of all sources held by this group.
353       * 
354       * @throws RepositoryException under no conditions.
355       * 
356       * @see #getAll()
357       * @see #getSourceTables()
358       * @see #getSources(SourceFilter)
359       */
360      public List<Source> getSources()
361        throws RepositoryException
362      {
363        List<Source> results = new ArrayList<Source>();
364        
365        for (SourceCatalogEntry member : getAll())
366        {
367          if (member instanceof Source)
368          {
369            results.add((Source)member);
370          }
371        }
372        
373        return results;
374      }
375    
376      /**
377       * Returns a set of all sources in this group that can pass through
378       * {@code filter}.  If {@code filter} is <i>null</i>, it will be treated
379       * as a wide-open filter, allowing all sources to pass.
380       * <p>
381       * See {@link #getSources()} for details about the returned set.</p>
382       * 
383       * @param filter a filter to apply to all sources in this group.  Only
384       *               those sources that may pass through {@code filter}
385       *               will be in the returned set.
386       *               
387       * @return a set of all sources in this group that can pass through
388       *         {@code filter}.
389       *         
390       * @throws RepositoryException under no conditions.
391       * 
392       * @see #getAll()
393       * @see #getSources()
394       * @see #getSourceTables()
395       */
396      public List<Source> getSources(Filter<Source> filter)
397        throws RepositoryException
398      {
399        //Quick exit for null filter
400        if (filter == null)
401          return getSources();
402    
403        List<Source> results = new ArrayList<Source>();
404        
405        for (SourceCatalogEntry member : getAll())
406        {
407          if (member instanceof Source)
408          {
409            Source source = (Source)member;
410            if (filter.allows(source))
411              results.add(source);
412          }
413        }
414        
415        return results;
416      }
417      
418      /**
419       * Returns a set of all source lookup tables held by this group.
420       * <p>
421       * Note that the returned set is <i>not</i> held internally by this gorup.
422       * This means that any changes made to the set after calling this method
423       * will <i>not</i> be reflected in this object.  The tables themselves,
424       * however, are the actual tables held in this group, so changes made to
425       * them <i>will</i> be reflected in this object.  Furthermore, if this
426       * group is part of a catalog, then the catalog, and perhaps other groups,
427       * will be referring to these same table instances, so changes made to
428       * the tables in the returned set will be reflected in all those other
429       * containers.</p>
430       * 
431       * @return a set of all source lookup tables held by this group.
432       * 
433       * @throws RepositoryException under no conditions.
434       * 
435       * @see #getAll()
436       * @see #getSources()
437       */
438      public List<SourceLookupTable> getSourceTables()
439        throws RepositoryException
440      {
441        List<SourceLookupTable> results = new ArrayList<SourceLookupTable>();
442        
443        for (SourceCatalogEntry member : getAll())
444        {
445          if (member instanceof SourceLookupTable)
446          {
447            results.add((SourceLookupTable)member);
448          }
449        }
450        
451        return results;
452      }
453    
454      /* (non-Javadoc)
455       * @see SourceProvider#findSourceById(long)
456       */
457      public Source findSourceById(long id) throws RepositoryException
458      {
459        Source result = null;
460        
461        for (SourceCatalogEntry member : getAll())
462        {
463          if ((member instanceof Source) &&
464              (member.getId().longValue() == id))
465          {
466            result = (Source)member;
467            break;
468          }
469        }
470        
471        return result;
472      }
473    
474      /* (non-Javadoc)
475       * @see SourceProvider#findSourceTableById(long)
476       */
477      public SourceLookupTable findSourceTableById(long id)
478        throws RepositoryException
479      {
480        SourceLookupTable result = null;
481        
482        for (SourceCatalogEntry member : getAll())
483        {
484          if ((member instanceof SourceLookupTable) &&
485              (member.getId().longValue() == id))
486          {
487            result = (SourceLookupTable)member;
488            break;
489          }
490        }
491        
492        return result;
493      }
494    
495      /* (non-Javadoc)
496       * @see SourceProvider#findSourceByName(java.lang.String)
497       */
498      public List<Source> findSourceByName(String name)
499        throws RepositoryException
500      {
501        List<Source> results = new ArrayList<Source>();
502        
503        for (SourceCatalogEntry member : getAll())
504        {
505          if ((member instanceof Source) &&
506              name.equalsIgnoreCase(member.getName()))
507          {
508            results.add((Source)member);
509          }
510        }
511        
512        return results;
513      }
514      
515      /* (non-Javadoc)
516       * @see SourceProvider#findSourceTableByName(java.lang.String)
517       */
518      public List<SourceLookupTable> findSourceTableByName(String name)
519        throws RepositoryException
520      {
521        List<SourceLookupTable> results = new ArrayList<SourceLookupTable>();
522      
523        for (SourceCatalogEntry member : getAll())
524        {
525          if ((member instanceof SourceLookupTable) &&
526              name.equalsIgnoreCase(member.getName()))
527          {
528            results.add((SourceLookupTable)member);
529          }
530        }
531      
532        return results;
533      }
534    
535      //============================================================================
536      // LISTENING TO TABLES
537      //============================================================================
538      
539      SourceTableListener getTableListener()  { return tableListener; }
540    
541      /** Sets the listener of all contained tables to this group's listener. */
542      private void synchronizeTablesWithListener()
543      {
544        for (SourceCatalogEntry member : getAll())
545        {
546          if (member instanceof SourceLookupTable)
547          {
548            ((SourceLookupTable)member).setListener(this.tableListener); 
549          }
550        }
551      }
552    
553      //============================================================================
554      // TEXT
555      //============================================================================
556    
557      /**
558       * Creates a new group from the XML data in the given file.
559       * 
560       * @param xmlFile the name of an XML file.  This method will attempt to locate
561       *                the file by using {@link Class#getResource(String)}.
562       *                
563       * @return a new group from the XML data in the given file.
564       * 
565       * @throws FileNotFoundException if the XML file cannot be found.
566       * 
567       * @throws JAXBException if the schema file used (if any) is malformed, if
568       *           the XML file cannot be read, or if the XML file is not
569       *           schema-valid.
570       * 
571       * @throws XMLStreamException if there is a problem opening the XML file,
572       *           if the XML is not well-formed, or for some other
573       *           "unexpected processing conditions".
574       */
575      public static SourceGroup fromXml(String xmlFile)
576        throws JAXBException, XMLStreamException, FileNotFoundException
577      {
578        return JaxbUtility.getSharedInstance()
579                          .xmlFileToObject(xmlFile, SourceGroup.class);
580      }
581      
582      /**
583       * Creates a new group based on the XML data read from {@code reader}.
584       * 
585       * @param reader the source of the XML data.
586       *               If this value is <i>null</i>, <i>null</i> is returned.
587       *               
588       * @return a new group based on the XML data read from {@code reader}.
589       * 
590       * @throws XMLStreamException if the XML is not well-formed,
591       *           or for some other "unexpected processing conditions".
592       *           
593       * @throws JAXBException if anything else goes wrong during the
594       *           transformation.
595       */
596      public static SourceGroup fromXml(Reader reader)
597        throws JAXBException, XMLStreamException
598      {
599        return JaxbUtility.getSharedInstance()
600                          .readObjectAsXmlFrom(reader, SourceGroup.class, null);
601      }
602    
603      //----------------------------------------------------------------------------
604      // XML Helpers
605      //----------------------------------------------------------------------------
606      
607      //These methods are here solely to help JAXB do its thing.
608    
609      @XmlIDREF
610      @SuppressWarnings("unused")
611      private void setSourceTable(SourceLookupTable[] tables)
612      {
613        getInternalMemberList().addAll(Arrays.asList(tables));
614      }
615      @SuppressWarnings("unused")
616      private SourceLookupTable[] getSourceTable()
617      {
618        ArrayList<SourceLookupTable> tables = new ArrayList<SourceLookupTable>();
619        
620        for (SourceCatalogEntry member : this.getInternalMemberList())
621          if (member instanceof SourceLookupTable)
622            tables.add((SourceLookupTable)member);
623    
624        return tables.toArray(new SourceLookupTable[tables.size()]);
625      }
626    
627      @XmlIDREF
628      @SuppressWarnings("unused")
629      private void setSource(Source[] sources)
630      {
631        getInternalMemberList().addAll(Arrays.asList(sources));
632      }
633      @SuppressWarnings("unused")
634      private Source[] getSource()
635      {
636        ArrayList<Source> sources = new ArrayList<Source>();
637        
638        for (SourceCatalogEntry member : this.getInternalMemberList())
639          if (member instanceof Source)
640            sources.add((Source)member);
641    
642        return sources.toArray(new Source[sources.size()]);
643      }
644    
645      //============================================================================
646      // 
647      //============================================================================
648      
649      /**
650       * Returns a comparator that uses the name of the source groups sent to it.
651       * This comparator treats groups named "DEC +/-##" and "RA ##" as special
652       * cases, treating them as coming after all other group names.
653       * 
654       * @return
655       *   a comparator for sorting source groups by name, with special
656       *   treatment for RA and DEC groups.
657       */
658      public static Comparator<SourceGroup> getNameComparator()
659      {
660        return new SourceGroupNameComparator();
661      }
662      
663      /**
664       * Returns a source group that is a copy of this one.
665       * The clone is a deep clone; all the members in the returned group are
666       * copies of those in this group.
667       * <p>
668       * <b><u>Special Notes on Cloning Methodology</u></b>
669       * <ol>
670       *   <li>The clone's ID property will be set to
671       *       {@link Identifiable#UNIDENTIFIED}.</li>
672       *   <li>The clone will belong to no catalog.  That is, its catalog
673       *       property will be <i>null</i>.</li>
674       *   <li>Any tables that refer to sources that are not in this
675       *       group will be cloned into the returned group, and clones of
676       *       <i>the missing sources will be added to the returned group</i>.</li>
677       * </ol></p>
678       * 
679       * @see #cloneIntoSameCatalog()
680       */
681      @Override
682      public SourceGroup clone()
683      {
684        SourceGroup clonedGroup = null;
685    
686        try
687        {
688          clonedGroup = (SourceGroup)super.cloneAllExceptMembers();
689      
690          //Used for straightening out source reference held in tables
691          Map<SourceLookupTable, SourceLookupTable> tableMap =
692            new HashMap<SourceLookupTable, SourceLookupTable>();
693    
694          //Create new member list & populate w/ clones.
695          //We'll partially clone the lookup tables and add them now, then
696          //come back and fix up their references below.
697          for (SourceCatalogEntry originalMember : this.getInternalMemberList())
698          {
699            if (originalMember instanceof SourceLookupTable)
700            {
701              //We remove the sources from the cloned table prior to adding it
702              //to the cloned group because the add of the table may cause new
703              //source entries to be made in the cloned group.  We want to
704              //control the order in which these source (clones) are added.
705              SourceLookupTable clonedTable =
706                ((SourceLookupTable)originalMember).cloneAllButSources();
707              clonedTable.clear();
708              clonedGroup.add(clonedTable);
709              //This map is used later to populate the clonedTable
710              tableMap.put(clonedTable, (SourceLookupTable)originalMember);
711            }
712            else
713            {
714              clonedGroup.add(originalMember.clone());
715            }
716          }
717    
718          //At this point the cloned group has clones of all Source
719          //and SourceLookupTable elements in the proper order.
720          //The tables, though, are empty and need to be told to
721          //point at the cloned sources.
722          List<SourceCatalogEntry> clonedGroupsMembers =
723            clonedGroup.getInternalMemberList();
724          int memberCount = clonedGroupsMembers.size();
725          for (int i=0; i < memberCount; i++) //See Note 1 for trivia
726          {
727            SourceCatalogEntry clonedMember = clonedGroupsMembers.get(i);
728            
729            if (clonedMember instanceof SourceLookupTable)
730            {
731              SourceLookupTable clonedTable   = (SourceLookupTable)clonedMember;
732              SourceLookupTable originalTable = tableMap.get(clonedTable);
733              
734              for (Date key : originalTable.getKeySet())
735              {
736                Source originalSource = originalTable.get(key);
737    
738                int index = clonedGroupsMembers.indexOf(originalSource);
739                
740                if (index >= 0)
741                {
742                  //Cloned group has EQUAL source.  Make clonedTable point at it.
743                  clonedTable.put(key, (Source)clonedGroupsMembers.get(index));
744                }
745                else
746                {
747                  //Cloned group has no equal source.  This is legitimate iff
748                  //the original group belonged to a catalog.
749                  if (this.getCatalog() == null)
750                    throw new IllegalStateException(
751                      "Programmer Error: Orphan group is missing source " +
752                      originalSource.getName());
753                  
754                  //Clone original source & add.
755                  Source clonedSource = originalSource.clone();
756                  clonedGroup.add(clonedSource);
757                  clonedTable.put(key, clonedSource);
758                }
759              }//for key
760            }//s inst of table
761          }//for s : clone.members
762        }
763        catch (Exception ex)
764        {
765          throw new RuntimeException(ex);
766        }
767        
768        return clonedGroup;
769        
770        //Note 1:  This line was originally
771        //         "for (SourceCatalogEntry s : cloneGroup.members)".
772        //         This worked fine throughout unit testing.  However, when using
773        //         Hibernate and cloning a previously saved catalog, a
774        //         ConcurrentModificationException was thrown.  The for loop in
775        //         use now gets us around this problem.
776      }
777    
778      /**
779       *  Returns a source group that is a copy of this one.
780       *  <p>
781       *  The copying procedure uses a <i>shallow</i> approach: the returned
782       *  copy will contain the same members as this group, not copies of the
783       *  members.  Furthermore, the returned group will belong to the same catalog
784       *  as this group.</p> 
785       *  <p>
786       *  If anything goes wrong during the cloning procedure,
787       *  a {@code RuntimeException} will be thrown.</p>
788       */
789      /*
790      public SourceGroup makeShallowCopy()
791      {
792        SourceGroup clone = null;
793    
794        try
795        {
796          //This line takes care of the primitive fields properly
797          clone = (SourceGroup)super.clone();
798          
799          //We do NOT want the clone to have the same ID as the original.
800          //The ID is here for the persistence layer; it is in charge of
801          //setting IDs.  To help it, we put the clone's ID in the uninitialized
802          //state.
803          clone.id = Identifiable.UNIDENTIFIED;
804          
805          clone.name = "Copy "
806        }
807        catch (Exception ex)
808        {
809          throw new RuntimeException(ex);
810        }
811        
812        return clone;
813      }
814      */
815    
816      /**
817       * Returns <i>true</i> if {@code o} is equal to this source group.
818       * <p>
819       * In addition to having the same name, the two groups must have the
820       * same number of members, and for every member of this group their
821       * must be exactly one equal member in {@code o}.</p>
822       * <p>
823       * <b>Ordering of Members</b><br/>
824       * The members of the two groups do <i>not</i> have to be in the
825       * same order.  However, the sources in each group must be in the
826       * same position relative to all other sources in the group.  The
827       * same is true of the tables.  Example:</p><pre>
828       *   This Group       Group o
829       *   ----------       -------
830       *   source1          source1
831       *   tableA           source2
832       *   source2          source3
833       *   source3          tableA
834       *   tableB           tableB</pre>
835       * The two groups above would be equal because their sources
836       * are in the same order, relative only to the other sources,
837       * and the tables are also in the same relative order.
838       */
839      @Override
840      public boolean equals(Object o)
841      {
842        //Quick exit if o is this
843        if (o == this)
844          return true;
845        
846        //Quick exit if clearly unequal
847        if (clearlyNotEqualTo(o))
848          return false;
849        
850        SourceGroup other = (SourceGroup)o;
851        
852        //Compare the lists of sources and the lists of tables individually
853        ArrayList<Source> ourSources = new ArrayList<Source>();
854        ArrayList<Source> theirSources = new ArrayList<Source>();
855        
856        ArrayList<SourceLookupTable> ourTables =
857          new ArrayList<SourceLookupTable>();
858        ArrayList<SourceLookupTable> theirTables =
859          new ArrayList<SourceLookupTable>();
860        
861        for (SourceCatalogEntry member : this.getInternalMemberList())
862        {
863          if (member instanceof SourceLookupTable)
864            ourTables.add((SourceLookupTable)member);
865          else
866            ourSources.add((Source)member);
867        }
868        
869        for (SourceCatalogEntry member : other.getInternalMemberList())
870        {
871          if (member instanceof SourceLookupTable)
872            theirTables.add((SourceLookupTable)member);
873          else
874            theirSources.add((Source)member);
875        }
876        
877        return ourSources.equals(theirSources) &&
878               ourTables.equals(theirTables);
879      }
880    
881      /** Returns a hash code value for this group. */
882      @Override
883      public int hashCode()
884      {
885        //Taken from the Effective Java book by Joshua Bloch.
886        //The constants 17 & 37 are arbitrary & carry no meaning.
887        int result = 17;
888        
889        //NOTE: Keep this method in synch w/ equals
890        
891        result = 37 * result + getName().hashCode();
892        result = 37 * result + new Boolean(getNameIsLocked()).hashCode();
893    
894        //Put into same order as the equals method
895        ArrayList<Source>            sources = new ArrayList<Source>();
896        ArrayList<SourceLookupTable> tables  = new ArrayList<SourceLookupTable>();
897        
898        for (SourceCatalogEntry member : this.getInternalMemberList())
899        {
900          if (member instanceof SourceLookupTable)
901            tables.add((SourceLookupTable)member);
902          else
903            sources.add((Source)member);
904        }
905        
906        result = 37 * result + sources.hashCode();
907        result = 37 * result + tables.hashCode();
908        
909        return result;
910      }
911    
912      //============================================================================
913      // HELPER CLASSES
914      //============================================================================
915    
916      /**
917       * This listener is used only for groups whose catalog is null.
918       * UPDATE: This listener CAN be used by a group whose catalog is non-null.
919       *         That situation is intended to occur only for the main group of a
920       *         catalog.
921       */
922      private class Listener implements SourceTableListener
923      {
924        //Technically, we don't need to have this client in order to
925        //access its variables -- it just makes the code clearer.
926        private SourceGroup client;
927        private boolean     checkForNullCatalog;
928        
929        Listener(SourceGroup group, boolean checkForNullCatalog)
930        {
931          this.client = group;
932          this.checkForNullCatalog = checkForNullCatalog;
933        }
934        
935        public void sourceAdded(Source source, SourceLookupTable table)
936        {
937          if (checkForNullCatalog)
938            assertNullCatalog();
939    
940          assertHaveTable(table);
941          
942          if (!client.contains(source))
943            client.add(source);
944        }
945        
946        public void sourcesAdded(Collection<Source> source, SourceLookupTable table)
947        { 
948          if (checkForNullCatalog)
949            assertNullCatalog();
950          
951          assertHaveTable(table);
952    
953          for (Source s : source)
954            if (!client.contains(s))
955              client.add(s);
956        }
957        
958        public void sourceRemoved(Source source, SourceLookupTable table)
959        { 
960          //Intentionally doing nothing.
961          //Just because a source was removed from one of our client's tables does
962          //NOT mean the source should be removed from the client group.  It might
963          //be that someone added this source directly to the group, not just via
964          //the add of the table.
965        }
966        
967        public void entriesCleared(SourceLookupTable table)
968        { 
969          //Intentionally doing nothing.
970          //See comments in sourceRemoved, above.
971        }
972        
973        private void assertNullCatalog()
974        {
975          if (client.getCatalog() != null)
976          {
977            StringBuilder errMsg = new StringBuilder("PROGRAMMER ERROR.  ");
978            errMsg.append("SourceGroup ").append(client.getName());
979            errMsg.append(" is listening to tables even though its catalog is null.");
980          
981            throw new RuntimeException(errMsg.toString());
982          }
983        }
984        
985        private void assertHaveTable(SourceLookupTable table)
986        {
987          if (!client.contains(table))
988          {
989            StringBuilder errMsg = new StringBuilder("PROGRAMMER ERROR.  ");
990            errMsg.append("SourceGroup ").append(client.getName());
991            errMsg.append(" is listening to a table that it doesn't own.");
992          
993            throw new RuntimeException(errMsg.toString());
994          }
995        }
996      }
997    
998      //============================================================================
999      // 
1000      //============================================================================
1001      /*
1002      //Test name comparator
1003      public static void main(String... args) throws Exception
1004      {
1005        List<SourceGroup> groups = new ArrayList<SourceGroup>();
1006        
1007        SourceGroup g;
1008        g = new SourceGroup();  g.setName("DEC +80");  groups.add(g);
1009        g = new SourceGroup();  g.setName("3C");  groups.add(g);
1010        g = new SourceGroup();  g.setName("Abc");  groups.add(g);
1011        g = new SourceGroup();  g.setName("DEC -80");  groups.add(g);
1012        g = new SourceGroup();  g.setName("RA 22");  groups.add(g);
1013        g = new SourceGroup();  g.setName("Moe");  groups.add(g);
1014        g = new SourceGroup();  g.setName("RA 13");  groups.add(g);
1015        g = new SourceGroup();  g.setName("Inky");  groups.add(g);
1016        g = new SourceGroup();  g.setName("DEC -00");  groups.add(g);
1017        g = new SourceGroup();  g.setName("Manny");  groups.add(g);
1018        g = new SourceGroup();  g.setName("Blinky");  groups.add(g);
1019        g = new SourceGroup();  g.setName("3C new");  groups.add(g);
1020        g = new SourceGroup();  g.setName("RA 01");  groups.add(g);
1021        g = new SourceGroup();  g.setName("DEC +30");  groups.add(g);
1022        g = new SourceGroup();  g.setName("DEC +00");  groups.add(g);
1023        g = new SourceGroup();  g.setName("Jack");  groups.add(g);
1024        g = new SourceGroup();  g.setName("RA 11");  groups.add(g);
1025        g = new SourceGroup();  g.setName("Pinky");  groups.add(g);
1026        g = new SourceGroup();  g.setName("Clyde");  groups.add(g);
1027        
1028        for (SourceGroup grp : groups)
1029          System.out.println(grp.getName());
1030        
1031        Collections.sort(groups, SourceGroup.getNameComparator());
1032        System.out.println();
1033        
1034        for (SourceGroup grp : groups)
1035          System.out.println(grp.getName());
1036      }
1037      */
1038    }
1039    
1040    class SourceGroupNameComparator implements Comparator<SourceGroup>
1041    {
1042      private static final String RA  = "RA ";
1043      private static final String DEC = "DEC ";
1044      
1045      //Sort by name EXCEPT put Dec & RA groups last
1046      public int compare(SourceGroup g1, SourceGroup g2)
1047      {
1048        String name1 = g1.getName();
1049        String name2 = g2.getName();
1050        
1051        //RA groups come last
1052        if (name1.startsWith(RA) && !name2.startsWith(RA))  return +1;
1053        if (!name1.startsWith(RA) && name2.startsWith(RA))  return -1;
1054        
1055        //DEC groups come before only the RA groups
1056        if (name1.startsWith(DEC))
1057        {
1058          if ( name2.startsWith(RA) )  return -1;
1059          if (!name2.startsWith(DEC))  return +1;
1060          else                         return compareDec(name1, name2);
1061        }
1062        if (name2.startsWith(DEC))
1063        {
1064          if ( name1.startsWith(RA) )  return +1;
1065          if (!name1.startsWith(DEC))  return -1;
1066        }
1067        
1068        //At this point either:
1069        // 1. Both names start with "RA " or
1070        // 2. Neither name starts w/ "DEC " or "RA "
1071        return name1.compareTo(name2);
1072      }
1073    
1074      private int compareDec(String dec1, String dec2)
1075      {
1076        boolean dec1IsNeg = (dec1.charAt(DEC.length()) == '-');
1077        boolean dec2IsNeg = (dec2.charAt(DEC.length()) == '-');
1078        
1079        //See if we have opposite signs
1080        if ( dec1IsNeg && !dec2IsNeg)  return -1;
1081        if (!dec1IsNeg &&  dec2IsNeg)  return +1;
1082        
1083        int natural = dec1.compareTo(dec2);
1084        
1085        return dec1IsNeg ? -natural : natural;
1086      }
1087    }