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.net.URL;
007    import java.util.ArrayList;
008    import java.util.Date;
009    import java.util.HashSet;
010    import java.util.List;
011    import java.util.Map;
012    import java.util.SortedMap;
013    import java.util.SortedSet;
014    import java.util.TreeMap;
015    import java.util.TreeSet;
016    import java.util.UUID;
017    
018    import javax.xml.bind.JAXBException;
019    import javax.xml.bind.annotation.XmlAttribute;
020    import javax.xml.bind.annotation.XmlElement;
021    import javax.xml.bind.annotation.XmlElementWrapper;
022    import javax.xml.bind.annotation.XmlID;
023    import javax.xml.bind.annotation.XmlRootElement;
024    import javax.xml.bind.annotation.XmlType;
025    import javax.xml.stream.XMLStreamException;
026    
027    import edu.nrao.sss.astronomy.CoordinateConversionException;
028    import edu.nrao.sss.astronomy.SkyPosition;
029    import edu.nrao.sss.geom.EarthPosition;
030    import edu.nrao.sss.measure.Frequency;
031    import edu.nrao.sss.measure.LinearVelocity;
032    import edu.nrao.sss.model.UserAccountable;
033    import edu.nrao.sss.model.resource.ReceiverBand;
034    import edu.nrao.sss.model.resource.TelescopeConfiguration;
035    import edu.nrao.sss.util.Identifiable;
036    import edu.nrao.sss.util.JaxbUtility;
037    import edu.nrao.sss.util.StringUtil;
038    
039    /**
040     * An astronomical emitter of radio waves.
041     * <p>
042     * <b>Version Info:</b>
043     * <table style="margin-left:2em">
044     *   <tr><td>$Revision: 1709 $</td></tr>
045     *   <tr><td>$Date: 2008-11-14 11:22:37 -0700 (Fri, 14 Nov 2008) $</td></tr>
046     *   <tr><td>$Author: dharland $ (last person to modify)</td></tr>
047     * </table></p>
048     *  
049     * @author David M. Harland
050     * @since 2006-02-24
051     */
052    @XmlRootElement
053    @XmlType(propOrder={"name",
054                        "createdBy","createdOn","lastUpdatedBy","lastUpdatedOn",
055                        "originOfInformation",
056                        "aliases", "xmlUserDefinedValues", "imageLinks",
057                        "historicalRecords", "notes", "links",
058                        "subsources"})
059    public class Source
060      implements SourceCatalogEntry
061    {
062      private static final String NO_ORIGIN = "";
063      private static final String NO_NAME   = "[NEW]";
064      
065      public static final String CENTER_POSITION_NAME = "CENTER";
066    
067      //IDENTIFICATION
068      private long   id;
069      private String name;
070    
071      @XmlAttribute(required=true)
072      @XmlID
073      String xmlId;
074      
075      //USER TRACKING
076      private Long createdBy;      //This is a user ID
077      private Date createdOn;
078      private Long lastUpdatedBy;  //This is a user ID
079      private Date lastUpdatedOn;
080      
081      //COMPONENT SOURCES
082      private SortedSet<Subsource> subsources;
083      
084      //OTHER PROPERTIES
085      private String                    originOfInformation;
086      private SortedMap<String, String> userDefinedValues;
087      private List<SourceImageLink>     imageLinks;
088      private List<String>              historicalRecords;
089      private List<String>              notes;
090      private List<URL>                 links;
091    
092      @XmlElement(name="alias")
093      private List<String> aliases;
094      
095      /**
096       * Creates a new unnamed source.
097       * The new source will have one subsource.  The name of that
098       * subsource will be {@link #CENTER_POSITION_NAME}.
099       */ 
100      public Source()
101      {
102        subsources        = new TreeSet<Subsource>();
103        aliases           = new ArrayList<String>();
104        userDefinedValues = new TreeMap<String, String>();
105        imageLinks        = new ArrayList<SourceImageLink>();
106        historicalRecords = new ArrayList<String>();
107        notes             = new ArrayList<String>();
108        links             = new ArrayList<URL>();
109        
110        initialize();
111        
112        addSubsource(new Subsource(CENTER_POSITION_NAME));
113      }
114      
115      /**
116       * Creates a new instance with the given name.
117       * The new source will have one subsource.  The name of that
118       * subsource will be {@link #CENTER_POSITION_NAME}.
119       */ 
120      public Source(String name)
121      {
122        this();
123        setName(name);
124      }
125      
126      /** Initializes the instance variables of this class.  */
127      private void initialize()
128      {
129        id    = Identifiable.UNIDENTIFIED;
130        name  = NO_NAME;
131        xmlId = "source-" + UUID.randomUUID().toString();
132      
133        createdBy     = UserAccountable.NULL_USER_ID;
134        createdOn     = new Date();
135        lastUpdatedBy = UserAccountable.NULL_USER_ID;
136        lastUpdatedOn = new Date();
137        
138        originOfInformation = NO_ORIGIN;
139      }
140      
141      /**
142       *  Resets this source to its initial state.  A reset source has the same
143       *  state as a new source. 
144       */
145      public void reset()
146      {
147        aliases.clear();
148        subsources.clear();
149        userDefinedValues.clear();
150        imageLinks.clear();
151        historicalRecords.clear();
152        notes.clear();
153        links.clear();
154    
155        initialize();
156      }
157    
158      /* (non-Javadoc)
159       * @see edu.nrao.sss.model.source.SourceCatalogEntry#get(java.util.Date)
160       */
161      public Source get(Date pointInTime)  { return this; }
162      
163      /**
164       * Returns <i>true</i> if the position of this source at time T
165       * could be different than its position at time U &ne; T.
166       * <p>
167       * The determination of motion will be made with respect to the
168       * coordinate system in which the position of this source is expressed.
169       * For example, a position that is expressed in the equatorial system,
170       * and that is holding steady at its position in that system, will said
171       * to be not moving, even though in other coordinate systems it may be
172       * moving quite rapidly.</p>
173       * <p>
174       * This is a convenience method that is equivalent to calling
175       * <tt>getCentralSubsource().isMoving()</tt>.</p>
176       * 
177       * @return <i>true</i> if this source is moving.
178       */
179      public boolean isMoving()  { return getCentralSubsource().isMoving(); }
180      
181      //============================================================================
182      // IDENTIFICATION
183      //============================================================================
184      
185      /* (non-Javadoc)
186       * @see edu.nrao.sss.model.util.Identifiable#getId()
187       */
188      @XmlAttribute
189      public Long getId()
190      {
191        return id;
192      }
193      
194      /* (non-Javadoc)
195       * @see SourceCatalogEntry#setId(java.lang.Long)
196       */
197      public void setId(Long id)
198      {
199        this.id = id;
200      }
201    
202      /* (non-Javadoc)
203       * @see edu.nrao.sss.model.source.SourceCatalogEntry#clearId()
204       */
205      public void clearId()
206      {
207        this.id = Identifiable.UNIDENTIFIED;
208        
209        for (Subsource ss : subsources)
210          ss.clearId();
211      }
212      
213      /**
214       * Sets the name of this source.
215       * <p>
216       * If {@code newName} is <i>null</i> or the empty string
217       * (<tt>""</tt>), the request to change the name will be
218       * denied and the current name will remain in place.</p>
219       * 
220       * @param newName the new name for this source.
221       */
222      public void setName(String newName)
223      {
224        if (newName != name && newName.length() > 0)
225          name = newName;
226      }
227      
228      /**
229       * Returns the name of this source.
230       * @return the name of this source.
231       */
232      public String getName()
233      {
234        return name;
235      }
236    
237      //============================================================================
238      // USER-DEFINED-VALUES
239      //============================================================================
240      
241      /**
242       * Returns a collection of user-defined key/value pairs.
243       * The returned map may be empty, but is guaranteed to be non-null.
244       * <p>
245       * Note that the returned map is the one actually held by this object.
246       * This means that any changes made to the returned map <i>will</i>
247       * be reflected in this source.  It also means that to add, delete, or change
248       * a keyword mapping, clients call this method and then operate directly on
249       * the returned map.</p>
250       * 
251       * @return a collection of user-defined key/value pairs.
252       */
253      public SortedMap<String, String> getUserDefinedValues()
254      {
255        return userDefinedValues;
256      }
257      
258      /**
259       * Returns text that can be used as a key for the {@link #getUserDefinedValues()
260       * user defined values map}.
261       * The returned key is used to get a VLA calibrator quality code and
262       * consists of some boiler plate text, a code for the receiver
263       * band, and a code for the telescope configuration.
264       * 
265       * @param band
266       *   the receiver band for which a key is desired.
267       *   
268       * @param cfg
269       *   the telescope configuration for which a key is desired.
270       *   
271       * @return
272       *   text that can be used as a user defined value key.
273       *   
274       * @since 2008-07-25
275       */
276      public static String makeUdbKeyVlaCalibQuality(ReceiverBand band,
277                                                    TelescopeConfiguration cfg)
278      {
279        return makeUdbKeyVlaCalibQuality(band.getDisplayName(), cfg.getCode());
280      }
281      
282      /**
283       * Returns text that can be used as a key for the {@link #getUserDefinedValues()
284       * user defined values map}.
285       * The returned key is used to get a VLA calibrator quality code and
286       * consists of some boiler plate text, a code for the receiver
287       * band, and a code for the telescope configuration.
288       * 
289       * @param bandCode
290       *   a code for a receiver band.  By convention this should be a value returned
291       *   by the {@link ReceiverBand#getDisplayName() getDisplayName() method} of
292       *   {@link ReceiverBand}.
293       *   
294       * @param teleCfgCode
295       *   a code for a telescope configuration.  By convention this should be a value
296       *   returned by the {@link TelescopeConfiguration#getCode() getCode method} of
297       *   {@link TelescopeConfiguration}.
298       *   
299       * @return
300       *   text that can be used as a user defined value key.
301       *   
302       * @since 2008-07-25
303       */
304      public static String makeUdbKeyVlaCalibQuality(String bandCode,
305                                                    String teleCfgCode)
306      {
307        StringBuilder buff = new StringBuilder("Quality.");
308        buff.append(bandCode).append("-band.");
309        buff.append(teleCfgCode).append("-cfg");
310        return buff.toString();
311      }
312      
313      /**
314       * Returns text that can be used as a key for the {@link #getUserDefinedValues()
315       * user defined values map}.
316       * The returned key is used to get a UV<sub>max</sub> value and
317       * consists of some boiler plate text and a code for the receiver band.
318       * 
319       * @param band
320       *   the receiver band for which a key is desired.
321       * 
322       * @return
323       *   text that can be used as a user defined value key.
324       *   
325       * @since 2008-07-25
326       */
327      public static String makeUdbKeyVlaUvMax(ReceiverBand band)
328      {
329        return makeUdbKeyVlaUvMax(band.getDisplayName());
330      }
331      
332      /**
333       * Returns text that can be used as a key for the {@link #getUserDefinedValues()
334       * user defined values map}.
335       * The returned key is used to get a UV<sub>max</sub> value and
336       * consists of some boiler plate text and a code for the receiver band.
337       * 
338       * @param bandCode
339       *   a code for a receiver band.  By convention this should be a value returned
340       *   by the {@link ReceiverBand#getDisplayName() getDisplayName() method} of
341       *   {@link ReceiverBand}.
342       * 
343       * @return
344       *   text that can be used as a user defined value key.
345       *   
346       * @since 2008-07-25
347       */
348      public static String makeUdbKeyVlaUvMax(String bandCode)
349      {
350        return "UV max, " + bandCode + " band";
351      }
352      
353      /**
354       * Returns text that can be used as a key for the {@link #getUserDefinedValues()
355       * user defined values map}.
356       * The returned key is used to get a UV<sub>min</sub> value and
357       * consists of some boiler plate text and a code for the receiver band.
358       * 
359       * @param band
360       *   the receiver band for which a key is desired.
361       * 
362       * @return
363       *   text that can be used as a user defined value key.
364       *   
365       * @since 2008-07-25
366       */
367      public static String makeUdbKeyVlaUvMin(ReceiverBand band)
368      {
369        return makeUdbKeyVlaUvMin(band.getDisplayName());
370      }
371      
372      /**
373       * Returns text that can be used as a key for the {@link #getUserDefinedValues()
374       * user defined values map}.
375       * The returned key is used to get a UV<sub>min</sub> value and
376       * consists of some boiler plate text and a code for the receiver band.
377       * 
378       * @param bandCode
379       *   a code for a receiver band.  By convention this should be a value returned
380       *   by the {@link ReceiverBand#getDisplayName() getDisplayName() method} of
381       *   {@link ReceiverBand}.
382       * 
383       * @return
384       *   text that can be used as a user defined value key.
385       *   
386       * @since 2008-07-25
387       */
388      public static String makeUdbKeyVlaUvMin(String bandCode)
389      {
390        return "UV min, " + bandCode + " band";
391      }
392    
393      //============================================================================
394      // OTHER PROPERTIES
395      //============================================================================
396    
397      /**
398       * Sets the origin of this source's information.
399       * @param origin the origin of this source's information.  A value of
400       *               <i>null</i> will be replaced by a non-null default value.
401       */
402      public void setOriginOfInformation(String origin)
403      {
404        originOfInformation = (origin == null) ? NO_ORIGIN : origin;
405      }
406      
407      /**
408       * Returns the origin of this source's information.
409       * @return the origin of this source's information.
410       */
411      public String getOriginOfInformation()
412      {
413        return originOfInformation;
414      }
415      
416      /**
417       * Returns a list of other names by which this source is known.
418       * The returned list may be empty, but is guaranteed to be non-null.
419       * <p>
420       * Note that the returned list is the one actually held by this object.
421       * This means that any changes made to the returned list <i>will</i>
422       * be reflected in this source.  It also means that to add or delete
423       * an alias, clients call this method and then operate directly on the
424       * returned list.</p>
425       * 
426       * @return a list of other names by which this source is known.
427       */
428      public List<String> getAliases()
429      {
430        return aliases;
431      }
432      
433      /**
434       * Returns a collection of links to images of this source.
435       * The returned list may be empty, but is guaranteed to be non-null.
436       * <p>
437       * Note that the returned list is the one actually held by this object.
438       * This means that any changes made to the returned list <i>will</i>
439       * be reflected in this source.  It also means that to add, delete, or change
440       * an image link, clients call this method and then operate directly on
441       * the returned list.</p>
442       * 
443       * @return a collection of user-defined key/value pairs.
444       */
445      @XmlElementWrapper
446      @XmlElement(name="imageLink")
447      public List<SourceImageLink> getImageLinks()
448      {
449        return imageLinks;
450      }
451      
452      /** This is here for mechanisms that need setX/getX pairs, such as JAXB. */
453      @SuppressWarnings("unused")
454      private void setImageLinks(List<SourceImageLink> replacementList)
455      {
456        imageLinks = (replacementList == null) ? new ArrayList<SourceImageLink>()
457                                               : replacementList;
458      }
459    
460      /**
461       * Returns one element of text for each historical record held by this source.
462       * Each record is free-form text, but most clients are expected to structure
463       * the text in some way, perhaps as XML or as delimited key=value pairs.
464       * <p>
465       * This method returns the list actually held by this {@code Source}, so
466       * any list manipulations may be performed by first fetching the list and
467       * then operating on it.</p>
468       * 
469       * @return a list of historical records for this source.
470       */
471      @XmlElementWrapper
472      @XmlElement(name="historicalRecord")
473      public List<String> getHistoricalRecords()
474      {
475        return historicalRecords;
476      }
477      
478      /** This is here for mechanisms that need setX/getX pairs, such as JAXB. */
479      @SuppressWarnings("unused")
480      private void setHistoricalRecords(List<String> replacementList)
481      {
482        historicalRecords = (replacementList == null) ? new ArrayList<String>()
483                                                      : replacementList;
484      }
485    
486      /**
487       * Returns a list of notes about this source.
488       * Each note is free-form text with no particular structure.
489       * <p>
490       * This method returns the list actually held by this {@code Source}, so
491       * any list manipulations may be performed by first fetching the list and
492       * then operating on it.</p>
493       * 
494       * @return a list of notes about this source.
495       */
496      @XmlElementWrapper
497      @XmlElement(name="note")
498      public List<String> getNotes()
499      {
500        return notes;
501      }
502      
503      /** This is here for mechanisms that need setX/getX pairs, such as JAXB. */
504      @SuppressWarnings("unused")
505      private void setNotes(List<String> replacementList)
506      {
507        notes = (replacementList == null) ? new ArrayList<String>()
508                                          : replacementList;
509      }
510    
511      /**
512       * Returns a list of URL links to more information about this source.
513       * <p>
514       * This method returns the list actually held by this {@code Source}, so
515       * any list manipulations may be performed by first fetching the list and
516       * then operating on it.</p>
517       * 
518       * @return a list of links that refer to this source.
519       */
520      @XmlElementWrapper
521      @XmlElement(name="link")
522      public List<URL> getLinks()
523      {
524        return links;
525      }
526      
527      /** This is here for mechanisms that need setX/getX pairs, such as JAXB. */
528      @SuppressWarnings("unused")
529      private void setLinks(List<URL> replacementList)
530      {
531        links = (replacementList == null) ? new ArrayList<URL>()
532                                          : replacementList;
533      }
534    
535      //============================================================================
536      // SUBSOURCES
537      //============================================================================
538      
539      /**
540       * Adds {@code newSubsource} to this source.
541       * If {@code newSubsource} is <i>null</i>, no action is taken.
542       * 
543       * @param newSubsource the new subsource to be added to this source.
544       */
545      public void addSubsource(Subsource newSubsource)
546      {
547        if (newSubsource != null)
548          subsources.add(newSubsource);
549      }
550      
551      /**
552       * Removes {@code oldSubsource} from this source.
553       * <p>
554       * If this source is not holding {@code oldSubsource}, this method
555       * does nothing.</p> 
556       * 
557       * @param oldSubsource the subsource to be removed from this source.
558       */
559      public void removeSubsource(Subsource oldSubsource)
560      {
561        if (oldSubsource != null)
562          subsources.remove(oldSubsource);
563      }
564      
565      /**
566       * Removes all subsources from this source. 
567       */
568      public void removeAllSubsources()
569      {
570        subsources.clear();
571      }
572      
573      /**
574       * Replaces this source's collection of subsources with
575       * {@code replacementSet}.
576       * <p>
577       * Note that this source will hold a reference to {@code replacementSet}
578       * (unless it is <i>null</i>); it will not store a copy.  This means
579       * that any changes a client makes to {@code replacementSet} <i>after</i>
580       * calling this method will be reflected in this source.</p>
581       * 
582       * @param replacementSet a replacement set of subsources for this source.
583       *                       If {@code replacementSet} is <i>null</i>, it will
584       *                       be interpreted as an empty set.
585       */
586      public void setSubsources(SortedSet<Subsource> replacementSet)
587      {
588        subsources = (replacementSet == null) ? new TreeSet<Subsource>()
589                                              : replacementSet;
590      }
591      
592      /**
593       * Returns this source's collection of subsources.
594       * <p>
595       * Note that returned set is the one actually held by this source,
596       * not a clone thereof.  That means that any changes that a client
597       * makes to the set will affect this source.</p>
598       * 
599       * @return this source's collection of subsources.  The value returned
600       *         is guaranteed to be non-null, though it may be an empty set.
601       */
602      @XmlElementWrapper
603      @XmlElement(name="subsource")
604      public SortedSet<Subsource> getSubsources()
605      {
606        return subsources;
607      }
608      
609      /**
610       * Returns the subsource that represents the center position of this source.
611       * <p>
612       * This method will search this source's set of subsources for one whose
613       * name is {@link #CENTER_POSITION_NAME}.  If no such subsource is found,
614       * an arbitrary subsource will be returned.  If this source has no subsources
615       * the returned value will be <i>null</i>.</p>
616       * 
617       * @return the central subsource, or <i>null</i>.
618       */
619      public Subsource getCentralSubsource()
620      {
621        //Quick exit if no subsources
622        if ((subsources == null) || (subsources.size() < 1))
623          return null;
624        
625        //The code here originally took advantage of the fact that we're using a
626        //SortedSet.  Unfortunately, something was going wrong in a Hibernate
627        //wrapper class (PersistentSortedSet), wherein Hibernate was using a
628        //HashSet to hold the subsources.
629        Subsource central = null;
630    
631        for (Subsource ss : subsources)
632        {
633          if (central == null)
634            central = ss; //will return first ss if we never find match on name
635          
636          if (ss.getName().equalsIgnoreCase(CENTER_POSITION_NAME))
637          {
638            central = ss;
639            break;
640          }
641        }
642        
643        //Return either the true central subsource,
644        //or the first in the list if no central subsource 
645        return central;
646      }
647      
648      //----------------------------------------------------------------------------
649      // Delegation to Central Subsource
650      //----------------------------------------------------------------------------
651    
652      /**
653       * Returns the position of this source.
654       * This is a convenience method that is equivalent to calling
655       * {@code getCentralSubsource().getPosition()}.
656       * 
657       * @return the position of this source.
658       */
659      public SkyPosition getPosition()
660      {
661        return getCentralSubsource().getPosition();
662      }
663    
664      /**
665       * Returns the velocity of this source toward or away from the
666       * observer at the given point in time.
667       * 
668       * @param observer
669       *   a position on earth.  The radial velocity of this source is
670       *   calculated relative this observer.
671       *   
672       * @param dateTime
673       *   the point in time at which the velocity is calculated.
674       *   
675       * @return
676       *   the radial velocity of this source toward or away from a point
677       *   on earth.
678       *   
679       * @throws CoordinateConversionException
680       *   if the position of this source cannot be converted to
681       *   an equatorial RA / Dec position.
682       *   
683       * @since 2008-07-29
684       */
685      public LinearVelocity calcVelocityRelativeTo(EarthPosition observer,
686                                                   Date          dateTime)
687        throws CoordinateConversionException
688      {
689        return getCentralSubsource().calcVelocityRelativeTo(observer, dateTime);
690      }
691      
692      /**
693       * Returns an observed frequency based on a rest frequency and the
694       * relative motion between this source and an earth-bound observer.
695       * 
696       * @param restFreq
697       *   the rest frequency for which an observed frequency is requested.
698       *    
699       * @param observer
700       *   a position on earth.  The relative radial velocity between this
701       *   observer and this source determines the amount by which the
702       *   {@code restFreq} is shifted into an observed frequency.
703       *   
704       * @param dateTime
705       *   the time for which the velocity calculation is performed.
706       *   
707       * @return
708       *   an observed frequency based on {@code restFreq} and the relative
709       *   motion between this source and the {@code observer}.
710       *   
711       * @throws CoordinateConversionException
712       *   if the position of this source cannot be converted to
713       *   an equatorial RA / Dec position.
714       *   
715       * @since 2008-07-29
716       */
717      public Frequency calcShiftedFrequency(Frequency     restFreq,
718                                            EarthPosition observer,
719                                            Date          dateTime)
720        throws CoordinateConversionException
721      {
722        return getCentralSubsource().calcShiftedFrequency(restFreq,
723                                                          observer, dateTime);
724      }
725      
726      //============================================================================
727      // INTERFACE UserAccountable
728      //============================================================================
729      
730      public void setCreatedBy(Long userId)
731      {
732        createdBy = (userId == null) ? UserAccountable.NULL_USER_ID : userId;
733      }
734      
735      public void setCreatedOn(Date d)
736      {
737        if (d != null)
738          createdOn = d;
739      }
740    
741      public void setLastUpdatedBy(Long userId)
742      {
743        lastUpdatedBy = (userId == null) ? UserAccountable.NULL_USER_ID : userId;
744      }
745    
746      public void setLastUpdatedOn(Date d)
747      {
748        if (d != null)
749          lastUpdatedOn = d;
750      }
751    
752      public Long getCreatedBy()      { return createdBy;     }
753      public Date getCreatedOn()      { return createdOn;     }
754      public Long getLastUpdatedBy()  { return lastUpdatedBy; }
755      public Date getLastUpdatedOn()  { return lastUpdatedOn; }
756    
757      //============================================================================
758      // PERSISTENCE HELPERS
759      //============================================================================
760      
761      //This pair of methods converts List<String> to/from single string
762      @SuppressWarnings("unused")
763      private String getPersistentAliases()
764      {
765        return StringUtil.getInstance().fromCollection(aliases, " | ");
766      }
767      
768      @SuppressWarnings("unused")
769      private void setPersistentAliases(String text)
770      {
771        aliases.clear();
772        StringUtil.getInstance().toCollection(text, " | ", aliases);
773      }
774    
775      //============================================================================
776      // TEXT
777      //============================================================================
778      
779      /**
780       * Returns a text representation of this source.
781       * The default form of the text is XML.  However, if anything goes wrong
782       * during the conversion to XML, an alternate, and much abbreviated, form
783       * will be returned.
784       * 
785       * @return a text representation of this source.
786       */
787      public String toString()
788      {
789        try {
790          return toXml();
791        }
792        catch (Exception ex) {
793          StringBuilder buff = new StringBuilder();
794          
795          buff.append("name=").append(name);
796          buff.append(", id=").append(id);
797          buff.append(", subsources=").append(subsources.size());
798          
799          return buff.toString();
800        }
801      }
802    
803      /**
804       * Returns an XML representation of this source.
805       * @return an XML representation of this source.
806       * @throws JAXBException if anything goes wrong during the conversion to XML.
807       * @see #writeAsXmlTo(Writer)
808       */
809      public String toXml() throws JAXBException
810      {
811        return JaxbUtility.getSharedInstance().objectToXmlString(this);
812      }
813      
814      /**
815       * Writes an XML representation of this source to {@code writer}.
816       * @param writer the device to which XML is written.
817       * @throws JAXBException if anything goes wrong during the conversion to XML.
818       */
819      public void writeAsXmlTo(Writer writer) throws JAXBException
820      {
821        JaxbUtility.getSharedInstance().writeObjectAsXmlTo(writer, this, null);
822      }
823      
824      /**
825       * Creates a new source from the XML data in the given file.
826       * 
827       * @param xmlFile the name of an XML file.  This method will attempt to locate
828       *                the file by using {@link Class#getResource(String)}.
829       *                
830       * @return a new source from the XML data in the given file.
831       * 
832       * @throws FileNotFoundException if the XML file cannot be found.
833       * 
834       * @throws JAXBException if the schema file used (if any) is malformed, if
835       *           the XML file cannot be read, or if the XML file is not
836       *           schema-valid.
837       * 
838       * @throws XMLStreamException if there is a problem opening the XML file,
839       *           if the XML is not well-formed, or for some other
840       *           "unexpected processing conditions".
841       */
842      public static Source fromXml(String xmlFile)
843        throws JAXBException, XMLStreamException, FileNotFoundException
844      {
845        return JaxbUtility.getSharedInstance()
846                          .xmlFileToObject(xmlFile, Source.class);
847      }
848      
849      /**
850       * Creates a new source based on the XML data read from {@code reader}.
851       * 
852       * @param reader the source of the XML data.
853       *               If this value is <i>null</i>, <i>null</i> is returned.
854       *               
855       * @return a new source based on the XML data read from {@code reader}.
856       * 
857       * @throws XMLStreamException if the XML is not well-formed,
858       *           or for some other "unexpected processing conditions".
859       *           
860       * @throws JAXBException if anything else goes wrong during the
861       *           transformation.
862       */
863      public static Source fromXml(Reader reader)
864        throws JAXBException, XMLStreamException
865      {
866        return JaxbUtility.getSharedInstance()
867                          .readObjectAsXmlFrom(reader, Source.class, null);
868      }
869      
870      //----------------------------------------------------------------------------
871      // User-Defined Values
872      //----------------------------------------------------------------------------
873      
874      //Used only for JAXB.  Helps deal w/ the underlying sorted map.
875      @XmlElementWrapper(name="userDefinedValues")
876      @XmlElement(name="userDefinedValue")
877      @SuppressWarnings("unused")
878      private KeyValue[] getXmlUserDefinedValues()
879      {
880        int valueCount = userDefinedValues.size();
881        if (valueCount == 0)
882          return null;
883        
884        KeyValue[] result = new KeyValue[valueCount];
885        
886        int e=0;
887        for (Map.Entry<String, String> udv : userDefinedValues.entrySet())
888          result[e++] = new KeyValue(udv.getKey(), udv.getValue());
889        
890        return result;
891      }
892      
893      //Used only for JAXB.  Helps deal w/ the underlying sorted map.
894      @SuppressWarnings("unused")
895      private void setXmlUserDefinedValues(KeyValue[] yyy)
896      {
897        userDefinedValues.clear();
898        for (KeyValue x : yyy)
899          userDefinedValues.put(x.key, x.value);
900      }
901      
902      //Used only for JAXB.  Helps deal w/ the underlying sorted map.
903      static class KeyValue
904      {
905        @XmlAttribute String key;
906        @XmlElement   String value;
907        
908        KeyValue()  { this("",""); }
909        
910        KeyValue(String key, String value)
911        {
912          this.key = key;
913          this.value = value;
914        }
915      }
916      
917      //============================================================================
918      // 
919      //============================================================================
920      
921      /**
922       *  Returns a source that is equal to this one.
923       *  <p>
924       *  If anything goes wrong during the cloning procedure,
925       *  a {@code RuntimeException} will be thrown.</p>
926       */
927      public Source clone()
928      {
929        Source clone = null;
930    
931        try
932        {
933          //This line takes care of the primitive fields properly
934          clone = (Source)super.clone();
935          
936          //We do NOT want the clone to have the same ID as the original.
937          //The ID is here for the persistence layer; it is in charge of
938          //setting IDs.  To help it, we put the clone's ID in the uninitialized
939          //state.
940          clone.id = Identifiable.UNIDENTIFIED;
941          
942          clone.xmlId = "source-" + UUID.randomUUID().toString();
943    
944          clone.createdOn     = new Date();
945          clone.lastUpdatedOn = clone.createdOn;
946          
947          //Clone list, but it's OK to share refs to the same immutable strings.
948          clone.aliases           = new ArrayList<String>(this.aliases);
949          clone.historicalRecords = new ArrayList<String>(this.historicalRecords);
950          clone.notes             = new ArrayList<String>(this.notes);
951          
952          clone.links = new ArrayList<URL>();
953          for (URL link : this.links)
954            clone.links.add(new URL(link.toString()));
955          
956          clone.userDefinedValues =
957            new TreeMap<String, String>(this.userDefinedValues);
958          
959          clone.imageLinks = new ArrayList<SourceImageLink>();
960          for (SourceImageLink link : this.imageLinks)
961            clone.imageLinks.add(link.clone());
962          
963          clone.subsources = new TreeSet<Subsource>();
964          for (Subsource ss : this.subsources)
965            clone.addSubsource(ss.clone());
966        }
967        catch (Exception ex)
968        {
969          throw new RuntimeException(ex);
970        }
971        
972        return clone;
973      }
974      
975      /** Returns <i>true</i> if {@code o} is equal to this source. */
976      public boolean equals(Object o)
977      {
978        //Quick exit if o is this
979        if (o == this)
980          return true;
981        
982        //Quick exit if o is null
983        if (o == null)
984          return false;
985        
986        //Quick exit if classes are different
987        if (!o.getClass().equals(this.getClass()))
988          return false;
989        
990        Source other = (Source)o;
991        
992        //NOTE 1: The absence of the id property here is intentional.
993        
994        //NOTE 2: New philosophy: Base equality mainly on the science.
995        //        For example, we will no longer compare the createdOn/By
996        //        and lastUpdatedOn/By properties.
997        
998        //NOTE 3: Absence of aliases, origin of information, user-defined values,
999        //        image links, plain links, and historicalRecords
1000        //        here is intentional.  TODO Include any of these?
1001        
1002        //Compare the attributes
1003        if (!other.name.equals(this.name))
1004          return false;
1005        
1006        //We can't rely on comparing the two sets directly because the
1007        //Set classes don't handle mutability of their elements very
1008        //well.  (See http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6579200)
1009        //If we make fresh sets, though, we're OK.
1010        //(In this particular case we have troubles of our own making as well.
1011        //We're using a SortedSet for subsources, but the Subsource.compareTo
1012        //method is not consistent w/ equals, which means Subsource is not a
1013        //good candidate for sorted sets.)
1014        HashSet<Subsource> theseSubs = new HashSet<Subsource>( this.subsources);
1015        HashSet<Subsource> thoseSubs = new HashSet<Subsource>(other.subsources);
1016        
1017        return thoseSubs.equals(theseSubs);
1018      }
1019    
1020      /** Returns a hash code value for this source. */
1021      public int hashCode()
1022      {
1023        //Taken from the Effective Java book by Joshua Bloch.
1024        //The constants 17 & 37 are arbitrary & carry no meaning.
1025        int result = 17;
1026        
1027        //NOTE: The absence of the id property here is intentional.
1028        //      See other NOTES in equals method.
1029        
1030        result = 37 * result + name.hashCode();
1031        result = 37 * result + subsources.hashCode();
1032        
1033        return result;
1034      }
1035      
1036      //============================================================================
1037      // 
1038      //============================================================================
1039      /*
1040      public static void main(String[] args)
1041      {
1042        SourceBuilder builder = new SourceBuilder();
1043        
1044        Source src = builder.makeSource("Deep Throat");
1045    
1046        try
1047        {
1048          src.writeAsXmlTo(new java.io.PrintWriter(System.out));
1049        }
1050        catch (Exception ex)
1051        {
1052          System.out.println("Trouble w/ src.toXml.  Msg:");
1053          System.out.println(ex.getMessage());
1054          ex.printStackTrace();
1055          
1056          System.out.println("Attempting to write XML w/out schema verification:");
1057          JaxbUtility.getSharedInstance().setLookForDefaultSchema(false);
1058          try
1059          {
1060            System.out.println(src.toXml());
1061          }
1062          catch (JAXBException ex2)
1063          {
1064            System.out.println("Still had trouble w/ src.toXml.  Msg:");
1065            System.out.println(ex.getMessage());
1066            ex.printStackTrace();
1067          }
1068        }
1069      }
1070      */
1071      /*
1072      public static void main(String... args) throws Exception
1073      {
1074        java.io.FileReader reader =
1075          new java.io.FileReader("/export/home/calmer/dharland/JUNK/Source.xml");
1076        
1077        Source src = Source.fromXml(reader);
1078        
1079        java.io.FileWriter writer =
1080          new java.io.FileWriter("/export/home/calmer/dharland/JUNK/Source-OUT.xml");
1081        src.writeAsXmlTo(writer);
1082      }
1083      */
1084      /*
1085      public static void main(String... args) throws Exception
1086      {
1087        SourceBuilder sb = new SourceBuilder();
1088        Subsource ss1=null;
1089        do
1090        {
1091          ss1 = sb.makeSubsource(ss1);
1092        }while (!(ss1.getPosition() instanceof SimpleSkyPosition));
1093        
1094        Source s1 = sb.makeSource("orig");
1095        s1.addSubsource(ss1);
1096        Source s2 = s1.clone();
1097        
1098        System.out.println("s1 == s1.clone()? => " + s1.equals(s2));
1099    
1100        String ssName = ss1.getName();
1101        Subsource ss2=null;
1102        for (Subsource subSrc : s2.getSubsources())
1103        {
1104          if (subSrc.getName().equals(ssName))
1105          {
1106            ss2=subSrc;
1107            SimpleSkyPosition pos = (SimpleSkyPosition)ss2.getPosition();
1108            Distance d = pos.getDistance();
1109            pos.setDistance(d.add(new Distance("1.0")));
1110            System.out.println("new dist = " +pos.getDistance());
1111          }
1112        }
1113        
1114        boolean eq = s1.equals(s2);
1115        System.out.println(" s1 ==  s2? => " + eq);
1116        System.out.println("ss1 == ss2? => " + ss1.equals(ss2));
1117        System.out.println("s1.hash = " + s1.hashCode());
1118        System.out.println("s2.hash = " + s2.hashCode());
1119      }
1120      */
1121      /*
1122      public static void main(String... args) throws Exception
1123      {
1124        for (ReceiverBand rb : ReceiverBand.values())
1125        {
1126          System.out.print(makeUdbKeyVlaUvMax(rb)+"   |   ");
1127          System.out.println(makeUdbKeyVlaUvMin(rb));
1128          for (TelescopeConfiguration tc : TelescopeConfiguration.values())
1129          {
1130            System.out.println("  " + makeUdbKeyVlaCalibQuality(rb, tc));
1131          }
1132        }
1133      }
1134      */
1135    }