001    package edu.nrao.sss.model.source;
002    
003    import java.io.FileNotFoundException;
004    import java.io.Reader;
005    import java.io.Writer;
006    import java.util.ArrayList;
007    import java.util.Arrays;
008    import java.util.Date;
009    import java.util.HashMap;
010    import java.util.HashSet;
011    import java.util.List;
012    
013    import javax.xml.bind.JAXBException;
014    import javax.xml.bind.annotation.XmlElement;
015    import javax.xml.bind.annotation.XmlElementRef;
016    import javax.xml.bind.annotation.XmlElementWrapper;
017    import javax.xml.bind.annotation.XmlRootElement;
018    import javax.xml.bind.annotation.XmlType;
019    import javax.xml.stream.XMLStreamException;
020    
021    import edu.nrao.sss.catalog.Catalog;
022    import edu.nrao.sss.measure.ArcUnits;
023    import edu.nrao.sss.model.RepositoryException;
024    import edu.nrao.sss.model.UserAccountable;
025    import edu.nrao.sss.util.Filter;
026    import edu.nrao.sss.util.Identifiable;
027    import edu.nrao.sss.util.JaxbUtility;
028    import edu.nrao.sss.util.StringUtil;
029    
030    /**
031     * A catalog of {@link Source}s.
032     * <p>
033     * Each entry in a catalog is either a {@code Source} or
034     * {@link SourceLookupTable}.  This catalog also supports
035     * the notion of {@link SourceGroup}s, which serve to
036     * associate sources with similar traits with one another.</p>
037     * <p>
038     * <b>Version Info:</b>
039     * <table style="margin-left:2em">
040     *   <tr><td>$Revision: 2313 $</td></tr>
041     *   <tr><td>$Date: 2009-05-20 15:00:52 -0600 (Wed, 20 May 2009) $</td></tr>
042     *   <tr><td>$Author: btruitt $</td></tr>
043     * </table></p>
044     *  
045     * @author David M. Harland
046     * @since 2006-09-15
047     */
048    @XmlRootElement
049    @XmlType(propOrder={"owner",
050                        "createdBy","createdOn","lastUpdatedBy","lastUpdatedOn",
051                        "xmlSources", "xmlSourceTables", "sourceGroups"
052                        })
053    public class SourceCatalog
054      extends Catalog<SourceCatalogEntry, SourceGroup, SourceCatalog>
055      implements Identifiable, UserAccountable, SourceProvider
056    {
057      //USER TRACKING
058      private Long owner;          //This is a user ID
059      private Long createdBy;      //This is a user ID
060      private Date createdOn;
061      private Long lastUpdatedBy;  //This is a user ID
062      private Date lastUpdatedOn;
063      
064      //NON-PERSISTED PROPERTIES
065      private SourceTableListener tableListener;
066    
067      /** Creates a new catalog with a default name. */
068      public SourceCatalog()
069      {
070        this(null);
071      }
072      
073      /**
074       * Creates a new catalog with the given name.
075       * 
076       * @param nameOfCatalog the name of this catalog.  If this value is
077       *                      <i>null</i>, this catalog will be given a
078       *                      default name.
079       */
080      public SourceCatalog(String nameOfCatalog)
081      {
082        super(nameOfCatalog);
083        
084        owner         = UserAccountable.NULL_USER_ID;
085        createdBy     = UserAccountable.NULL_USER_ID;
086        createdOn     = new Date();
087        lastUpdatedBy = UserAccountable.NULL_USER_ID;
088        lastUpdatedOn = new Date();
089      }
090      
091      /** Returns {@link Identifiable#UNIDENTIFIED}. */
092      @Override
093      protected long getIdOfUnidentified()
094      {
095        return Identifiable.UNIDENTIFIED;
096      }
097    
098      @Override
099      protected SourceGroup createMainGroup()
100      {
101        setReservedGroupName("All Sources");
102        
103        SourceGroup mainSourceGroup = new SourceGroup(this);
104        
105        tableListener = mainSourceGroup.getTableListener();
106        
107        return mainSourceGroup;
108      }
109    
110      @Override
111      public SourceGroup createGroup()
112      {
113        return new SourceGroup();
114      }
115      
116      SourceTableListener getTableListener()
117      {
118        return tableListener;
119      }
120    
121      /**
122       * Resets this catalog's ID, and the IDs of all its contents,
123       * to a value that represents the unidentified state.
124       * <p>
125       * This method is useful for preparing a catalog for storage in a database.
126       * The ID property (as of now, though this may change in the future) is
127       * used by our persistence mechanism to identify objects.  If you are
128       * persisting this catalog for the first time, you may need to call
129       * this method before performing a save.  This is especially true if
130       * you have created this source from XML, as the XML unmarshalling
131       * brings along the ID property.</p> 
132       */
133      public void clearId()
134      {
135        super.clearId();
136        
137        for (SourceCatalogEntry entry : getInternalItemList())
138          entry.clearId();
139      }
140    
141      //============================================================================
142      // ADDING ENTRIES & GROUPS 
143      //============================================================================
144    
145      /**
146       * Adds a new entry to this catalog.
147       * 
148       * @param newItem a new entry for this catalog.
149       * 
150       * @return <i>true</i> if this catalog changed as a result of the call.
151       *         Reasons for a return value of <i>false</i>:
152       *         <ol>
153       *           <li>{@code newEntry} is <i>null</i></li>
154       *           <li>{@code newEntry} is already contained in this catalog</li>
155       *         </ol>
156       */
157      @Override
158      public SourceCatalogEntry addItem(SourceCatalogEntry newItem)
159      {
160        SourceCatalogEntry addedItem = super.addItem(newItem);
161    
162        //Special logic for handling lookup tables
163        if (addedItem instanceof SourceLookupTable)
164          addSourcesFrom((SourceLookupTable)addedItem);
165        
166        return addedItem;
167      }
168      
169      //Adds sources from table to this catalog, or replaces the table's source
170      //with a reference to an equal source that is already in this catalog.
171      void addSourcesFrom(SourceLookupTable table)
172      {
173        List<SourceCatalogEntry> currentSources = getItems();
174    
175        //For each source in the table, either replace the table's source
176        //with an EQUAL source from our current entries, or add the new
177        //source to our list of entries.
178        for (Date key : table.getKeySet())
179        {
180          Source tableSource = table.get(key);
181    
182          int index = currentSources.indexOf(tableSource);
183    
184          if (index >= 0)
185          {
186            //Catalog has EQUAL source.  If it is not IDENTICAL source,
187            //update the table so that it refers to the existing member.
188            Source currentMember = (Source)currentSources.get(index);
189            if (tableSource != currentMember)
190              table.put(key, currentMember);
191          }
192          else
193          {
194            //This catalog has no equal source, so add it to this catalog.
195            super.addItem(tableSource);
196          }
197        }
198      }
199      
200      //These methods are here for database persistence
201      @SuppressWarnings("unused")
202      private List<SourceCatalogEntry> getEntries()
203      {
204        return getInternalItemList();
205      }
206      @SuppressWarnings("unused")
207      private void setEntries(List<SourceCatalogEntry> replacementList)
208      {
209        setInternalItemList(replacementList);
210      }
211    
212      //============================================================================
213      // REMOVING ENTRIES & GROUPS 
214      //============================================================================
215    
216      @Override
217      public SourceCatalogEntry removeItem(SourceCatalogEntry entry)
218      {
219        SourceCatalogEntry formerEntry = super.removeItem(entry);
220        
221        finishRemovingItem(formerEntry);
222        
223        return formerEntry;
224      }
225      
226      @Override
227      public SourceCatalogEntry removeItem(int index)
228      {
229        SourceCatalogEntry formerEntry = super.removeItem(index);
230        
231        finishRemovingItem(formerEntry);
232        
233        return formerEntry;
234      }
235      
236      //Deals with removal of source from tables.
237      private void finishRemovingItem(SourceCatalogEntry formerItem)
238      {
239        if ((formerItem != null) && (formerItem instanceof Source))
240        {
241          try
242          {
243            for (SourceLookupTable table : getSourceTables())
244              table.removeValue((Source)formerItem);
245          }
246          catch (RepositoryException ex)
247          {
248            //Can't happen, but just in case...
249            throw new RuntimeException("PROGRAMMER ERROR", ex);
250          }
251        }
252      }
253      
254      //============================================================================
255      // AUTOMATIC CREATION OF GROUPS 
256      //============================================================================
257      
258      /**
259       * Returns a list of source groups based on the right ascensions of the
260       * sources in this catalog.
261       * <p>
262       * Only those groups that have sources in the appropriate RA range are
263       * held by the returned list.  Each group holds one hour-angle of sources.
264       * This method does <i>not</i> automatically add the returned groups to
265       * this catalog.</p> 
266       * 
267       * @return a list of source groups based on the right ascensions of the
268       *         sources in this catalog.
269       *         
270       * @see #makeDeclinationGroups()
271       * @see #updateRaAndDecGroups()
272       */
273      public List<SourceGroup> makeRightAscensionGroups()
274      {
275        SourceGroup[] groups = new SourceGroup[24];
276        
277        int groupCount = 0;
278        
279        for (SourceCatalogEntry entry : getItems())
280        {
281          if (!(entry instanceof Source))
282            continue;
283          
284          Source source = (Source)entry;
285          
286          //TODO need to convert to Equatorial to ensure RA / Dec
287          int hour = source.getCentralSubsource()
288                           .getPosition()
289                           .getLongitude().toUnits(ArcUnits.HOUR).intValue();
290          
291          SourceGroup group = groups[hour];
292    
293          //Create new group first time we see this hour
294          if (group == null)
295          {
296            groupCount++;
297            
298            String name = "RA ";
299            if (hour < 10)
300              name += "0";
301            name += Integer.toString(hour);
302    
303            group = new SourceGroup();
304            group.setName(name);
305            groups[hour] = group;
306          }
307          
308          group.add(source);
309        }
310        
311        List<SourceGroup> groupList = new ArrayList<SourceGroup>(groupCount);
312        
313        //Return list containing only populated groups
314        for (SourceGroup group : groups)
315          if (group != null)
316            groupList.add(group);
317    
318        return groupList;
319      }
320    
321      /**
322       * Returns a list of source groups based on the declinations of the sources
323       * in this catalog.
324       * <p>
325       * Only those groups that have sources in the appropriate declination range
326       * are held by the returned list.  Each group holds a declination range of
327       * ten degrees.
328       * This method does <i>not</i> automatically add the returned groups to
329       * this catalog.</p> 
330       * 
331       * @return a list of source groups based on the declinations of the sources
332       *         in this catalog.
333       *         
334       * @see #makeRightAscensionGroups()
335       * @see #updateRaAndDecGroups()
336       */
337      public List<SourceGroup> makeDeclinationGroups()
338      {
339        SourceGroup[] groups = new SourceGroup[20];
340        
341        final String[] names =
342        {"-90", "-80", "-70", "-60", "-50", "-40", "-30", "-20", "-10", "-00",
343         "+00", "+10", "+20", "+30", "+40", "+50", "+60", "+70", "+80", "+90"};
344    
345        int groupCount = 0;
346        
347        for (SourceCatalogEntry entry : getItems())
348        {
349          if (!(entry instanceof Source))
350            continue;
351          
352          Source source = (Source)entry;
353          
354          //Indexing into groups and names arrays
355          //TODO need to convert to Equatorial to ensure RA / Dec
356          double degrees = source.getCentralSubsource()
357                                 .getPosition()
358                                 .getLatitude().toUnits(ArcUnits.DEGREE).doubleValue();
359          
360          int signum = (int)Math.signum(degrees);
361          int decade = (int)Math.floor(Math.abs(degrees) / 10.0);
362          int index;
363          
364          if (signum >= 0)  index = 10 + decade;
365          else              index =  9 - decade;
366    
367          SourceGroup group = groups[index];
368          
369          //Create new group first time we see this index
370          if (group == null)
371          {
372            groupCount++;
373            group = new SourceGroup();
374            group.setName("DEC " + names[index]);
375            groups[index] = group;
376          }
377          
378          group.add(source);
379        }
380        
381        List<SourceGroup> groupList = new ArrayList<SourceGroup>(groupCount);
382        
383        //Return list containing only populated groups
384        for (SourceGroup group : groups)
385          if (group != null)
386            groupList.add(group);
387    
388        return groupList;
389      }
390     
391      /**
392       * Updates this catalog with a new collection of right ascension and
393       * declination groups.
394       * <p>
395       * This method is thorough, but not efficient.  It works by 
396       * creating brand new RA and Dec groups, removing the current
397       * RA and Dec groups (if any), and then adding the new groups.
398       * If you want to make RA and Dec groups for the first time, if you
399       * are adding several sources at once, or if you suspect that the
400       * sources' positions may have changed, calling this method is
401       * probably the right thing to do.  If you are adding just one new
402       * source, though, and if you know its RA and Dec, it could be more
403       * efficient to add it directly to the RA group and Dec group to
404       * which it belongs on your own.</p>
405       * 
406       * @see #makeDeclinationGroups()
407       * @see #makeRightAscensionGroups()
408       */
409      public void updateRaAndDecGroups()
410      {
411        //Create a list of new RA & Dec groups
412        List<SourceGroup> newGroups = makeDeclinationGroups();
413        newGroups.addAll(makeRightAscensionGroups());
414        
415        //Gather the names of the new groups
416        HashSet<String> newGroupNames = new HashSet<String>();
417        
418        for (SourceGroup newGroup : newGroups)
419          newGroupNames.add(newGroup.getName());
420        
421        //From this catalog, remove every group that has the same name as
422        //one of the new groups
423        for (SourceGroup currentGroup : getGroups())
424          if (newGroupNames.contains(currentGroup.getName()))
425            removeGroup(currentGroup);
426        
427        //Add the new groups
428        addGroups(newGroups);
429      }
430      
431      //============================================================================
432      // INTERFACE SourceProvider 
433      //============================================================================
434    
435      public List<Source> getSources(Filter<Source> filter)
436        throws RepositoryException
437      {
438        return toGroup().getSources(filter);
439      }
440    
441      public List<Source> getSources()
442        throws RepositoryException
443      {
444        return toGroup().getSources();
445      }
446    
447      public List<SourceLookupTable> getSourceTables()
448        throws RepositoryException
449      {
450        return toGroup().getSourceTables();
451      }
452    
453      public Source findSourceById(long id)
454        throws RepositoryException
455      {
456        return toGroup().findSourceById(id);
457      }
458    
459      public List<Source> findSourceByName(String name)
460        throws RepositoryException
461      {
462        return toGroup().findSourceByName(name);
463      }
464    
465      public SourceLookupTable findSourceTableById(long id)
466        throws RepositoryException
467      {
468        return toGroup().findSourceTableById(id);
469      }
470    
471      public List<SourceLookupTable> findSourceTableByName(String name)
472        throws RepositoryException
473      {
474        return toGroup().findSourceTableByName(name);
475      }
476    
477      //============================================================================
478      // INTERFACE UserAccountable & OWNERSHIP
479      //============================================================================
480      
481      /**
482       * Sets the ID of the user who owns this catalog.
483       * 
484       * @param userId the ID of the user who owns this catalog.  If this value is
485       *               <i>null</i> it will be replaced with
486       *               {@link UserAccountable#NULL_USER_ID}.
487       */
488      public void setOwner(Long userId)
489      {
490        owner = (userId == null) ? UserAccountable.NULL_USER_ID : userId;
491      }
492      
493      public void setCreatedBy(Long userId)
494      {
495        createdBy = (userId == null) ? UserAccountable.NULL_USER_ID : userId;
496      }
497      
498      public void setCreatedOn(Date d)
499      {
500        if (d != null)
501          createdOn = d;
502      }
503    
504      public void setLastUpdatedBy(Long userId)
505      {
506        lastUpdatedBy = (userId == null) ? UserAccountable.NULL_USER_ID : userId;
507      }
508    
509      public void setLastUpdatedOn(Date d)
510      {
511        if (d != null)
512          lastUpdatedOn = d;
513      }
514    
515      /**
516       * Returns the ID of the user who owns this catalog.
517       * @return the ID of the user who owns this catalog.
518       */
519      public Long getOwner()          { return owner;         }
520      public Long getCreatedBy()      { return createdBy;     }
521      public Date getCreatedOn()      { return createdOn;     }
522      public Long getLastUpdatedBy()  { return lastUpdatedBy; }
523      public Date getLastUpdatedOn()  { return lastUpdatedOn; }
524    
525      //============================================================================
526      // TEXT
527      //============================================================================
528    
529      /**
530       * Creates a new catalog from the XML data in the given file.
531       * 
532       * @param xmlFile the name of an XML file.  This method will attempt to locate
533       *                the file by using {@link Class#getResource(String)}.
534       *                
535       * @return a new catalog from the XML data in the given file.
536       * 
537       * @throws FileNotFoundException if the XML file cannot be found.
538       * 
539       * @throws JAXBException if the schema file used (if any) is malformed, if
540       *           the XML file cannot be read, or if the XML file is not
541       *           schema-valid.
542       * 
543       * @throws XMLStreamException if there is a problem opening the XML file,
544       *           if the XML is not well-formed, or for some other
545       *           "unexpected processing conditions".
546       */
547      public static SourceCatalog fromXml(String xmlFile)
548        throws JAXBException, XMLStreamException, FileNotFoundException
549      {
550        return JaxbUtility.getSharedInstance()
551                          .xmlFileToObject(xmlFile, SourceCatalog.class);
552      }
553      
554      /**
555       * Creates a new catalog based on the XML data read from {@code reader}.
556       * 
557       * @param reader the source of the XML data.
558       *               If this value is <i>null</i>, <i>null</i> is returned.
559       *               
560       * @return a new catalog based on the XML data read from {@code reader}.
561       * 
562       * @throws XMLStreamException if the XML is not well-formed,
563       *           or for some other "unexpected processing conditions".
564       *           
565       * @throws JAXBException if anything else goes wrong during the
566       *           transformation.
567       */
568      public static SourceCatalog fromXml(Reader reader)
569        throws JAXBException, XMLStreamException
570      {
571        return JaxbUtility.getSharedInstance()
572                          .readObjectAsXmlFrom(reader, SourceCatalog.class, null);
573      }
574      
575      /**
576       * Returns an XML representation of this catalog.
577       * @return an XML representation of this catalog.
578       * @throws JAXBException if anything goes wrong during the conversion to XML.
579       */
580      @Override
581      public String toXml()
582        throws JAXBException
583      {
584        giveItemsCatalogIds();
585        
586        return super.toXml();
587      }
588      
589      /**
590       * Writes an XML representation of this catalog to {@code writer}.
591       * @param writer the device to which XML is written.
592       * @throws JAXBException if anything goes wrong during the conversion to XML.
593       */
594      @Override
595      public void writeAsXmlTo(Writer writer)
596        throws JAXBException
597      {
598        giveItemsCatalogIds();
599        
600        super.writeAsXmlTo(writer);
601      }
602      
603      /**
604       * Creates an ID for each source in this catalog
605       * that is unique WITHIN this catalog.  This was done in order to be
606       * more user-friendly to people creating or manipulating the XML files
607       * by hand.  HOWEVER, it has the drawback that this does not create
608       * a universely unique ID.  Since our use case calls for the
609       * import and export of one catalog at a time, this is fine.
610       * If we want to be more general, though, we can leave the value of
611       * xmlId alone.  Source's constructor gave this property a
612       * UNIVERSALLY unique ID.  We would then eliminate this method
613       * and the overrides of the XML methods above.
614       */
615      private void giveItemsCatalogIds()
616      {
617        HashMap<String, Integer> nameMap = new HashMap<String, Integer>();
618        
619        StringUtil util = StringUtil.getInstance();
620        
621        for (SourceCatalogEntry e : getInternalItemList())
622        {
623          String name = util.toXmlNCName(e.getName(), '_');
624          int count = nameMap.containsKey(name) ? nameMap.get(name) + 1 : 1;
625          nameMap.put(name, count);
626          
627          if (e instanceof Source)
628          {
629            ((Source)e).xmlId = (count == 1) ? name : name + "_" + count;
630          }
631          else
632          {
633            SourceLookupTable table = (SourceLookupTable)e;
634            table.xmlId = (count == 1) ? name : name + "_" + count;
635            table.useSourceReferencesInXml = true;
636          }
637        }
638      }
639    
640      //----------------------------------------------------------------------------
641      // XML Helpers
642      //----------------------------------------------------------------------------
643      
644      //These methods are here solely to help JAXB do its thing.
645    
646      @XmlElementWrapper(name="sources")
647      @XmlElement(name="source")
648      @SuppressWarnings("unused")
649      private Source[] getXmlSources()
650      {
651        ArrayList<Source> sources = new ArrayList<Source>();
652        
653        for (SourceCatalogEntry e : getInternalItemList())
654          if (e instanceof Source)
655            sources.add((Source)e);
656        
657        return sources.toArray(new Source[sources.size()]);
658      }
659      @SuppressWarnings("unused")
660      private void setXmlSources(Source[] sources)
661      {
662        getInternalItemList().addAll(Arrays.asList(sources));
663      }
664      
665      @XmlElementWrapper(name="sourceTables")
666      @XmlElement(name="sourceLookupTable")
667      @SuppressWarnings("unused")
668      private SourceLookupTable[] getXmlSourceTables()
669      {
670        ArrayList<SourceLookupTable> tables = new ArrayList<SourceLookupTable>();
671        
672        for (SourceCatalogEntry e : getInternalItemList())
673          if (e instanceof SourceLookupTable)
674            tables.add((SourceLookupTable)e);
675        
676        return tables.toArray(new SourceLookupTable[tables.size()]);
677      }
678      @SuppressWarnings("unused")
679     private void setXmlSourceTables(SourceLookupTable[] sourceTables)
680      {
681        getInternalItemList().addAll(Arrays.asList(sourceTables));
682      }
683    
684      @XmlElementWrapper
685      @XmlElementRef(name="sourceGroup")
686      @SuppressWarnings("unused")
687      private List<SourceGroup> getSourceGroups()
688      {
689        return getInternalGroupList();
690      }
691      @SuppressWarnings("unused")
692      private void setSourceGroups(List<SourceGroup> replacementList)
693      {
694        setInternalGroupList(replacementList);
695      }
696        
697      //============================================================================
698      // 
699      //============================================================================
700      
701      @Override
702      public SourceCatalog clone()
703      {
704        SourceCatalog clone = null;
705    
706        try
707        {
708          clone = (SourceCatalog)super.clone();
709          
710          clone.createdOn     = (Date)this.createdOn.clone();
711          clone.lastUpdatedOn = (Date)this.lastUpdatedOn.clone();
712        }
713        catch (Exception ex)
714        {
715          throw new RuntimeException(ex);
716        }
717        
718        return clone;
719      }
720    
721      //============================================================================
722      // 
723      //============================================================================
724      /*
725      public static void main(String[] args)
726      {
727        SourceBuilder builder = new SourceBuilder();
728        
729        builder.setIdentifiers(true);
730        SourceCatalog catalog = builder.makeCatalog("Random");
731        
732        try
733        {
734          System.out.println(catalog.toXml());
735        }
736        catch (JAXBException ex)
737        {
738          System.out.println("Trouble w/ catalog.toXml.  Msg:");
739          System.out.println(ex.getMessage());
740          ex.printStackTrace();
741          
742          System.out.println("Attempting to write XML w/out schema verification:");
743          JaxbUtility.getSharedInstance().setLookForDefaultSchema(false);
744          try
745          {
746            System.out.println(catalog.toXml());
747          }
748          catch (JAXBException ex2)
749          {
750            System.out.println("Still had trouble w/ catalog.toXml.  Msg:");
751            System.out.println(ex2.getMessage());
752            ex2.printStackTrace();
753          }
754        }
755        try
756        {
757          java.io.FileWriter writer =
758            new java.io.FileWriter("/export/home/calmer/dharland/JUNK/SourceCatalog.xml");
759          catalog.writeAsXmlTo(writer);
760        }
761        catch (Exception ex3)
762        {
763          System.out.println(ex3.getMessage());
764          ex3.printStackTrace();
765        }
766      }
767      */
768      /*
769      public static void main(String... args) throws Exception
770      {
771        java.io.FileReader reader =
772          new java.io.FileReader("/export/home/calmer/dharland/JUNK/SourceCatalog.xml");
773        
774        SourceCatalog cat = SourceCatalog.fromXml(reader);
775        
776        java.io.FileWriter writer =
777          new java.io.FileWriter("/export/home/calmer/dharland/JUNK/SourceCatalog-OUT.xml");
778        cat.writeAsXmlTo(writer);
779      }
780      */
781    }