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 }