001 package edu.nrao.sss.model.source; 002 003 import java.io.FileNotFoundException; 004 import java.io.Reader; 005 import java.util.ArrayList; 006 import java.util.Arrays; 007 import java.util.Collection; 008 import java.util.Comparator; 009 import java.util.Date; 010 import java.util.HashMap; 011 import java.util.List; 012 import java.util.Map; 013 014 import javax.xml.bind.JAXBException; 015 import javax.xml.bind.annotation.XmlIDREF; 016 import javax.xml.bind.annotation.XmlRootElement; 017 import javax.xml.bind.annotation.XmlType; 018 import javax.xml.stream.XMLStreamException; 019 020 import edu.nrao.sss.catalog.CatalogItemGroup; 021 import edu.nrao.sss.model.RepositoryException; 022 import edu.nrao.sss.util.Filter; 023 import edu.nrao.sss.util.Identifiable; 024 import edu.nrao.sss.util.JaxbUtility; 025 026 /** 027 * A collection of {@link Source}s and {@link SourceLookupTable}s. 028 * A {@code SourceGroup} is normally contained in a 029 * {@link SourceCatalog} and is a way of categorizing sources that 030 * have similar traits. 031 * <p> 032 * <b>Version Info:</b> 033 * <table style="margin-left:2em"> 034 * <tr><td>$Revision: 2313 $</td></tr> 035 * <tr><td>$Date: 2009-05-20 15:00:52 -0600 (Wed, 20 May 2009) $</td></tr> 036 * <tr><td>$Author: btruitt $</td></tr> 037 * </table></p> 038 * 039 * @author David M. Harland 040 * @since 2006-09-15 041 */ 042 @XmlRootElement 043 @XmlType(propOrder={"source", "sourceTable"}) 044 public class SourceGroup 045 extends CatalogItemGroup<SourceCatalogEntry, SourceGroup, SourceCatalog> 046 implements Identifiable, Cloneable, SourceProvider 047 { 048 private static final boolean CHECK_FOR_NON_NULL_CATALOG = true; 049 private static final boolean DO_NOT_CHECK_FOR_NON_NULL_CATALOG = false; 050 051 //NON-PERSISTED PROPERTIES 052 private SourceTableListener tableListener; 053 054 /** 055 * Creates a new group that has a default name and that belongs to no catalog. 056 */ 057 public SourceGroup() 058 { 059 this(null, null); 060 } 061 062 /** 063 * Creates a new group that belongs to {@code container}. 064 * 065 * @param container the name of the one catalog to which this group belongs. 066 * This value may be <i>null</i>. 067 * 068 * @param nameOfGroup the name of this group. If this value is <i>null</i>, 069 * a non-null default name will be used. 070 */ 071 public SourceGroup(SourceCatalog container, String nameOfGroup) 072 { 073 super(container, nameOfGroup); 074 075 if (container == null) 076 tableListener = new Listener(this, CHECK_FOR_NON_NULL_CATALOG); 077 } 078 079 /** 080 * Special constructor used only by SourceCatalog for constructing its main 081 * group. 082 */ 083 SourceGroup(SourceCatalog container) 084 { 085 super(container); 086 087 tableListener = new Listener(this, DO_NOT_CHECK_FOR_NON_NULL_CATALOG); 088 } 089 090 /** Returns {@link Identifiable#UNIDENTIFIED}. */ 091 @Override 092 protected long getIdOfUnidentified() 093 { 094 return Identifiable.UNIDENTIFIED; 095 } 096 097 //============================================================================ 098 // CONTAINER 099 //============================================================================ 100 101 /** 102 * Sets the catalog to which this source group belongs. 103 * See {@link CatalogItemGroup#setCatalog(edu.nrao.sss.catalog.Catalog)} 104 * for more details. 105 * 106 * @param newCatalog the catalog to which this group belongs. 107 * 108 * @return <i>true</i> if {@code newCatalog} is the new catalog for this 109 * group. Note that this means if {@code newCatalog} is already 110 * the catalog of this group, the return value is <i>true</i>. 111 */ 112 @Override 113 public boolean setCatalog(SourceCatalog newCatalog) 114 { 115 //Quick exit if this group already belongs to newCatalog. 116 //(Intentional use of "==" here.) 117 if (getCatalog() == newCatalog) 118 return true; //"true" that newCatalog is this group's catalog 119 120 boolean haveNewCatalog = super.setCatalog(newCatalog); 121 122 //Change listener ONLY if newCatalog is our catalog. 123 if (haveNewCatalog) 124 { 125 tableListener = 126 (newCatalog == null) ? new Listener(this, CHECK_FOR_NON_NULL_CATALOG) 127 : newCatalog.getTableListener(); 128 129 synchronizeTablesWithListener(); 130 } 131 132 return haveNewCatalog; 133 } 134 135 /** 136 * Sets this group's catalog to {@code newCatalog} without 137 * contacting either the former or new catalog. This method is 138 * used only by the SourceCatalog class. 139 */ 140 @Override 141 protected void simplySetCatalog(SourceCatalog newCatalog) 142 { 143 super.simplySetCatalog(newCatalog); 144 145 tableListener = 146 (getCatalog() == null) ? new Listener(this, CHECK_FOR_NON_NULL_CATALOG) 147 : newCatalog.getTableListener(); 148 149 synchronizeTablesWithListener(); 150 } 151 152 //============================================================================ 153 // ADDING MEMBERS 154 //============================================================================ 155 156 /** 157 * Adds a new member to this source group. 158 * <p> 159 * If this group is not part of a catalog, and if the {@code newMember} is 160 * accepted into the group, this group will hold a reference to 161 * {@code newMember}. However, if this group is part of a catalog, and 162 * if the catalog holds an entry that is <i>equal</i> to {@code newMember}, 163 * this group will hold a reference to the equivalent entry in the 164 * catalog.</p> 165 * 166 * @param newMember a new member of this source group. 167 * 168 * @return <i>true</i> if this group changed as a result of the call. 169 * Reasons for a return value of <i>false</i>: 170 * <ol> 171 * <li>{@code newMember} is <i>null</i></li> 172 * <li>{@code newMember} is already a member of this group</li> 173 * </ol> 174 */ 175 @Override 176 public SourceCatalogEntry add(SourceCatalogEntry newMember) 177 { 178 SourceCatalogEntry addedMember = super.add(newMember); 179 180 //Special logic for handling listeners of lookup tables 181 if (addedMember instanceof SourceLookupTable) 182 { 183 SourceLookupTable newTable = (SourceLookupTable)addedMember; 184 185 newTable.setListener(tableListener); 186 187 addSourcesFrom(newTable); 188 } 189 190 return addedMember; 191 } 192 193 //Adds sources from table to this group, or this group's catalog, or 194 //replaces the table's source with a reference to an equal source 195 //that is already in this group, or this group's catalog. 196 private void addSourcesFrom(SourceLookupTable table) 197 { 198 //Redirect & quick exit if this group is in a catalog 199 if (getCatalog() != null) 200 { 201 ((SourceCatalog)getCatalog()).addSourcesFrom(table); 202 return; 203 } 204 205 //CATALOG IS NULL FROM HERE DOWN 206 207 //Get a list of the current sources from this group 208 List<SourceCatalogEntry> currentSources = getInternalMemberList(); 209 210 //For each source in the table, either replace the table's source 211 //with an EQUAL source from our current entries, or add the new 212 //source to our list of entries. 213 for (Date key : table.getKeySet()) 214 { 215 Source tableSource = table.get(key); 216 217 int index = currentSources.indexOf(tableSource); 218 219 if (index >= 0) 220 { 221 //Group has EQUAL source. If it is not IDENTICAL source, 222 //update the table so that it refers to the existing source. 223 Source currentMember = (Source)currentSources.get(index); 224 if (tableSource != currentMember) 225 table.put(key, currentMember); 226 } 227 else 228 { 229 //This group has no equal source, so add it to this group. 230 add(tableSource); 231 } 232 } 233 } 234 235 //============================================================================ 236 // REMOVING MEMBERS 237 //============================================================================ 238 239 @Override 240 public SourceCatalogEntry remove(SourceCatalogEntry member) 241 { 242 SourceCatalogEntry removedMember = super.remove(member); 243 244 if (removedMember != null) 245 finishRemovingMember(removedMember); 246 247 return removedMember; 248 } 249 250 @Override 251 public SourceCatalogEntry remove(int index) 252 { 253 SourceCatalogEntry removedMember = super.remove(index); 254 255 if (removedMember != null) 256 finishRemovingMember(removedMember); 257 258 return removedMember; 259 } 260 261 //Deals with removal of tables entries within tables. 262 private void finishRemovingMember(SourceCatalogEntry removedMember) 263 { 264 if (getCatalog() == null) 265 { 266 //If we're removing a table, and we're not part of a catalog, 267 //we need to remove our listener from the table. 268 //Note: do NOT remove the table's sources from this group. Those 269 //sources may have been put there directly, not via the add of 270 //the table. 271 if (removedMember instanceof SourceLookupTable) 272 { 273 ((SourceLookupTable)removedMember).removeListener(tableListener); 274 } 275 //If we're removing a source, and we're not part of a catalog, 276 //we need to remove the source from every table in this group. 277 //Note: even though we remove sources from tables, we do NOT 278 //remove even an empty table from the group here. 279 else if (removedMember instanceof Source) 280 { 281 try 282 { 283 for (SourceLookupTable table : getSourceTables()) 284 table.removeValue((Source)removedMember); 285 } 286 catch (RepositoryException ex) 287 { 288 //Can't happen, but just in case... 289 throw new RuntimeException("PROGRAMMER ERROR", ex); 290 } 291 } 292 } 293 } 294 295 /** 296 * Removes all sources from this group. 297 * @return a list of all sources that were removed from this group. 298 */ 299 public List<Source> removeAllSources() 300 { 301 List<Source> removedSources = new ArrayList<Source>(); 302 303 for (SourceCatalogEntry member : getAll()) 304 if (member instanceof Source) 305 removedSources.add((Source)remove(member)); 306 307 return removedSources; 308 } 309 310 /** 311 * Removes all source tables from this group. 312 * @return a list of all source tables that were removed from this group. 313 */ 314 public List<SourceLookupTable> removeAllSourceTables() 315 { 316 List<SourceLookupTable> removedTables = new ArrayList<SourceLookupTable>(); 317 318 for (SourceCatalogEntry member : getAll()) 319 if (member instanceof SourceLookupTable) 320 removedTables.add((SourceLookupTable)remove(member)); 321 322 return removedTables; 323 } 324 325 //============================================================================ 326 // FETCHING MEMBERS AND MEMBER INFORMATION 327 //============================================================================ 328 //---------------------------------------------------------------------------- 329 // SourceProvider Interface 330 //---------------------------------------------------------------------------- 331 332 /** 333 * Returns a set of all sources held by this group. 334 * <p> 335 * If this group is contained in a catalog, it may have references to 336 * sources that are not members of this group. This happens when the 337 * group contains {@code SourceLookupTable}s; the sources in those tables 338 * need not be in this group. In this situation, those sources are 339 * <i>not</i> present in the returned set. Only sources held directly 340 * by this group are returned.</p> 341 * <p> 342 * Note that the returned set is <i>not</i> held internally by this gorup. 343 * This means that any changes made to the set after calling this method 344 * will <i>not</i> be reflected in this object. The sources themselves, 345 * however, are the actual sources held in this group, so changes made to 346 * them <i>will</i> be reflected in this object. Furthermore, if this 347 * group is part of a catalog, then the catalog, and perhaps other groups, 348 * will be referring to these same source instances, so changes made to 349 * the sources in the returned set will be reflected in all those other 350 * containers.</p> 351 * 352 * @return a set of all sources held by this group. 353 * 354 * @throws RepositoryException under no conditions. 355 * 356 * @see #getAll() 357 * @see #getSourceTables() 358 * @see #getSources(SourceFilter) 359 */ 360 public List<Source> getSources() 361 throws RepositoryException 362 { 363 List<Source> results = new ArrayList<Source>(); 364 365 for (SourceCatalogEntry member : getAll()) 366 { 367 if (member instanceof Source) 368 { 369 results.add((Source)member); 370 } 371 } 372 373 return results; 374 } 375 376 /** 377 * Returns a set of all sources in this group that can pass through 378 * {@code filter}. If {@code filter} is <i>null</i>, it will be treated 379 * as a wide-open filter, allowing all sources to pass. 380 * <p> 381 * See {@link #getSources()} for details about the returned set.</p> 382 * 383 * @param filter a filter to apply to all sources in this group. Only 384 * those sources that may pass through {@code filter} 385 * will be in the returned set. 386 * 387 * @return a set of all sources in this group that can pass through 388 * {@code filter}. 389 * 390 * @throws RepositoryException under no conditions. 391 * 392 * @see #getAll() 393 * @see #getSources() 394 * @see #getSourceTables() 395 */ 396 public List<Source> getSources(Filter<Source> filter) 397 throws RepositoryException 398 { 399 //Quick exit for null filter 400 if (filter == null) 401 return getSources(); 402 403 List<Source> results = new ArrayList<Source>(); 404 405 for (SourceCatalogEntry member : getAll()) 406 { 407 if (member instanceof Source) 408 { 409 Source source = (Source)member; 410 if (filter.allows(source)) 411 results.add(source); 412 } 413 } 414 415 return results; 416 } 417 418 /** 419 * Returns a set of all source lookup tables held by this group. 420 * <p> 421 * Note that the returned set is <i>not</i> held internally by this gorup. 422 * This means that any changes made to the set after calling this method 423 * will <i>not</i> be reflected in this object. The tables themselves, 424 * however, are the actual tables held in this group, so changes made to 425 * them <i>will</i> be reflected in this object. Furthermore, if this 426 * group is part of a catalog, then the catalog, and perhaps other groups, 427 * will be referring to these same table instances, so changes made to 428 * the tables in the returned set will be reflected in all those other 429 * containers.</p> 430 * 431 * @return a set of all source lookup tables held by this group. 432 * 433 * @throws RepositoryException under no conditions. 434 * 435 * @see #getAll() 436 * @see #getSources() 437 */ 438 public List<SourceLookupTable> getSourceTables() 439 throws RepositoryException 440 { 441 List<SourceLookupTable> results = new ArrayList<SourceLookupTable>(); 442 443 for (SourceCatalogEntry member : getAll()) 444 { 445 if (member instanceof SourceLookupTable) 446 { 447 results.add((SourceLookupTable)member); 448 } 449 } 450 451 return results; 452 } 453 454 /* (non-Javadoc) 455 * @see SourceProvider#findSourceById(long) 456 */ 457 public Source findSourceById(long id) throws RepositoryException 458 { 459 Source result = null; 460 461 for (SourceCatalogEntry member : getAll()) 462 { 463 if ((member instanceof Source) && 464 (member.getId().longValue() == id)) 465 { 466 result = (Source)member; 467 break; 468 } 469 } 470 471 return result; 472 } 473 474 /* (non-Javadoc) 475 * @see SourceProvider#findSourceTableById(long) 476 */ 477 public SourceLookupTable findSourceTableById(long id) 478 throws RepositoryException 479 { 480 SourceLookupTable result = null; 481 482 for (SourceCatalogEntry member : getAll()) 483 { 484 if ((member instanceof SourceLookupTable) && 485 (member.getId().longValue() == id)) 486 { 487 result = (SourceLookupTable)member; 488 break; 489 } 490 } 491 492 return result; 493 } 494 495 /* (non-Javadoc) 496 * @see SourceProvider#findSourceByName(java.lang.String) 497 */ 498 public List<Source> findSourceByName(String name) 499 throws RepositoryException 500 { 501 List<Source> results = new ArrayList<Source>(); 502 503 for (SourceCatalogEntry member : getAll()) 504 { 505 if ((member instanceof Source) && 506 name.equalsIgnoreCase(member.getName())) 507 { 508 results.add((Source)member); 509 } 510 } 511 512 return results; 513 } 514 515 /* (non-Javadoc) 516 * @see SourceProvider#findSourceTableByName(java.lang.String) 517 */ 518 public List<SourceLookupTable> findSourceTableByName(String name) 519 throws RepositoryException 520 { 521 List<SourceLookupTable> results = new ArrayList<SourceLookupTable>(); 522 523 for (SourceCatalogEntry member : getAll()) 524 { 525 if ((member instanceof SourceLookupTable) && 526 name.equalsIgnoreCase(member.getName())) 527 { 528 results.add((SourceLookupTable)member); 529 } 530 } 531 532 return results; 533 } 534 535 //============================================================================ 536 // LISTENING TO TABLES 537 //============================================================================ 538 539 SourceTableListener getTableListener() { return tableListener; } 540 541 /** Sets the listener of all contained tables to this group's listener. */ 542 private void synchronizeTablesWithListener() 543 { 544 for (SourceCatalogEntry member : getAll()) 545 { 546 if (member instanceof SourceLookupTable) 547 { 548 ((SourceLookupTable)member).setListener(this.tableListener); 549 } 550 } 551 } 552 553 //============================================================================ 554 // TEXT 555 //============================================================================ 556 557 /** 558 * Creates a new group from the XML data in the given file. 559 * 560 * @param xmlFile the name of an XML file. This method will attempt to locate 561 * the file by using {@link Class#getResource(String)}. 562 * 563 * @return a new group from the XML data in the given file. 564 * 565 * @throws FileNotFoundException if the XML file cannot be found. 566 * 567 * @throws JAXBException if the schema file used (if any) is malformed, if 568 * the XML file cannot be read, or if the XML file is not 569 * schema-valid. 570 * 571 * @throws XMLStreamException if there is a problem opening the XML file, 572 * if the XML is not well-formed, or for some other 573 * "unexpected processing conditions". 574 */ 575 public static SourceGroup fromXml(String xmlFile) 576 throws JAXBException, XMLStreamException, FileNotFoundException 577 { 578 return JaxbUtility.getSharedInstance() 579 .xmlFileToObject(xmlFile, SourceGroup.class); 580 } 581 582 /** 583 * Creates a new group based on the XML data read from {@code reader}. 584 * 585 * @param reader the source of the XML data. 586 * If this value is <i>null</i>, <i>null</i> is returned. 587 * 588 * @return a new group based on the XML data read from {@code reader}. 589 * 590 * @throws XMLStreamException if the XML is not well-formed, 591 * or for some other "unexpected processing conditions". 592 * 593 * @throws JAXBException if anything else goes wrong during the 594 * transformation. 595 */ 596 public static SourceGroup fromXml(Reader reader) 597 throws JAXBException, XMLStreamException 598 { 599 return JaxbUtility.getSharedInstance() 600 .readObjectAsXmlFrom(reader, SourceGroup.class, null); 601 } 602 603 //---------------------------------------------------------------------------- 604 // XML Helpers 605 //---------------------------------------------------------------------------- 606 607 //These methods are here solely to help JAXB do its thing. 608 609 @XmlIDREF 610 @SuppressWarnings("unused") 611 private void setSourceTable(SourceLookupTable[] tables) 612 { 613 getInternalMemberList().addAll(Arrays.asList(tables)); 614 } 615 @SuppressWarnings("unused") 616 private SourceLookupTable[] getSourceTable() 617 { 618 ArrayList<SourceLookupTable> tables = new ArrayList<SourceLookupTable>(); 619 620 for (SourceCatalogEntry member : this.getInternalMemberList()) 621 if (member instanceof SourceLookupTable) 622 tables.add((SourceLookupTable)member); 623 624 return tables.toArray(new SourceLookupTable[tables.size()]); 625 } 626 627 @XmlIDREF 628 @SuppressWarnings("unused") 629 private void setSource(Source[] sources) 630 { 631 getInternalMemberList().addAll(Arrays.asList(sources)); 632 } 633 @SuppressWarnings("unused") 634 private Source[] getSource() 635 { 636 ArrayList<Source> sources = new ArrayList<Source>(); 637 638 for (SourceCatalogEntry member : this.getInternalMemberList()) 639 if (member instanceof Source) 640 sources.add((Source)member); 641 642 return sources.toArray(new Source[sources.size()]); 643 } 644 645 //============================================================================ 646 // 647 //============================================================================ 648 649 /** 650 * Returns a comparator that uses the name of the source groups sent to it. 651 * This comparator treats groups named "DEC +/-##" and "RA ##" as special 652 * cases, treating them as coming after all other group names. 653 * 654 * @return 655 * a comparator for sorting source groups by name, with special 656 * treatment for RA and DEC groups. 657 */ 658 public static Comparator<SourceGroup> getNameComparator() 659 { 660 return new SourceGroupNameComparator(); 661 } 662 663 /** 664 * Returns a source group that is a copy of this one. 665 * The clone is a deep clone; all the members in the returned group are 666 * copies of those in this group. 667 * <p> 668 * <b><u>Special Notes on Cloning Methodology</u></b> 669 * <ol> 670 * <li>The clone's ID property will be set to 671 * {@link Identifiable#UNIDENTIFIED}.</li> 672 * <li>The clone will belong to no catalog. That is, its catalog 673 * property will be <i>null</i>.</li> 674 * <li>Any tables that refer to sources that are not in this 675 * group will be cloned into the returned group, and clones of 676 * <i>the missing sources will be added to the returned group</i>.</li> 677 * </ol></p> 678 * 679 * @see #cloneIntoSameCatalog() 680 */ 681 @Override 682 public SourceGroup clone() 683 { 684 SourceGroup clonedGroup = null; 685 686 try 687 { 688 clonedGroup = (SourceGroup)super.cloneAllExceptMembers(); 689 690 //Used for straightening out source reference held in tables 691 Map<SourceLookupTable, SourceLookupTable> tableMap = 692 new HashMap<SourceLookupTable, SourceLookupTable>(); 693 694 //Create new member list & populate w/ clones. 695 //We'll partially clone the lookup tables and add them now, then 696 //come back and fix up their references below. 697 for (SourceCatalogEntry originalMember : this.getInternalMemberList()) 698 { 699 if (originalMember instanceof SourceLookupTable) 700 { 701 //We remove the sources from the cloned table prior to adding it 702 //to the cloned group because the add of the table may cause new 703 //source entries to be made in the cloned group. We want to 704 //control the order in which these source (clones) are added. 705 SourceLookupTable clonedTable = 706 ((SourceLookupTable)originalMember).cloneAllButSources(); 707 clonedTable.clear(); 708 clonedGroup.add(clonedTable); 709 //This map is used later to populate the clonedTable 710 tableMap.put(clonedTable, (SourceLookupTable)originalMember); 711 } 712 else 713 { 714 clonedGroup.add(originalMember.clone()); 715 } 716 } 717 718 //At this point the cloned group has clones of all Source 719 //and SourceLookupTable elements in the proper order. 720 //The tables, though, are empty and need to be told to 721 //point at the cloned sources. 722 List<SourceCatalogEntry> clonedGroupsMembers = 723 clonedGroup.getInternalMemberList(); 724 int memberCount = clonedGroupsMembers.size(); 725 for (int i=0; i < memberCount; i++) //See Note 1 for trivia 726 { 727 SourceCatalogEntry clonedMember = clonedGroupsMembers.get(i); 728 729 if (clonedMember instanceof SourceLookupTable) 730 { 731 SourceLookupTable clonedTable = (SourceLookupTable)clonedMember; 732 SourceLookupTable originalTable = tableMap.get(clonedTable); 733 734 for (Date key : originalTable.getKeySet()) 735 { 736 Source originalSource = originalTable.get(key); 737 738 int index = clonedGroupsMembers.indexOf(originalSource); 739 740 if (index >= 0) 741 { 742 //Cloned group has EQUAL source. Make clonedTable point at it. 743 clonedTable.put(key, (Source)clonedGroupsMembers.get(index)); 744 } 745 else 746 { 747 //Cloned group has no equal source. This is legitimate iff 748 //the original group belonged to a catalog. 749 if (this.getCatalog() == null) 750 throw new IllegalStateException( 751 "Programmer Error: Orphan group is missing source " + 752 originalSource.getName()); 753 754 //Clone original source & add. 755 Source clonedSource = originalSource.clone(); 756 clonedGroup.add(clonedSource); 757 clonedTable.put(key, clonedSource); 758 } 759 }//for key 760 }//s inst of table 761 }//for s : clone.members 762 } 763 catch (Exception ex) 764 { 765 throw new RuntimeException(ex); 766 } 767 768 return clonedGroup; 769 770 //Note 1: This line was originally 771 // "for (SourceCatalogEntry s : cloneGroup.members)". 772 // This worked fine throughout unit testing. However, when using 773 // Hibernate and cloning a previously saved catalog, a 774 // ConcurrentModificationException was thrown. The for loop in 775 // use now gets us around this problem. 776 } 777 778 /** 779 * Returns a source group that is a copy of this one. 780 * <p> 781 * The copying procedure uses a <i>shallow</i> approach: the returned 782 * copy will contain the same members as this group, not copies of the 783 * members. Furthermore, the returned group will belong to the same catalog 784 * as this group.</p> 785 * <p> 786 * If anything goes wrong during the cloning procedure, 787 * a {@code RuntimeException} will be thrown.</p> 788 */ 789 /* 790 public SourceGroup makeShallowCopy() 791 { 792 SourceGroup clone = null; 793 794 try 795 { 796 //This line takes care of the primitive fields properly 797 clone = (SourceGroup)super.clone(); 798 799 //We do NOT want the clone to have the same ID as the original. 800 //The ID is here for the persistence layer; it is in charge of 801 //setting IDs. To help it, we put the clone's ID in the uninitialized 802 //state. 803 clone.id = Identifiable.UNIDENTIFIED; 804 805 clone.name = "Copy " 806 } 807 catch (Exception ex) 808 { 809 throw new RuntimeException(ex); 810 } 811 812 return clone; 813 } 814 */ 815 816 /** 817 * Returns <i>true</i> if {@code o} is equal to this source group. 818 * <p> 819 * In addition to having the same name, the two groups must have the 820 * same number of members, and for every member of this group their 821 * must be exactly one equal member in {@code o}.</p> 822 * <p> 823 * <b>Ordering of Members</b><br/> 824 * The members of the two groups do <i>not</i> have to be in the 825 * same order. However, the sources in each group must be in the 826 * same position relative to all other sources in the group. The 827 * same is true of the tables. Example:</p><pre> 828 * This Group Group o 829 * ---------- ------- 830 * source1 source1 831 * tableA source2 832 * source2 source3 833 * source3 tableA 834 * tableB tableB</pre> 835 * The two groups above would be equal because their sources 836 * are in the same order, relative only to the other sources, 837 * and the tables are also in the same relative order. 838 */ 839 @Override 840 public boolean equals(Object o) 841 { 842 //Quick exit if o is this 843 if (o == this) 844 return true; 845 846 //Quick exit if clearly unequal 847 if (clearlyNotEqualTo(o)) 848 return false; 849 850 SourceGroup other = (SourceGroup)o; 851 852 //Compare the lists of sources and the lists of tables individually 853 ArrayList<Source> ourSources = new ArrayList<Source>(); 854 ArrayList<Source> theirSources = new ArrayList<Source>(); 855 856 ArrayList<SourceLookupTable> ourTables = 857 new ArrayList<SourceLookupTable>(); 858 ArrayList<SourceLookupTable> theirTables = 859 new ArrayList<SourceLookupTable>(); 860 861 for (SourceCatalogEntry member : this.getInternalMemberList()) 862 { 863 if (member instanceof SourceLookupTable) 864 ourTables.add((SourceLookupTable)member); 865 else 866 ourSources.add((Source)member); 867 } 868 869 for (SourceCatalogEntry member : other.getInternalMemberList()) 870 { 871 if (member instanceof SourceLookupTable) 872 theirTables.add((SourceLookupTable)member); 873 else 874 theirSources.add((Source)member); 875 } 876 877 return ourSources.equals(theirSources) && 878 ourTables.equals(theirTables); 879 } 880 881 /** Returns a hash code value for this group. */ 882 @Override 883 public int hashCode() 884 { 885 //Taken from the Effective Java book by Joshua Bloch. 886 //The constants 17 & 37 are arbitrary & carry no meaning. 887 int result = 17; 888 889 //NOTE: Keep this method in synch w/ equals 890 891 result = 37 * result + getName().hashCode(); 892 result = 37 * result + new Boolean(getNameIsLocked()).hashCode(); 893 894 //Put into same order as the equals method 895 ArrayList<Source> sources = new ArrayList<Source>(); 896 ArrayList<SourceLookupTable> tables = new ArrayList<SourceLookupTable>(); 897 898 for (SourceCatalogEntry member : this.getInternalMemberList()) 899 { 900 if (member instanceof SourceLookupTable) 901 tables.add((SourceLookupTable)member); 902 else 903 sources.add((Source)member); 904 } 905 906 result = 37 * result + sources.hashCode(); 907 result = 37 * result + tables.hashCode(); 908 909 return result; 910 } 911 912 //============================================================================ 913 // HELPER CLASSES 914 //============================================================================ 915 916 /** 917 * This listener is used only for groups whose catalog is null. 918 * UPDATE: This listener CAN be used by a group whose catalog is non-null. 919 * That situation is intended to occur only for the main group of a 920 * catalog. 921 */ 922 private class Listener implements SourceTableListener 923 { 924 //Technically, we don't need to have this client in order to 925 //access its variables -- it just makes the code clearer. 926 private SourceGroup client; 927 private boolean checkForNullCatalog; 928 929 Listener(SourceGroup group, boolean checkForNullCatalog) 930 { 931 this.client = group; 932 this.checkForNullCatalog = checkForNullCatalog; 933 } 934 935 public void sourceAdded(Source source, SourceLookupTable table) 936 { 937 if (checkForNullCatalog) 938 assertNullCatalog(); 939 940 assertHaveTable(table); 941 942 if (!client.contains(source)) 943 client.add(source); 944 } 945 946 public void sourcesAdded(Collection<Source> source, SourceLookupTable table) 947 { 948 if (checkForNullCatalog) 949 assertNullCatalog(); 950 951 assertHaveTable(table); 952 953 for (Source s : source) 954 if (!client.contains(s)) 955 client.add(s); 956 } 957 958 public void sourceRemoved(Source source, SourceLookupTable table) 959 { 960 //Intentionally doing nothing. 961 //Just because a source was removed from one of our client's tables does 962 //NOT mean the source should be removed from the client group. It might 963 //be that someone added this source directly to the group, not just via 964 //the add of the table. 965 } 966 967 public void entriesCleared(SourceLookupTable table) 968 { 969 //Intentionally doing nothing. 970 //See comments in sourceRemoved, above. 971 } 972 973 private void assertNullCatalog() 974 { 975 if (client.getCatalog() != null) 976 { 977 StringBuilder errMsg = new StringBuilder("PROGRAMMER ERROR. "); 978 errMsg.append("SourceGroup ").append(client.getName()); 979 errMsg.append(" is listening to tables even though its catalog is null."); 980 981 throw new RuntimeException(errMsg.toString()); 982 } 983 } 984 985 private void assertHaveTable(SourceLookupTable table) 986 { 987 if (!client.contains(table)) 988 { 989 StringBuilder errMsg = new StringBuilder("PROGRAMMER ERROR. "); 990 errMsg.append("SourceGroup ").append(client.getName()); 991 errMsg.append(" is listening to a table that it doesn't own."); 992 993 throw new RuntimeException(errMsg.toString()); 994 } 995 } 996 } 997 998 //============================================================================ 999 // 1000 //============================================================================ 1001 /* 1002 //Test name comparator 1003 public static void main(String... args) throws Exception 1004 { 1005 List<SourceGroup> groups = new ArrayList<SourceGroup>(); 1006 1007 SourceGroup g; 1008 g = new SourceGroup(); g.setName("DEC +80"); groups.add(g); 1009 g = new SourceGroup(); g.setName("3C"); groups.add(g); 1010 g = new SourceGroup(); g.setName("Abc"); groups.add(g); 1011 g = new SourceGroup(); g.setName("DEC -80"); groups.add(g); 1012 g = new SourceGroup(); g.setName("RA 22"); groups.add(g); 1013 g = new SourceGroup(); g.setName("Moe"); groups.add(g); 1014 g = new SourceGroup(); g.setName("RA 13"); groups.add(g); 1015 g = new SourceGroup(); g.setName("Inky"); groups.add(g); 1016 g = new SourceGroup(); g.setName("DEC -00"); groups.add(g); 1017 g = new SourceGroup(); g.setName("Manny"); groups.add(g); 1018 g = new SourceGroup(); g.setName("Blinky"); groups.add(g); 1019 g = new SourceGroup(); g.setName("3C new"); groups.add(g); 1020 g = new SourceGroup(); g.setName("RA 01"); groups.add(g); 1021 g = new SourceGroup(); g.setName("DEC +30"); groups.add(g); 1022 g = new SourceGroup(); g.setName("DEC +00"); groups.add(g); 1023 g = new SourceGroup(); g.setName("Jack"); groups.add(g); 1024 g = new SourceGroup(); g.setName("RA 11"); groups.add(g); 1025 g = new SourceGroup(); g.setName("Pinky"); groups.add(g); 1026 g = new SourceGroup(); g.setName("Clyde"); groups.add(g); 1027 1028 for (SourceGroup grp : groups) 1029 System.out.println(grp.getName()); 1030 1031 Collections.sort(groups, SourceGroup.getNameComparator()); 1032 System.out.println(); 1033 1034 for (SourceGroup grp : groups) 1035 System.out.println(grp.getName()); 1036 } 1037 */ 1038 } 1039 1040 class SourceGroupNameComparator implements Comparator<SourceGroup> 1041 { 1042 private static final String RA = "RA "; 1043 private static final String DEC = "DEC "; 1044 1045 //Sort by name EXCEPT put Dec & RA groups last 1046 public int compare(SourceGroup g1, SourceGroup g2) 1047 { 1048 String name1 = g1.getName(); 1049 String name2 = g2.getName(); 1050 1051 //RA groups come last 1052 if (name1.startsWith(RA) && !name2.startsWith(RA)) return +1; 1053 if (!name1.startsWith(RA) && name2.startsWith(RA)) return -1; 1054 1055 //DEC groups come before only the RA groups 1056 if (name1.startsWith(DEC)) 1057 { 1058 if ( name2.startsWith(RA) ) return -1; 1059 if (!name2.startsWith(DEC)) return +1; 1060 else return compareDec(name1, name2); 1061 } 1062 if (name2.startsWith(DEC)) 1063 { 1064 if ( name1.startsWith(RA) ) return +1; 1065 if (!name1.startsWith(DEC)) return -1; 1066 } 1067 1068 //At this point either: 1069 // 1. Both names start with "RA " or 1070 // 2. Neither name starts w/ "DEC " or "RA " 1071 return name1.compareTo(name2); 1072 } 1073 1074 private int compareDec(String dec1, String dec2) 1075 { 1076 boolean dec1IsNeg = (dec1.charAt(DEC.length()) == '-'); 1077 boolean dec2IsNeg = (dec2.charAt(DEC.length()) == '-'); 1078 1079 //See if we have opposite signs 1080 if ( dec1IsNeg && !dec2IsNeg) return -1; 1081 if (!dec1IsNeg && dec2IsNeg) return +1; 1082 1083 int natural = dec1.compareTo(dec2); 1084 1085 return dec1IsNeg ? -natural : natural; 1086 } 1087 }