001 package edu.nrao.sss.catalog; 002 003 import java.io.Writer; 004 import java.util.ArrayList; 005 import java.util.Collection; 006 import java.util.Collections; 007 import java.util.Comparator; 008 import java.util.List; 009 import java.util.Set; 010 import java.util.concurrent.CopyOnWriteArraySet; 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.XmlRootElement; 017 import javax.xml.bind.annotation.XmlTransient; 018 019 import edu.nrao.sss.util.JaxbUtility; 020 021 /** 022 * A grouping of {@link CatalogItem catalog items}. 023 * A {@code CatalogItemGroup} is normally contained in a 024 * {@link Catalog} and is a way of collecting together 025 * items that have similar traits. 026 * <p> 027 * <b>Version Info:</b> 028 * <table style="margin-left:2em"> 029 * <tr><td>$Revision: 2232 $</td></tr> 030 * <tr><td>$Date: 2009-04-23 13:38:46 -0600 (Thu, 23 Apr 2009) $</td></tr> 031 * <tr><td>$Author: dharland $ (last person to modify)</td></tr> 032 * </table></p> 033 * 034 * @author David M. Harland 035 * @since 2006-10-23 036 */ 037 @XmlRootElement 038 public class CatalogItemGroup<I extends CatalogItem<I>, 039 G extends CatalogItemGroup<I,G,C>, 040 C extends Catalog<I,G,C>> 041 implements Cloneable 042 { 043 private static final String INIT_NAME = "[New Group]"; 044 //private static final String NO_NAME = "[Unamed Group]"; 045 046 //============================================================================ 047 // INSTANCE VARIABLES AND CONSTRUCTORS 048 //============================================================================ 049 050 //IDENTIFICATION 051 private long id; 052 private String name; 053 054 //CONTAINER & CONTENTS 055 private List<I> items; 056 private C catalog; 057 058 //OTHER PROPERTIES 059 private boolean nameIsLocked; 060 private List<String> notes; 061 062 //NONPERSISTED PROPERTIES 063 private Set<CatalogItemGroupListener<I,G,C>> listeners; 064 private boolean suppressNotification; 065 066 /** 067 * Creates a new group with a default name and that belongs to no catalog. 068 */ 069 public CatalogItemGroup() 070 { 071 this(null, INIT_NAME); 072 } 073 074 /** 075 * Creates a new group that belongs to {@code container}. 076 * 077 * @param container the name of the one catalog to which this group belongs. 078 * This value may be <i>null</i>. 079 * 080 * @param nameOfGroup the name of this group. If this value is <i>null</i>, 081 * a non-null default name will be used. 082 */ 083 @SuppressWarnings("unchecked") 084 public CatalogItemGroup(C container, String nameOfGroup) 085 { 086 id = getIdOfUnidentified(); 087 name = (nameOfGroup == null) ? INIT_NAME : nameOfGroup; 088 nameIsLocked = false; 089 items = new ArrayList<I>(); 090 notes = new ArrayList<String>(); 091 suppressNotification = false; 092 093 listeners = new CopyOnWriteArraySet<CatalogItemGroupListener<I,G,C>>(); 094 095 if (container != null) 096 { 097 container.addGroup((G)this); //Sets this.catalog 098 } 099 else 100 { 101 catalog = null; 102 } 103 } 104 105 /** 106 * Special constructor used only by Catalog for constructing its main group. 107 */ 108 protected CatalogItemGroup(C container) 109 { 110 id = getIdOfUnidentified(); 111 name = container.getReservedGroupName(); 112 nameIsLocked = true; 113 items = new ArrayList<I>(); 114 notes = new ArrayList<String>(); 115 suppressNotification = false; 116 catalog = container; 117 118 listeners = new CopyOnWriteArraySet<CatalogItemGroupListener<I,G,C>>(); 119 } 120 121 //============================================================================ 122 // IDENTIFICATION 123 //============================================================================ 124 125 /** 126 * Sets the ID of this group. If {@code id} is <i>null</i>, this group's ID 127 * ID will be set to an <i>unidentified</i> ID. 128 * 129 * @param id a new ID for this group. 130 */ 131 protected void setId(Long id) 132 { 133 this.id = (id == null) ? getIdOfUnidentified() : id; 134 } 135 136 /** 137 * Returns a value that represents an undefined ID. 138 * Subclasses may override this method to suit their needs. 139 */ 140 protected long getIdOfUnidentified() 141 { 142 return -1L; 143 } 144 145 /** 146 * Returns this group's ID. 147 * @return this group's ID. 148 */ 149 @XmlAttribute 150 public Long getId() 151 { 152 return id; 153 } 154 155 /** 156 * Attempts to change the name of this group to {@code newName}. 157 * <p> 158 * This method exists for the use of frameworks that rely on 159 * java-bean naming conventions and set-methods that are of 160 * type void. The preferred method for all other clients is 161 * {@link #setNameAndConfirm(String)}, which will let the caller 162 * know whether or not the change of name was successful. 163 * If this method fails to change this group's name it does so 164 * <em>silently</em>.</p> 165 * 166 * @param newName the new name for this group. 167 * 168 * @see #setNameAndConfirm(String) 169 */ 170 public void setName(String newName) 171 { 172 setNameAndConfirm(newName); 173 } 174 175 /** 176 * Attempts to set the name of this group and returns <i>true</i> if 177 * this group's name was changed. 178 * 179 * @param newName the new name for this group. 180 * 181 * @return <i>true</i> if the name of this group was changed to 182 * {@code newName}. Reasons for a return value of <i>false</i>: 183 * <ol> 184 * <li>The name of this group is locked. (This is controlled by 185 * the containing catalog, if this group is a member of a 186 * catalog.)</li> 187 * <li>{@code newName} is <i>null</i>.</li> 188 * <li>{@code newName} is the empty string <tt>""</tt>.</li> 189 * <li>This group's name is already equal to {@code newName}.</li> 190 * <li>This group belongs to a catalog and another group in the 191 * catalog has a name of {@code newName}. (The catalog is 192 * now enforcing uniqueness of its groups' names.)</li> 193 * </ol> 194 */ 195 public boolean setNameAndConfirm(String newName) 196 { 197 boolean nameChanged; 198 199 //Will not change name if name is locked, if it's null, 200 //or if it's same as current name 201 if (nameIsLocked || (newName == null) || newName.equals(name) || newName.equals("")) 202 { 203 nameChanged = false; 204 } 205 else //name is NOT locked, newName is NOT null, AND newName is truly new 206 { 207 if (catalog == null) 208 { 209 name = newName; 210 nameChanged = true; 211 } 212 else //group belongs to catalog 213 { 214 if (catalog.containsGroupNamed(newName)) 215 { 216 nameChanged = false; 217 } 218 else //catalog has no group w/ this name 219 { 220 name = newName; 221 nameChanged = true; 222 } 223 } 224 } 225 226 return nameChanged; 227 } 228 229 /** 230 * If necessary, this method gives this group a new name of the form 231 * <tt><i>currentName</i> (Copy <i>#</i>)</tt>. This is necessary 232 * only if there is another group in the client catalog with the 233 * same name, or if this group is using the reserved name. 234 * The main purpose of this method is to allow a user to add 235 * a group to a catalog that already has a group with the same 236 * name as this one. 237 * 238 * @param client the catalog that is making the request to rename. 239 */ 240 void adjustNameForCatalog(C client) 241 { 242 StringBuilder buff = new StringBuilder(this.name); 243 244 //See if this group already has the " (Copy #)" suffix. 245 //If so, delete suffix from buffer. 246 final String suffix = " (Copy "; 247 int suffixIndex = buff.indexOf(suffix); 248 if (suffixIndex >= 0) 249 buff.delete(suffixIndex, buff.length()); 250 251 //baseName is everything in name before " (Copy #)" 252 String baseName = buff.toString(); 253 254 //copyBase is baseName and " (Copy " 255 String copyBase = baseName + suffix; 256 257 //Search all groups (even this one, if part of client) looking for 258 //names that start with copyBase and names that are exactly baseName. 259 int copyNumber = 0; 260 for (G group : client.getGroups()) 261 if (group.getName().equals(baseName) || 262 group.getName().startsWith(copyBase)) 263 copyNumber++; 264 265 //If someone has cloned the main group it's possible that this group 266 //has the reserved name. We'll have to change that name, and allow 267 //clients to do so, too, by unlocking the name. 268 if (baseName.equals(client.getReservedGroupName())) 269 this.nameIsLocked = false; 270 271 if (copyNumber > 0) 272 { 273 buff.append(suffix).append(copyNumber).append(')'); 274 this.name = buff.toString(); 275 } 276 } 277 278 /** 279 * Returns the name of this group. 280 * @return the name of this group. 281 */ 282 public String getName() 283 { 284 return name; 285 } 286 287 /** Restricts the ability to change this group's name. */ 288 void setNameIsLocked(boolean isLocked) 289 { 290 nameIsLocked = isLocked; 291 } 292 293 /** 294 * Returns <i>true</i> if the ability to change this group's name is 295 * restricted. 296 */ 297 protected boolean getNameIsLocked() 298 { 299 return nameIsLocked; 300 } 301 302 //============================================================================ 303 // OTHER PROPERTIES 304 //============================================================================ 305 306 /** 307 * Returns a list of notes about this group. 308 * Each note is free-form text with no particular structure. 309 * <p> 310 * This method returns the list actually held by this 311 * {@code CatalogItemGroup}, so 312 * any list manipulations may be performed by first fetching the list and 313 * then operating on it.</p> 314 * 315 * @return a list of notes about this catalog. 316 */ 317 @XmlElementWrapper 318 @XmlElement(name="note") 319 public List<String> getNotes() 320 { 321 return notes; 322 } 323 324 /** This is here for mechanisms that need setX/getX pairs, such as JAXB. */ 325 void setNotes(List<String> replacementList) 326 { 327 notes = (replacementList == null) ? new ArrayList<String>() 328 : replacementList; 329 } 330 331 //============================================================================ 332 // CONTAINER 333 //============================================================================ 334 335 /** 336 * Sets the catalog to which this group belongs. 337 * <p> 338 * If this group is currently contained in a catalog 339 * that is not the same as the {@code newCatalog} parameter, 340 * the current catalog will be told to remove this group 341 * from its collection of groups. If {@code newCatalog} 342 * is not <i>null</i>, it will be told to add this group 343 * to its collection. Finally, this group's catalog 344 * will be set to {@code newCatalog}, even if it is <i>null</i>.</p> 345 * <p> 346 * The current catalog has the right to deny movement of this group 347 * to a new catalog. If the request is denied, this group will 348 * remain with its current catalog.</p> 349 * <p> 350 * Passing this method a {@code newCatalog} of <i>null</i> has the 351 * effect of disconnecting this group from any catalog.</p> 352 * 353 * @param newCatalog the catalog to which this group belongs. 354 * 355 * @return <i>true</i> if {@code newCatalog} is now the catalog for this 356 * group. Note that this means if {@code newCatalog} is already 357 * the catalog of this group, the return value is <i>true</i>. 358 */ 359 @SuppressWarnings("unchecked") 360 public boolean setCatalog(C newCatalog) 361 { 362 C formerCatalog = this.catalog; 363 364 //Quick exit if this group may not be removed from its catalog 365 if ((formerCatalog != null) && !formerCatalog.allowsRemovalOf((G)this)) 366 return false; 367 368 //Quick exit if this group already belongs to newCatalog. 369 //(Intentional use of "==" here.) 370 if (formerCatalog == newCatalog) 371 return true; //"true" that newCatalog is this group's catalog 372 373 boolean catalogIsNewCatalog = false; 374 375 //If newCatalog is NOT null, it will be in charge of telling 376 //formerCatalog about its loss of this group. 377 if (newCatalog != null) 378 { 379 catalogIsNewCatalog = newCatalog.addGroup((G)this); 380 } 381 //Otherwise, we must tell the former catalog ourselves 382 else //newCatalog == null 383 { 384 if (formerCatalog != null) 385 catalogIsNewCatalog = (formerCatalog.removeGroup((G)this) != null); 386 } 387 388 //Make the change, or rollback to pre-call state 389 catalog = catalogIsNewCatalog ? newCatalog : formerCatalog; 390 391 return catalogIsNewCatalog; 392 } 393 394 /** 395 * Sets this group's catalog to {@code newCatalog} without 396 * contacting either the former or new catalog. This method is 397 * used only by the Catalog class. 398 */ 399 protected void simplySetCatalog(C newCatalog) 400 { 401 this.catalog = newCatalog; 402 } 403 404 /** 405 * Returns the catalog to which this group belongs, if any. 406 * <p> 407 * This group may be one of several that belong to 408 * the same catalog. If this group belongs to 409 * no catalog, the value returned is <i>null</i>.</p> 410 * 411 * @return the catalog to which this group belongs, if any. 412 * 413 * @see #hasCatalog() 414 */ 415 @XmlTransient 416 public C getCatalog() 417 { 418 return catalog; 419 } 420 421 /** 422 * Returns <i>true</i> if this group has a non-null catalog. 423 * <p> 424 * Catalog item groups should normally be contained within, and 425 * therefore have a non-null, catalog. However, there are some 426 * situations where this method will return <i>false</i>: 427 * <ol> 428 * <li>This group has just been created and its catalog 429 * has not yet been set.</li> 430 * <li>A client removed this group from its catalog and 431 * did not place it in a new catalog.</li> 432 * <li>A client explicitly set this group's catalog to 433 * <i>null</i>.</li> 434 * </ol></p> 435 * 436 * @return <i>true</i> if this group has a non-null catalog. 437 * Therefore a return value of <i>true</i> means that 438 * you can call {@link #getCatalog()} and know that 439 * it will return a non-null object. 440 */ 441 public boolean hasCatalog() 442 { 443 return catalog != null; 444 } 445 446 //============================================================================ 447 // ADDING MEMBERS 448 //============================================================================ 449 450 /** 451 * Adds a new member to this group. 452 * <p> 453 * The new member is added to this group only if it is not <i>null</i>, and 454 * this group currently contains no member that is equal to it. 455 * (To bypass the equality restriction, use 456 * {@link #addEvenIfEqualToCurrentMember(CatalogItem)}). If this 457 * group belongs to a catalog, the catalog is also informed about the new 458 * member.</p> 459 * <p> 460 * If this group is part of a catalog, and if the catalog 461 * has an item that is equal to {@code newMember}, then that equivalent 462 * item is used in place of {@code newMember} as the new member of this 463 * group. This ensures that an item copied from one group of a catalog 464 * to another group of the same catalog is a single item that is a 465 * member of the two groups. The next section shows what happens to 466 * this group, its catalog (if it has one), and the value returned by 467 * this method in a few situations.</p> 468 * <p> 469 * <b><u>A. This Group Does <i>Not</i> Belong to a Catalog</u></b></p> 470 * <ol> 471 * <li><tt>newMember</tt> is <i>null</i> or its addition to this group 472 * fails<sup>1</sup>.<br/> 473 * Nothing is added to this group and the value returned is <i>null</i>. 474 * </li><br/> 475 * <li>This group contains no members that are equal to <tt>newMember</tt>.<br/> 476 * <tt>newMember</tt> is added to this group and returned. 477 * </li><br/> 478 * <li>This group contains a member that is equal to <tt>newMember</tt>.<br/> 479 * The member equal to <tt>newMember</tt> that is already present in 480 * this group is returned. Nothing is added to this group. 481 * </li> 482 * </ol> 483 * <p><b><u>B. This Group <i>Does</i> Belong to a Catalog</u></b></p> 484 * <ol> 485 * <li><tt>newMember</tt> is <i>null</i> or its addition to this group 486 * or its catalog fails<sup>1</sup>.<br/> 487 * Nothing is added to this group or its catalog and the value 488 * returned is <i>null</i>. 489 * </li><br/> 490 * <li>This group and its catalog contain no members that are equal to 491 * <tt>newMember</tt>.<br/> 492 * <tt>newMember</tt> is added to this group and to its catalog, 493 * and is returned. 494 * </li><br/> 495 * <li>This group contains no members that are equal to <tt>newMember</tt>, 496 * but its catalog does.<br/> 497 * The member equal to <tt>newMember</tt> that is already present in 498 * the catalog is added to this group and returned. Nothing is added 499 * to the catalog. 500 * </li><br/> 501 * <li>This group, and therefore its catalog, contains a member that 502 * is equal to <tt>newMember</tt>.<br/> 503 * The member equal to <tt>newMember</tt> that is already present in 504 * this group and its is returned. 505 * Nothing is added to this group or its catalog. 506 * </li> 507 * </ol> 508 * <p> 509 * In general, the return value is the {@code newMember} parameter. However, 510 * this is not always true when this group is part of a catalog. See the 511 * description of the returned value, below.</p> 512 * <p> 513 * <sup>1</sup><i>With the exception of a programming error within this 514 * class or the Catalog class, this is not expected to happen.</i></p> 515 * 516 * @param newMember 517 * a prospective new member of this group. 518 * 519 * @return 520 * <i>null</i>, <tt>newMember</tt>, or an item that is equal to 521 * <tt>newMember</tt>, as outlined in the situations above. 522 * 523 * @see #addEvenIfEqualToCurrentMember(CatalogItem) 524 */ 525 public I add(I newMember) 526 { 527 I added = null; 528 529 if (newMember != null) 530 { 531 int position = items.indexOf(newMember); 532 533 added = position < 0 ? addNewMember(size(), newMember) : items.get(position); 534 } 535 536 return added; 537 } 538 539 /** 540 * Adds a new member to this group at the specified position. 541 * <p> 542 * For a full description of the behavior of this method and its return 543 * values, see {@link #add(CatalogItem)}.</p> 544 * 545 * @param index the position at which to insert the new member. 546 * 547 * @param newMember a prospective new member of this group. 548 * 549 * @return the member that was actually added to, or was already a part of, 550 * this group. 551 * 552 * @see #addEvenIfEqualToCurrentMember(int, CatalogItem) 553 */ 554 public I add(int index, I newMember) 555 { 556 I added = null; 557 558 if (newMember != null) 559 { 560 int position = items.indexOf(newMember); 561 562 if (position == -1) //this is, indeed, a new member of this group 563 { 564 added = addNewMember(index, newMember); 565 } 566 else //already have equal member in this group 567 { 568 added = items.get(position); 569 move(position, index); 570 } 571 } 572 573 return added; 574 } 575 576 /** 577 * @param newMember 578 * 579 public void addByForcingUniqueness(NameableCatalogItem<I> newMember) 580 { 581 I added = null; 582 583 if (newMember != null) 584 { 585 int position = items.indexOf(newMember); 586 587 //Not found in group, but might be an equal one in catalog 588 if (position == -1) 589 { 590 see if equal member is in catalog 591 if yes, change name 592 else vanilla add 593 } 594 else //already have equal member in this group 595 { 596 need a unique name 597 if catalog != null, need unique name catalog-wied 598 } 599 } 600 601 return added; 602 } 603 */ 604 //Adds new member w/out any checks. Don't call w/ null newMember. 605 @SuppressWarnings("unchecked") 606 private I addNewMember(int index, I newMember) 607 { 608 //Update catalog or use it to get reference to equal member 609 if (catalog != null) 610 { 611 //If the catalog has a resource that is EQUAL to newMember, 612 //replace the member sent to us with the one from the catalog. 613 if (catalog.contains(newMember)) 614 { 615 newMember = catalog.getItems().get(catalog.indexOf(newMember)); 616 } 617 else //!catalog.contains(newMember), so tell catalog about new item 618 { 619 catalog.addItem(newMember, (G)this); 620 } 621 } 622 623 //Try to add the newMember to this group. 624 items.add(index, newMember); 625 tellListenersAboutNewMember(newMember, index); 626 627 //Return the parameter, the equivalent item from the catalog, or null 628 return newMember; 629 } 630 631 /** 632 * Adds the collection of {@code newMembers} to this group. 633 * Only non-null candidates that are not already members of 634 * this group are added. 635 * 636 * @param newMembers a collection of prospective new members. 637 * 638 * @return a collection of the members actually added to this group. 639 * 640 * @see #add(CatalogItem) 641 */ 642 public Collection<I> addAll(Collection<? extends I> newMembers) 643 { 644 Collection<I> addedMembers = new ArrayList<I>(); 645 646 if (newMembers != null) 647 { 648 for (I member : newMembers) 649 addedMembers.add(this.add(member)); 650 } 651 652 return addedMembers; 653 } 654 655 /** 656 * Adds the collection of {@code newMembers} to this group at the 657 * specified position. 658 * Only non-null candidates that are not already members of 659 * this group are added. 660 * 661 * @param index the position at which to insert the first new member. 662 * Subsequent new members are added at successive positions. 663 * 664 * @param newMembers a collection of prospective new members. 665 * 666 * @return a collection of the members actually added to this group. 667 * 668 * @see #add(CatalogItem) 669 */ 670 public Collection<I> addAll(int index, Collection<? extends I> newMembers) 671 { 672 Collection<I> addedMembers = new ArrayList<I>(); 673 674 if (newMembers != null) 675 { 676 for (I member : newMembers) 677 addedMembers.add(this.add(index++, member)); 678 } 679 680 return addedMembers; 681 } 682 683 /** 684 * Adds {@code newMember} even if this group already has a member to which it 685 * is equal. Contrast this with {@link #add(CatalogItem)}. 686 * <p> 687 * The only times {@code newMember} is <i>not</i> added to this group are 688 * when it is <i>null</i> and when a reference to it is already contained 689 * in this group.</p> 690 * 691 * @param newMember a potential new member for this group. 692 * 693 * @return {@code newMember}. 694 */ 695 public I addEvenIfEqualToCurrentMember(I newMember) 696 { 697 return addEvenIfEqualToCurrentMember(size(), newMember); 698 } 699 700 /** 701 * Adds {@code newMember} at the specified position 702 * even if this group already has a member to which it 703 * is equal. Contrast this with {@link #add(int, CatalogItem)}. 704 * <p> 705 * The only times {@code newMember} is <i>not</i> added to this group are 706 * when it is <i>null</i> and when a reference to it is already contained 707 * in this group.</p> 708 * 709 * @param index the position at which to add {@code newMember}. 710 * 711 * @param newMember a potential new member for this group. 712 * 713 * @return {@code newMember}. 714 */ 715 @SuppressWarnings("unchecked") 716 public I addEvenIfEqualToCurrentMember(int index, I newMember) 717 { 718 //Quick exit if newMember is null 719 if (newMember == null) 720 return null; 721 722 //Quick exit if newMember itself is already present 723 for (I currentMember : items) 724 if (currentMember == newMember) 725 return newMember; 726 727 //We now know that newMember is not null & not already in this group. 728 //Bypass the normal members.contains(newMember) logic and force it in. 729 items.add(index, newMember); 730 tellListenersAboutNewMember(newMember, index); 731 732 //The catalog must also be told to accept this member, even if it already 733 //has an equal-but-not-identical item. This group and the main catalog 734 //must each hold a reference to newMember. 735 if (catalog != null) 736 catalog.addItemEvenIfEqualToCurrentItem(newMember, (G)this); 737 738 return newMember; 739 } 740 741 @SuppressWarnings("unchecked") 742 protected void tellListenersAboutNewMember(I newMember, int index) 743 { 744 if (!suppressNotification) 745 for (CatalogItemGroupListener<I,G,C> listener : listeners) 746 listener.memberAdded((G)this, newMember, index); 747 } 748 749 //============================================================================ 750 // REMOVING MEMBERS 751 //============================================================================ 752 753 /** 754 * Removes {@code member} from this group, if present. 755 * <p> 756 * The returned element is either <i>null</i> (if this group had no member 757 * equal to {@code member}) or is the removed member, which is the first 758 * member of this group that is equal to {@code member}.</p> 759 * 760 * @param member the member that is to be removed from this group. 761 * 762 * @return the removed element, or <i>null</i> if no element was removed. 763 */ 764 public I remove(I member) 765 { 766 int index = indexOf(member); 767 768 return index < 0 ? null : remove(index); 769 } 770 771 /** 772 * Removes the member at {@code index} from this group. 773 * 774 * @param index the index of the member to be removed. 775 * 776 * @return the member formerly at {@code index}. 777 * 778 * @throws IndexOutOfBoundsException if the index is out of range 779 * (index < 0 || index >= size()). 780 */ 781 @SuppressWarnings("unchecked") 782 public I remove(int index) 783 { 784 I formerMember = items.remove(index); 785 tellListenersAboutFormerMember(formerMember, index); 786 787 if (formerMember != null) 788 { 789 //Let catalog know about removal. It will decide whether or not to 790 //remove member entirely from itself. 791 if (catalog != null) 792 catalog.removeItem(formerMember, (G)this); 793 } 794 795 return formerMember; 796 } 797 798 /** 799 * Removes all elements of {@code unwantedMembers} that are contained in this 800 * group. 801 * 802 * @param unwantedMembers the members that are to be removed from this group. 803 * 804 * @return the former members that were removed from this group as a result 805 * of the call. 806 */ 807 public Collection<I> removeAll(Collection<? extends I> unwantedMembers) 808 { 809 Collection<I> formerMembers = new ArrayList<I>(); 810 811 if (unwantedMembers != null) 812 { 813 for (I member : unwantedMembers) 814 { 815 I formerMember = remove(member); 816 if (formerMember != null) 817 formerMembers.add(formerMember); 818 } 819 } 820 821 return formerMembers; 822 } 823 824 /** Removes all members from this group. */ 825 @SuppressWarnings("unchecked") 826 public void clear() 827 { 828 //We clone the list of members because we will do things in this order: 829 //1. Remove the members from this group. 830 //2. Notify listeners about the removals. 831 //3. Tell the catalog about the removals, in case it needs to do anything. 832 List<I> clonedMemberList = null; 833 synchronized(this) { 834 clonedMemberList = (List<I>)((ArrayList<I>)items).clone(); 835 } 836 837 //Remove this group's members. Do not use items for remainder of method. 838 items.clear(); 839 840 //Notify listeners. Traversing the member list backwards ensures 841 //that the index sent to listener matches current state of list. 842 for (int m=clonedMemberList.size()-1; m >= 0; m--) 843 tellListenersAboutFormerMember(clonedMemberList.get(m), m); 844 845 //The catalog will decide whether or not it actually removes members 846 if (catalog != null) 847 catalog.removeItems(clonedMemberList, (G)this); 848 } 849 850 @SuppressWarnings("unchecked") 851 protected void tellListenersAboutFormerMember(I formerMember, int index) 852 { 853 if (!suppressNotification) 854 for (CatalogItemGroupListener<I,G,C> listener : listeners) 855 listener.memberRemoved((G)this, formerMember, index); 856 } 857 858 //============================================================================ 859 // MOVING MEMBERS 860 //============================================================================ 861 862 /** 863 * Moves {@code member} to an index one higher than its current index. 864 * The member will not be moved if it is not in this group, or if 865 * it is already in the highest position. 866 * <p> 867 * If {@code member} is in this group in multiple positions, the instance 868 * with the <i>highest</i> index will be moved.</p> 869 * 870 * @param member the member to be moved. 871 * 872 * @return if no movement occurred, <i>null</i> is returned. Otherwise 873 * the member that was moved is returned. This will be either 874 * {@code member}, or an equal member that was already part of 875 * this group. 876 */ 877 public I incrementIndexOf(I member) 878 { 879 I movedMember = null; 880 881 int currentIndex = items.indexOf(member); 882 int highestIndex = items.size() - 1; 883 884 if ((0 <= currentIndex) && (currentIndex < highestIndex)) 885 { 886 suppressNotification = true; 887 888 movedMember = remove(currentIndex); 889 items.add(currentIndex + 1, movedMember); 890 891 suppressNotification = false; 892 tellListenersAboutMovedMember(movedMember, currentIndex, currentIndex+1); 893 } 894 895 return movedMember; 896 } 897 898 /** 899 * Moves {@code member} to an index one lower than its current index. 900 * The member will not be moved if it is not in this group, or if 901 * it is already in the lowest position. 902 * <p> 903 * If {@code member} is in this group in multiple positions, the instance 904 * with the <i>lowest</i> index will be moved.</p> 905 * 906 * @param member the member to be moved. 907 * 908 * @return if no movement occurred, <i>null</i> is returned. Otherwise 909 * the member that was moved is returned. This will be either 910 * {@code member}, or an equal member that was already part of 911 * this group. 912 */ 913 public I decrementIndexOf(I member) 914 { 915 I movedMember = null; 916 917 int currentIndex = items.indexOf(member); 918 919 if (0 < currentIndex) 920 { 921 suppressNotification = true; 922 923 movedMember = remove(currentIndex); 924 items.add(currentIndex - 1, movedMember); 925 926 suppressNotification = false; 927 tellListenersAboutMovedMember(movedMember, currentIndex, currentIndex-1); 928 } 929 930 return movedMember; 931 } 932 933 /** 934 * Moves the member at {@code index} to an index of {@code index}-plus-one. 935 * No move will occur if {@code index} is out of bounds, or if it is already 936 * the highest index of this group. 937 * 938 * @param index the index of the member to be moved. 939 * 940 * @return the member that was moved, or <i>null</i> if no member was moved. 941 */ 942 public I incrementIndexOfMemberAt(int index) 943 { 944 I movedMember = null; 945 946 int highestIndex = items.size() - 1; 947 948 if ((0 <= index) && (index < highestIndex)) 949 { 950 suppressNotification = true; 951 952 movedMember = items.remove(index); 953 items.add(index + 1, movedMember); 954 955 suppressNotification = false; 956 tellListenersAboutMovedMember(movedMember, index, index+1); 957 } 958 959 return movedMember; 960 } 961 962 /** 963 * Moves the member at {@code index} to an index of {@code index}-minus-one. 964 * No move will occur if {@code index} is out of bounds, or if it is already 965 * the lowest index of this group. 966 * 967 * @param index the index of the member to be moved. 968 * 969 * @return the member that was moved, or <i>null</i> if no member was moved. 970 */ 971 public I decrementIndexOfMemberAt(int index) 972 { 973 I movedMember = null; 974 975 int highestIndex = items.size() - 1; 976 977 if ((0 < index) && (index <= highestIndex)) 978 { 979 suppressNotification = true; 980 981 movedMember = items.remove(index); 982 items.add(index - 1, movedMember); 983 984 suppressNotification = false; 985 tellListenersAboutMovedMember(movedMember, index, index+1); 986 } 987 988 return movedMember; 989 } 990 991 /** 992 * Swaps the members at the given positions. 993 * <p> 994 * If the indices are equal, no action is taken. 995 * If one or both of the indices are not in the range 996 * [0,size()), the underlying {@link List} will throw 997 * an exception.</p> 998 * 999 * @param index1 the position of the one of the members. 1000 * @param index2 the position of another of the members. 1001 * 1002 * @return a list with the member originally at the lower index followed by 1003 * the member originally at the higher index. If no swap was made 1004 * the returned list will be empty. 1005 */ 1006 public List<I> swap(int index1, int index2) 1007 { 1008 List<I> swappedMembers = new ArrayList<I>(); 1009 1010 if (index1 != index2) 1011 { 1012 int low = Math.min(index1, index2); 1013 int high = Math.max(index1, index2); 1014 1015 I lowMember = items.get(low); 1016 I highMember = items.get(high); 1017 1018 items.remove(high); 1019 items.add(high, lowMember); 1020 items.remove(low); 1021 items.add(low, highMember); 1022 1023 tellListenersAboutMovedMember(highMember, high, low); 1024 1025 //The indices are incremented here because listeners will receive this 1026 //message after the one above. So to them, it looks as if lowMember is 1027 //currently at position low+1. 1028 tellListenersAboutMovedMember(lowMember, low+1, high+1); 1029 1030 swappedMembers.add(lowMember); 1031 swappedMembers.add(highMember); 1032 } 1033 1034 return swappedMembers; 1035 } 1036 1037 /** 1038 * Moves a member {@code fromIndex} {@code toIndex}. 1039 * 1040 * @param fromIndex the current index of a member. 1041 * @param toIndex the new index of that same member. 1042 * @return the member that moved, even if it moved back to the same position. 1043 */ 1044 public I move(int fromIndex, int toIndex) 1045 { 1046 suppressNotification = true; 1047 1048 I movedMember = items.remove(fromIndex); 1049 1050 if (movedMember != null) 1051 { 1052 int indexMax = items.size(); 1053 if (toIndex > indexMax) 1054 toIndex = indexMax; 1055 1056 items.add(toIndex, movedMember); 1057 1058 suppressNotification = false; 1059 tellListenersAboutMovedMember(movedMember, fromIndex, toIndex); 1060 } 1061 else 1062 { 1063 suppressNotification = false; 1064 } 1065 1066 return movedMember; 1067 } 1068 1069 /** 1070 * Moves {@code member} {@code toIndex}. 1071 * 1072 * @param member the member to be moved. 1073 * @param toIndex the new position for {@code member}. 1074 * @return the member that moved, or <i>null</i> if no movement occurred. 1075 * Note that the member that moved might be {@code member} or, 1076 * alternatively, a member <em>equal to</em> {@code member}, 1077 * if this group has such a member. 1078 */ 1079 public I move(I member, int toIndex) 1080 { 1081 int fromIndex = indexOf(member); 1082 1083 return fromIndex < 0 ? null : move(fromIndex, toIndex); 1084 } 1085 1086 @SuppressWarnings("unchecked") 1087 protected void tellListenersAboutMovedMember(I member, 1088 int fromIndex, int toIndex) 1089 { 1090 if (!suppressNotification) 1091 for (CatalogItemGroupListener<I,G,C> listener : listeners) 1092 listener.memberMoved((G)this, member, fromIndex, toIndex); 1093 } 1094 1095 /** 1096 * Uses {@code comparator} to sort the members of this group. 1097 * @param comparator used to order this group's members 1098 */ 1099 @SuppressWarnings("unchecked") 1100 public void sort(Comparator<? super I> comparator) 1101 { 1102 Collections.sort(items, comparator); 1103 1104 if (!suppressNotification) 1105 for (CatalogItemGroupListener<I,G,C> listener : listeners) 1106 listener.membersSorted((G)this); 1107 } 1108 1109 //============================================================================ 1110 // FETCHING MEMBERS AND MEMBER INFORMATION 1111 //============================================================================ 1112 1113 /** 1114 * Returns the number of members in this group. 1115 * @return the number of members in this group. 1116 */ 1117 public int size() 1118 { 1119 return items.size(); 1120 } 1121 1122 /** 1123 * Returns the member at the given position. 1124 * 1125 * @param index the position of the member to return. 1126 * 1127 * @return the member at {@code index}. 1128 */ 1129 public I get(int index) 1130 { 1131 return items.get(index); 1132 } 1133 1134 /** 1135 * Returns a list of this group's members. 1136 * <p> 1137 * Note that the returned list is <i>not</i> held internally by this group. 1138 * This means that any changes made to the returned list will <i>not</i> 1139 * be reflected in this object. The elements of the list, though, are the 1140 * actual members of this group, so changes made to them will be reflected 1141 * in this group (and in all other groups of which those items are 1142 * members).</p> 1143 * 1144 * @return a list of this group's members. The returned value is guaranteed 1145 * to be non-null, but it may be an empty list. 1146 */ 1147 public List<I> getAll() 1148 { 1149 return new ArrayList<I>(items); 1150 } 1151 1152 /** Returns the internal list of members. */ 1153 protected List<I> getInternalMemberList() 1154 { 1155 return items; 1156 } 1157 1158 /** 1159 * Replaces the internal member list with {@code replacementList}. 1160 * This method is meant solely for use by frameworks (such 1161 * as JAXB or Hibernate) that rely on getX/setX pairs for 1162 * persisted properties. It is {@code protected}, as opposed to 1163 * {@code private}, so that subclasses can create and annotate 1164 * methods that call this one. 1165 */ 1166 protected void setInternalMemberList(List<I> replacementList) 1167 { 1168 items = (replacementList == null) ? new ArrayList<I>() 1169 : replacementList; 1170 } 1171 1172 /** 1173 * Returns <i>true</i> if this group has no members. 1174 * @return <i>true</i> if this group has no members. 1175 */ 1176 public boolean isEmpty() 1177 { 1178 return items.isEmpty(); 1179 } 1180 1181 /** 1182 * Returns <i>true</i> if {@code member} belongs to this group. 1183 * @param member a possible member of this group. 1184 * @return <i>true</i> if {@code member} belongs to this group. 1185 */ 1186 public boolean contains(I member) 1187 { 1188 return items.contains(member); 1189 } 1190 1191 /** 1192 * Returns the index in this group of the first occurrence of 1193 * {@code member}. Indexing is zero-based. 1194 * 1195 * @param member the member whose index is sought. 1196 * 1197 * @return the index of the first occurrence of {@code member}. 1198 * If {@code member} does not belong to this group, 1199 * the returned value will be less than zero. 1200 */ 1201 public int indexOf(I member) 1202 { 1203 return member == null ? -1 : items.indexOf(member); 1204 } 1205 1206 /** 1207 * Returns the index in this group of the last occurrence of 1208 * {@code member}. Indexing is zero-based. 1209 * 1210 * @param member the member whose index is sought. 1211 * 1212 * @return the index of the last occurrence of {@code member}. 1213 * If {@code member} does not belong to this group, 1214 * the returned value will be less than zero. 1215 */ 1216 public int lastIndexOf(I member) 1217 { 1218 return member == null ? -1 : items.lastIndexOf(member); 1219 } 1220 1221 //============================================================================ 1222 // LISTENERS 1223 //============================================================================ 1224 1225 /** 1226 * Adds {@code newListener} to this group's list. 1227 * <p> 1228 * The listener will be informed whenever: 1229 * <ul> 1230 * <li>A new member is added to this group</li> 1231 * <li>A current member is removed from this group</li> 1232 * <li>A current member is moved from one position 1233 * in this group to another</li> 1234 * </ul></p> 1235 * 1236 * @param newListener the listener to be added to this group's list. 1237 */ 1238 public void addListener(CatalogItemGroupListener<I,G,C> newListener) 1239 { 1240 if (newListener != null) 1241 listeners.add(newListener); 1242 } 1243 1244 /** 1245 * Removes {@code listener} from this group's list. 1246 * @param listener the listener to be removed from this group's list. 1247 */ 1248 public void removeListener(CatalogItemGroupListener<I,G,C> listener) 1249 { 1250 listeners.remove(listener); 1251 } 1252 1253 /** Removes all listeners from this group. */ 1254 public void removeAllListeners() 1255 { 1256 listeners.clear(); 1257 } 1258 1259 //============================================================================ 1260 // TEXT 1261 //============================================================================ 1262 1263 /** 1264 * Returns a text representation of this group. 1265 * @return a text representation of this group. 1266 */ 1267 public String toString() 1268 { 1269 StringBuilder buff = new StringBuilder(); 1270 1271 buff.append("name=").append(name); 1272 buff.append(", id=").append(id); 1273 buff.append(", size=").append(size()); 1274 1275 return buff.toString(); 1276 } 1277 1278 /** 1279 * Returns an XML representation of this group. 1280 * @return an XML representation of this group. 1281 * @throws JAXBException if anything goes wrong during the conversion to XML. 1282 */ 1283 public String toXml() throws JAXBException 1284 { 1285 return JaxbUtility.getSharedInstance().objectToXmlString(this); 1286 } 1287 1288 /** 1289 * Writes an XML representation of this group to {@code writer}. 1290 * @param writer the device to which XML is written. 1291 * @throws JAXBException if anything goes wrong during the conversion to XML. 1292 */ 1293 public void writeAsXmlTo(Writer writer) throws JAXBException 1294 { 1295 JaxbUtility.getSharedInstance().writeObjectAsXmlTo(writer, this, null); 1296 } 1297 1298 //============================================================================ 1299 // 1300 //============================================================================ 1301 1302 /** 1303 * Clones this group and adds it to the same catalog to which it belongs. 1304 * If this group belongs to no catalog, this method behaves the same as 1305 * {@link #clone()}. 1306 * <p> 1307 * <b><u>Special Notes on Cloning Methodology</u></b> 1308 * <ol> 1309 * <li>The clone's ID property will be set to a 1310 * {@link #getIdOfUnidentified() value} that indicates 1311 * that this group is currently unidentified.</li> 1312 * <li>The clone will have no listeners.</li> 1313 * </ol></p> 1314 * <p> 1315 * If anything goes wrong during the cloning procedure, 1316 * a {@code RuntimeException} will be thrown.</p> 1317 * 1318 * @return a clone of this group that belongs to the same catalog. 1319 * 1320 * @see #clone() 1321 */ 1322 @SuppressWarnings("unchecked") 1323 public G cloneIntoSameCatalog() 1324 { 1325 //Quick exit for null catalog 1326 if (this.catalog == null) 1327 return clone(); 1328 1329 //The code from here down knows that the clone's catalog is NOT NULL. 1330 G clonedGroup = null; 1331 1332 try 1333 { 1334 //This line takes care of the primitive fields properly 1335 //(This is the line that requires suppression of unchecked conversions.) 1336 clonedGroup = (G)super.clone(); 1337 1338 //We do NOT want the clone to have the same ID as the original. 1339 //The ID is here for the persistence layer; it is in charge of 1340 //setting IDs. To help it, we put the clone's ID in the uninitialized 1341 //state. 1342 clonedGroup.id = getIdOfUnidentified(); 1343 1344 adjustNameForCatalog(this.catalog); 1345 1346 //Clear the listeners. 1347 clonedGroup.listeners = 1348 new CopyOnWriteArraySet<CatalogItemGroupListener<I,G,C>>(); 1349 1350 //Set up the catalog, which we know is NOT null. 1351 clonedGroup.simplySetCatalog(this.catalog); 1352 this.catalog.simplyAddGroup(clonedGroup); 1353 1354 //Create new member list & populate w/ references, not clones. 1355 //Remember: the catalog is the main holder of the items; the groups' 1356 //items are merely references to those in the catalog. We can do 1357 //this ONLY because we know we're cloning this group into the same 1358 //non-null catalog. 1359 clonedGroup.items = new ArrayList<I>(); 1360 for (I originalMember : this.items) 1361 { 1362 clonedGroup.add(originalMember); 1363 } 1364 } 1365 catch (Exception ex) 1366 { 1367 throw new RuntimeException(ex); 1368 } 1369 1370 return clonedGroup; 1371 } 1372 1373 /** 1374 * Returns a group that is a copy of this one. 1375 * The clone is a deep clone; all the members in the returned group are 1376 * copies of those in this group. 1377 * <p> 1378 * <b><u>Special Notes on Cloning Methodology</u></b> 1379 * <ol> 1380 * <li>The clone's ID property will be set to a 1381 * {@link #getIdOfUnidentified() value} that indicates 1382 * that this group is currently unidentified.</li> 1383 * <li>The clone will belong to no catalog. That is, its catalog 1384 * property will be <i>null</i>.</li> 1385 * <li>The clone will have no listeners.</li> 1386 * </ol></p> 1387 * <p> 1388 * If anything goes wrong during the cloning procedure, 1389 * a {@code RuntimeException} will be thrown.</p> 1390 * 1391 * @see #cloneIntoSameCatalog() 1392 */ 1393 @Override 1394 public G clone() 1395 { 1396 G clonedGroup = cloneAllButMembers(); 1397 1398 for (I originalMember : this.items) 1399 clonedGroup.add(originalMember.clone()); 1400 1401 return clonedGroup; 1402 } 1403 1404 /** 1405 * Clones this group but does not add any members to the new group. 1406 * @return a partial clone of this group. The returned clone has 1407 * no members. 1408 */ 1409 protected G cloneAllExceptMembers() 1410 { 1411 return cloneAllButMembers(); 1412 } 1413 1414 //Clones this group except for 1415 @SuppressWarnings("unchecked") 1416 private G cloneAllButMembers() 1417 { 1418 G clonedGroup = null; 1419 1420 try 1421 { 1422 //This line takes care of the primitive fields properly. 1423 //(This is the line that requires suppression of unchecked conversions.) 1424 clonedGroup = (G)super.clone(); 1425 1426 //We do NOT want the clone to have the same ID as the original. 1427 //The ID is here for the persistence layer; it is in charge of 1428 //setting IDs. To help it, we put the clone's ID in the uninitialized 1429 //state. 1430 clonedGroup.id = getIdOfUnidentified(); 1431 1432 //Clone list, but it's OK to share refs to the same immutable strings. 1433 clonedGroup.notes = new ArrayList<String>(this.notes); 1434 1435 //Set the catalog to null. 1436 clonedGroup.simplySetCatalog(null); 1437 1438 //Clear the listeners. 1439 clonedGroup.listeners = 1440 new CopyOnWriteArraySet<CatalogItemGroupListener<I,G,C>>(); 1441 1442 //Make new member list, but leave list empty. 1443 clonedGroup.items = new ArrayList<I>(); 1444 } 1445 catch (Exception ex) 1446 { 1447 throw new RuntimeException(ex); 1448 } 1449 1450 return clonedGroup; 1451 } 1452 1453 /** 1454 * Copies <i>some</i> properties of {@code otherGroup} into this one. 1455 * <p> 1456 * This method is used by the catalog's clone method to help copy 1457 * its main group, which is <i>not</i> created via the normal cloning 1458 * process. Subclasses should override this method and call 1459 * <tt>super.copyPropertiesFrom(otherGroup)</tt>. Failure to do so 1460 * will result in some properties of a cloned catalog from matching 1461 * that of the original.</p> 1462 * <p> 1463 * What is <i>not</i> copied:</p> 1464 * <ul> 1465 * <li>id</li> 1466 * <li>catalog reference</li> 1467 * <li>listeners</li> 1468 * <li>items</li> 1469 * </ul> 1470 * <p> 1471 * Subclasses should copy only those new properties that it 1472 * creates. This list will be similar, if not identical, to 1473 * the properties copied in the subclass's clone method.</p> 1474 * <p> 1475 * The fact that this method has become necessary probably means 1476 * that the way we're handling setup of a catalog's main group 1477 * could probably be done better. One reason we don't just 1478 * clone the main group is the main group uses a special 1479 * non-public constructor. A reworking of the main group creation 1480 * and cloning code should be done later on.</p> 1481 * 1482 * @param otherGroup a source of property values. 1483 */ 1484 protected void copyPropertiesFrom(G otherGroup) 1485 { 1486 //Clone list, but it's OK to share refs to the same immutable strings. 1487 this.notes = new ArrayList<String>(otherGroup.notes); 1488 } 1489 1490 /** 1491 * Returns <i>true</i> if {@code o} is equal to this group in all respects 1492 * with the possible exception of the ordering of the members. 1493 */ 1494 @SuppressWarnings("unchecked") 1495 public boolean equalsWithoutRespectToOrder(Object o) 1496 { 1497 //Quick exit if o is this 1498 if (o == this) 1499 return true; 1500 1501 //Quick exit if clearly unequal 1502 if (clearlyNotEqualTo(o)) 1503 return false; 1504 1505 CatalogItemGroup other = (CatalogItemGroup)o; 1506 1507 //Return true if members are same, even if they're in diff orders 1508 int count = this.size(); 1509 for (int m=0; m < count; m++) 1510 { 1511 if (!this.items.contains(other.items.get(m))) 1512 return false; 1513 1514 if (!other.items.contains(this.items.get(m))) 1515 return false; 1516 } 1517 1518 return true; 1519 } 1520 1521 /** Returns <i>true</i> if {@code o} is equal to this group. */ 1522 @SuppressWarnings("unchecked") 1523 @Override 1524 public boolean equals(Object o) 1525 { 1526 //Quick exit if o is this 1527 if (o == this) 1528 return true; 1529 1530 //Quick exit if clearly unequal 1531 if (clearlyNotEqualTo(o)) 1532 return false; 1533 1534 CatalogItemGroup other = (CatalogItemGroup)o; 1535 1536 //Members must be equal and in same order 1537 return this.items.equals(other.items); 1538 } 1539 1540 @SuppressWarnings("unchecked") 1541 protected boolean clearlyNotEqualTo(Object o) 1542 { 1543 //Quick exit if o is null 1544 if (o == null) 1545 return true; 1546 1547 //Quick exit if classes are different 1548 if (!o.getClass().equals(this.getClass())) 1549 return true; 1550 1551 CatalogItemGroup other = (CatalogItemGroup)o; 1552 1553 //NOTE: Absence of ID and catalog is intentional 1554 1555 //Compare the names and size 1556 if (!this.name.equals(other.name) || 1557 (this.nameIsLocked != other.nameIsLocked) || 1558 (this.size() != other.size())) 1559 return true; 1560 1561 return false; 1562 } 1563 1564 /** Returns a hash code value for this group. */ 1565 public int hashCode() 1566 { 1567 //Taken from the Effective Java book by Joshua Bloch. 1568 //The constants 17 & 37 are arbitrary & carry no meaning. 1569 int result = 17; 1570 1571 //NOTE: Keep this method in synch w/ equals 1572 1573 result = 37 * result + name.hashCode(); 1574 result = 37 * result + new Boolean(nameIsLocked).hashCode(); 1575 result = 37 * result + items.hashCode(); 1576 1577 return result; 1578 } 1579 }