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 }