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.Date;
008    import java.util.List;
009    import java.util.Map;
010    import java.util.UUID;
011    
012    import javax.xml.bind.JAXBException;
013    import javax.xml.bind.annotation.XmlAttribute;
014    import javax.xml.bind.annotation.XmlElement;
015    import javax.xml.bind.annotation.XmlElementWrapper;
016    import javax.xml.bind.annotation.XmlID;
017    import javax.xml.bind.annotation.XmlIDREF;
018    import javax.xml.bind.annotation.XmlRootElement;
019    import javax.xml.bind.annotation.XmlTransient;
020    import javax.xml.bind.annotation.XmlType;
021    import javax.xml.bind.annotation.XmlValue;
022    import javax.xml.stream.XMLStreamException;
023    
024    import edu.nrao.sss.model.UserAccountable;
025    import edu.nrao.sss.util.Identifiable;
026    import edu.nrao.sss.util.JaxbUtility;
027    import edu.nrao.sss.util.LookupTable;
028    
029    /**
030     * A lookup table where the index is of type {@link java.util.Date}
031     * and the value is of type {@link Source}.
032     * <p>
033     * <b>Version Info:</b>
034     * <table style="margin-left:2em">
035     *   <tr><td>$Revision: 1709 $</td></tr>
036     *   <tr><td>$Date: 2008-11-14 11:22:37 -0700 (Fri, 14 Nov 2008) $</td></tr>
037     *   <tr><td>$Author: dharland $ (last person to modify)</td></tr>
038     * </table></p>
039     *  
040     * @author David M. Harland
041     * @since 2006-09-15
042     */
043    @XmlRootElement
044    @XmlType(propOrder={"name",
045                        "createdBy","createdOn","lastUpdatedBy","lastUpdatedOn",
046                        "notes",
047                        "xmlSource", "xmlSourceRef"})
048    public class SourceLookupTable
049      extends LookupTable<Date, Source>
050      implements SourceCatalogEntry
051    {
052      private static final String NO_NAME = "[NEW]";
053      
054      //IDENTIFICATION
055      private long   id;
056      private String name;
057    
058      @XmlAttribute(required=true)
059      @XmlID
060      String xmlId;
061    
062      //USER TRACKING
063      private Long createdBy;      //This is a user ID
064      private Date createdOn;
065      private Long lastUpdatedBy;  //This is a user ID
066      private Date lastUpdatedOn;
067      
068      //OTHER PROPERTIES
069      private List<String> notes;
070    
071      //NON-PERSISTED ATTRIBUTES
072      private SourceTableListener listener;
073    
074      /** Creates a new table with a default name. */
075      public SourceLookupTable()  { this(NO_NAME); }
076      
077      /**
078       * Creates a new table with the given name.
079       * 
080       * @param nameOfTable the name of this table.  If this value is
081       *                    <i>null</i>, this table will be given a
082       *                    default name.
083       */
084      public SourceLookupTable(String nameOfTable)
085      {
086        super();
087        
088        id    = Identifiable.UNIDENTIFIED;
089        name  = (nameOfTable == null) ? NO_NAME : nameOfTable;
090        xmlId = "sourceTable-" + UUID.randomUUID().toString();
091        
092        createdBy     = UserAccountable.NULL_USER_ID;
093        createdOn     = new Date();
094        lastUpdatedBy = UserAccountable.NULL_USER_ID;
095        lastUpdatedOn = new Date();
096    
097        notes = new ArrayList<String>();
098    
099        listener = SourceTableListener.NULL_LISTENER;
100      }
101    
102      //============================================================================
103      // IDENTIFICATION
104      //============================================================================
105    
106      /* (non-Javadoc)
107       * @see SourceCatalogEntry#setId(java.lang.Long)
108       */
109      public void setId(Long id)  { this.id = id; }
110    
111      /* (non-Javadoc)
112       * @see edu.nrao.sss.model.util.Identifiable#getId()
113       */
114      @XmlAttribute
115      public Long getId()  { return id; }
116    
117      /* (non-Javadoc)
118       * @see edu.nrao.sss.model.source.SourceCatalogEntry#clearId()
119       */
120      public void clearId()
121      {
122        this.id = Identifiable.UNIDENTIFIED;
123    
124        for (Date key : this.getKeySet())
125          get(key).clearId();
126      }
127      
128      /**
129       * Sets the name of this table.
130       * <p>
131       * If {@code newName} is <i>null</i> or the empty string
132       * (<tt>""</tt>), the request to change the name will be
133       * denied and the current name will remain in place.</p>
134       * 
135       * @param newName the new name for this table.
136       */
137      public void setName(String newName)
138      {
139        if (newName != name && newName.length() > 0)
140          name = newName;
141      }
142      
143      /**
144       * Returns the name of this table.
145       * @return the name of this table.
146       */
147      public String getName()  { return name; }
148    
149      //============================================================================
150      // OTHER PROPERTIES
151      //============================================================================
152    
153      /**
154       * Returns a list of notes about this table.
155       * Each note is free-form text with no particular structure.
156       * <p>
157       * This method returns the list actually held by this
158       * {@code SourceLookuptable}, so
159       * any list manipulations may be performed by first fetching the list and
160       * then operating on it.</p>
161       * 
162       * @return a list of notes about this table.
163       */
164      @XmlElementWrapper
165      @XmlElement(name="note")
166      public List<String> getNotes()
167      {
168        return notes;
169      }
170      
171      /** This is here for mechanisms that need setX/getX pairs, such as JAXB. */
172      @SuppressWarnings("unused")
173      private void setNotes(List<String> replacementList)
174      {
175        notes = (replacementList == null) ? new ArrayList<String>()
176                                          : replacementList;
177      }
178    
179      //============================================================================
180      // LISTENER
181      //============================================================================
182      
183      /**
184       * Sets the object that will listen to this table.
185       * @param listener the object that will listen to this table.  If this value
186       *                 is <i>null</i>, a null-like listener will be used instead.
187       */
188      public void setListener(SourceTableListener listener)
189      {
190        this.listener = (listener == null) ? SourceTableListener.NULL_LISTENER
191                                           : listener;
192      }
193      
194      /**
195       * Removes {@code listener} as a listener of this table.
196       * @param listener a listener of this table.
197       */
198      public void removeListener(SourceTableListener listener)
199      {
200        //If incoming listener is our listener, revert to null listener
201        if (listener == this.listener)
202          this.listener = SourceTableListener.NULL_LISTENER;
203      }
204      
205      /**
206       * Returns the object that is listening to this table.
207       * @return the object that is listening to this table.
208       */
209      @XmlTransient
210      public SourceTableListener getListener()  { return listener; }
211      
212      //============================================================================
213      // INTERFACE UserAccountable
214      //============================================================================
215      
216      public void setCreatedBy(Long userId)
217      {
218        createdBy = (userId == null) ? UserAccountable.NULL_USER_ID : userId;
219      }
220      
221      public void setCreatedOn(Date d)
222      {
223        if (d != null)
224          createdOn = d;
225      }
226    
227      public void setLastUpdatedBy(Long userId)
228      {
229        lastUpdatedBy = (userId == null) ? UserAccountable.NULL_USER_ID : userId;
230      }
231    
232      public void setLastUpdatedOn(Date d)
233      {
234        if (d != null)
235          lastUpdatedOn = d;
236      }
237    
238      public Long getCreatedBy()      { return createdBy;     }
239      public Date getCreatedOn()      { return createdOn;     }
240      public Long getLastUpdatedBy()  { return lastUpdatedBy; }
241      public Date getLastUpdatedOn()  { return lastUpdatedOn; }
242      
243      //============================================================================
244      // OVERRIDES OF PARENT METHODS
245      //============================================================================
246    
247      //The reason for the overrides is to notify the listener.
248      
249      public void clear()
250      {
251        super.clear();
252        listener.entriesCleared(this);
253      }
254    
255      public Source put(Date key, Source value)
256      {
257        Source oldSource = super.put(key, value);
258        listener.sourceAdded(value, this);
259        return oldSource;
260      }
261    
262      public void putAll(Map<Date, Source> map)
263      {
264        super.putAll(map);
265        listener.sourcesAdded(map.values(), this);
266      }
267    
268      public Source remove(Date key)
269      {
270        Source oldSource = super.remove(key);
271        listener.sourceRemoved(oldSource, this);
272        return oldSource;
273      }
274    
275      //============================================================================
276      // TEXT
277      //============================================================================
278      
279      /**
280       * Returns a text representation of this table.
281       * The default form of the text is XML.  However, if anything goes wrong
282       * during the conversion to XML, an alternate, and much abbreviated, form
283       * will be returned.
284       * 
285       * @return a text representation of this table.
286       */
287      public String toString()
288      {
289        try {
290          return toXml();
291        }
292        catch (Exception ex) {
293          StringBuilder buff = new StringBuilder();
294          
295          buff.append("name=").append(name);
296          buff.append(", id=").append(id);
297          buff.append(", size=").append(size());
298          
299          return buff.toString();
300        }
301      }
302    
303      /**
304       * Returns an XML representation of this table.
305       * @return an XML representation of this table.
306       * @throws JAXBException if anything goes wrong during the conversion to XML.
307       */
308      public String toXml() throws JAXBException
309      {
310        return JaxbUtility.getSharedInstance().objectToXmlString(this);
311      }
312      
313      /**
314       * Writes an XML representation of this table to {@code writer}.
315       * @param writer the device to which XML is written.
316       * @throws JAXBException if anything goes wrong during the conversion to XML.
317       */
318      public void writeAsXmlTo(Writer writer) throws JAXBException
319      {
320        JaxbUtility.getSharedInstance().writeObjectAsXmlTo(writer, this, null);
321      }
322    
323      /**
324       * Creates a new table from the XML data in the given file.
325       * 
326       * @param xmlFile the name of an XML file.  This method will attempt to locate
327       *                the file by using {@link Class#getResource(String)}.
328       *                
329       * @return a new table from the XML data in the given file.
330       * 
331       * @throws FileNotFoundException if the XML file cannot be found.
332       * 
333       * @throws JAXBException if the schema file used (if any) is malformed, if
334       *           the XML file cannot be read, or if the XML file is not
335       *           schema-valid.
336       * 
337       * @throws XMLStreamException if there is a problem opening the XML file,
338       *           if the XML is not well-formed, or for some other
339       *           "unexpected processing conditions".
340       */
341      public static SourceLookupTable fromXml(String xmlFile)
342        throws JAXBException, XMLStreamException, FileNotFoundException
343      {
344        return JaxbUtility.getSharedInstance()
345                          .xmlFileToObject(xmlFile, SourceLookupTable.class);
346      }
347      
348      /**
349       * Creates a new table based on the XML data read from {@code reader}.
350       * 
351       * @param reader the source of the XML data.
352       *               If this value is <i>null</i>, <i>null</i> is returned.
353       *               
354       * @return a new table based on the XML data read from {@code reader}.
355       * 
356       * @throws XMLStreamException if the XML is not well-formed,
357       *           or for some other "unexpected processing conditions".
358       *           
359       * @throws JAXBException if anything else goes wrong during the
360       *           transformation.
361       */
362      public static SourceLookupTable fromXml(Reader reader)
363        throws JAXBException, XMLStreamException
364      {
365        return JaxbUtility.getSharedInstance()
366                          .readObjectAsXmlFrom(reader,
367                                               SourceLookupTable.class, null);
368      }
369    
370      //Used only for JAXB.  Helps deal w/ the underlying sorted map.
371      
372      /**
373       * Controls the way sources are handled in the XML for this table.
374       * If this value is <i>true</i>, then IDREFs are used for the sources.
375       * If it is <i>false</i>, then the full source XML is contained in
376       * the XML for this table.
377       */
378      boolean useSourceReferencesInXml = false;
379      
380      @XmlType(name="sourceLookupTableEntry")
381      private static class SourceLookupTableEntry
382      {
383        @XmlAttribute       Date   startTime;
384        @XmlValue @XmlIDREF Source source;
385    
386        SourceLookupTableEntry()  { }
387        SourceLookupTableEntry(Date d, Source s)  { startTime=d; source=s; }
388      }
389      
390      @XmlType(name="sourceLookupTableEntryFull")
391      private static class SourceLookupTableEntryFull
392      {
393        @XmlAttribute Date   startTime;
394        @XmlElement   Source source;
395    
396        SourceLookupTableEntryFull()  { }
397        SourceLookupTableEntryFull(Date d, Source s)  { startTime=d; source=s; }
398      }
399    
400      @XmlElement(name="sourceId")
401      @SuppressWarnings("unused")
402      private SourceLookupTableEntry[] getXmlSourceRef()
403      {
404        //If we're providing full source, don't provide reference
405        if (!useSourceReferencesInXml)
406          return null;
407        
408        ArrayList<SourceLookupTableEntry> entries =
409          new ArrayList<SourceLookupTableEntry>();
410        
411        for (Date key : getKeySet())
412          entries.add(new SourceLookupTableEntry(key, get(key)));
413    
414        return entries.toArray(new SourceLookupTableEntry[entries.size()]);
415      }
416      
417      @SuppressWarnings("unused")
418      private void setXmlSourceRef(SourceLookupTableEntry[] entries)
419      {
420        clear();
421        for (SourceLookupTableEntry e : entries)
422        {
423          put(e.startTime, e.source);
424        }
425      }
426    
427      @XmlElement(name="entry")
428      @SuppressWarnings("unused")
429      private SourceLookupTableEntryFull[] getXmlSource()
430      {
431        //If we're using references, don't provide full source
432        if (useSourceReferencesInXml)
433          return null;
434          
435        ArrayList<SourceLookupTableEntryFull> entries =
436          new ArrayList<SourceLookupTableEntryFull>();
437        
438        for (Date key : getKeySet())
439          entries.add(new SourceLookupTableEntryFull(key, get(key)));
440    
441        return entries.toArray(new SourceLookupTableEntryFull[entries.size()]);
442      }
443      
444      @SuppressWarnings("unused")
445      private void setXmlSource(SourceLookupTableEntryFull[] entries)
446      {
447        clear();
448        for (SourceLookupTableEntryFull e : entries)
449        {
450          put(e.startTime, e.source);
451        }
452      }
453    
454      //============================================================================
455      // 
456      //============================================================================
457      
458      /**
459       * Returns a deep copy of this source lookup table.
460       * Both the dates and the sources held in the returned table are clones of
461       * those held in this table.
462       * <p>
463       * If anything goes wrong during the cloning procedure,
464       * a {@code RuntimeException} will be thrown.</p>
465       * 
466       * @see #cloneAllButSources()
467       */
468      public SourceLookupTable clone()
469      {
470        SourceLookupTable clone;
471        
472        try
473        {
474          clone = (SourceLookupTable)super.clone();
475          
476          initClone(clone);
477         
478          //Recreate the lookup table using cloned keys and cloned values
479          clone.clear();
480          for (Date key : this.getKeySet())
481            clone.put((Date)key.clone(), this.get(key).clone());
482        }
483        catch (Exception ex)
484        {
485          throw new RuntimeException(ex);
486        }
487      
488        return clone;
489      }
490     
491      /**
492       * Returns a copy of this source lookup table that holds references to the
493       * same sources as this table.  The dates in the returned table are clones
494       * of those in this table, but both tables refer to the same source instances.
495       * 
496       * @return a copy of this source lookup table.
497       * 
498       * @see #clone()
499       */
500      public SourceLookupTable cloneAllButSources()
501      {
502        SourceLookupTable clonedTable;
503       
504        try
505        {
506          clonedTable = (SourceLookupTable)super.clone();
507          
508          initClone(clonedTable);
509    
510          //Recreate the lookup table using cloned keys and SAME values
511          clonedTable.clear();
512          for (Date key : this.getKeySet())
513            clonedTable.put((Date)key.clone(), this.get(key));
514        } 
515        catch (Exception ex)
516        {
517          throw new RuntimeException(ex);
518        }
519     
520        return clonedTable;
521      }
522      
523      private void initClone(SourceLookupTable newClone)
524      {
525        //We do NOT want the clone to have the same ID as the original.
526        //The ID is here for the persistence layer, which is in charge of
527        //setting IDs.  To help it, we put the clone's ID in the uninitialized
528        //state.
529        newClone.id = Identifiable.UNIDENTIFIED;
530        
531        newClone.xmlId = "sourceTable-" + UUID.randomUUID().toString();
532    
533        newClone.createdOn     = (Date)this.createdOn.clone();
534        newClone.lastUpdatedOn = (Date)this.lastUpdatedOn.clone();
535        
536        //Clone list, but it's OK to share refs to the same immutable strings.
537        newClone.notes = new ArrayList<String>(this.notes);
538    
539        newClone.setListener(null);
540      }
541      
542      /** Returns <i>true</i> if {@code o} is equal to this table. */
543      @Override
544      public boolean equals(Object o)
545      {
546        //Quick exit if o is this
547        if (o == this)
548          return true;
549        
550        //If super class says objects are different, they're different
551        if (!super.equals(o))
552          return false;
553        
554        //Super class ensured objects are of same class
555        SourceLookupTable other = (SourceLookupTable)o;
556        
557        //NOTE: Absence of ID, created/lastUpdated On/By, and notes is intentional
558        
559        //Compare the table names
560        return this.name.equals(other.name);
561      }
562    
563      /** Returns a hash code value for this table. */
564      public int hashCode()
565      {
566        //Taken from the Effective Java book by Joshua Bloch.
567        //The constants 17 & 37 are arbitrary & carry no meaning.
568        int result = 17;
569        
570        //NOTE: Keep this method in synch w/ equals
571        
572        result = 37 * result + super.hashCode();
573        result = 37 * result + name.hashCode();
574        
575        return result;
576      }
577    }