001package jmri.jmrit.roster; 002 003import java.awt.HeadlessException; 004import java.beans.PropertyChangeEvent; 005import java.beans.PropertyChangeListener; 006import java.beans.PropertyChangeSupport; 007import java.io.File; 008import java.io.IOException; 009import java.util.ArrayList; 010import java.util.Collections; 011import java.util.HashMap; 012import java.util.List; 013import java.util.Locale; 014import java.util.Set; 015import java.util.TreeSet; 016 017import javax.annotation.CheckForNull; 018import javax.annotation.Nonnull; 019import javax.swing.JOptionPane; 020import jmri.InstanceManager; 021import jmri.UserPreferencesManager; 022import jmri.beans.PropertyChangeProvider; 023import jmri.jmrit.XmlFile; 024import jmri.jmrit.roster.rostergroup.RosterGroup; 025import jmri.jmrit.roster.rostergroup.RosterGroupSelector; 026import jmri.jmrit.symbolicprog.SymbolicProgBundle; 027import jmri.profile.Profile; 028import jmri.profile.ProfileManager; 029import jmri.util.FileUtil; 030import org.jdom2.Document; 031import org.jdom2.Element; 032import org.jdom2.JDOMException; 033import org.jdom2.ProcessingInstruction; 034 035/** 036 * Roster manages and manipulates a roster of locomotives. 037 * <p> 038 * It works with the "roster-config" XML schema to load and store its 039 * information. 040 * <p> 041 * This is an in-memory representation of the roster xml file (see below for 042 * constants defining name and location). As such, this class is also 043 * responsible for the "dirty bit" handling to ensure it gets written. As a 044 * temporary reliability enhancement, all changes to this structure are now 045 * being written to a backup file, and a copy is made when the file is opened. 046 * <p> 047 * Multiple Roster objects don't make sense, so we use an "instance" member to 048 * navigate to a single one. 049 * <p> 050 * The only bound property is the list of RosterEntrys; a PropertyChangedEvent 051 * is fired every time that changes. 052 * <p> 053 * The entries are stored in an ArrayList, sorted alphabetically. That sort is 054 * done manually each time an entry is added. 055 * <p> 056 * The roster is stored in a "Roster Index", which can be read or written. Each 057 * individual entry (once stored) contains a filename which can be used to 058 * retrieve the locomotive information for that roster entry. Note that the 059 * RosterEntry information is duplicated in both the Roster (stored in the 060 * roster.xml file) and in the specific file for the entry. 061 * <p> 062 * Originally, JMRI managed just one global roster, held in a global Roster 063 * object. With the rise of more complicated layouts, code has been added to 064 * address multiple rosters, with the primary one now held in Roster.default(). 065 * We're moving references to Roster.default() out to the using code, so that 066 * eventually we can make those explicit references to other Roster objects 067 * as/when needed. 068 * 069 * @author Bob Jacobsen Copyright (C) 2001, 2008, 2010 070 * @author Dennis Miller Copyright 2004 071 * @see jmri.jmrit.roster.RosterEntry 072 */ 073public class Roster extends XmlFile implements RosterGroupSelector, PropertyChangeProvider, PropertyChangeListener { 074 075 /** 076 * List of contained {@link RosterEntry} elements. 077 */ 078 private final List<RosterEntry> _list = new ArrayList<>(); 079 private boolean dirty = false; 080 /* 081 * This should always be a real path, changes in the UserFiles location are 082 * tracked by listening to FileUtilSupport for those changes and updating 083 * this path as needed. 084 */ 085 private String rosterLocation = FileUtil.getUserFilesPath(); 086 private String rosterIndexFileName = Roster.DEFAULT_ROSTER_INDEX; 087 // since we can't do a "super(this)" in the ctor to inherit from PropertyChangeSupport, we'll 088 // reflect to it. 089 // Note that dispose() doesn't act on these. Its not clear whether it should... 090 private final PropertyChangeSupport pcs = new PropertyChangeSupport(this); 091 static final public String schemaVersion = ""; // NOI18N 092 private String defaultRosterGroup = null; 093 private final HashMap<String, RosterGroup> rosterGroups = new HashMap<>(); 094 095 /** 096 * Name of the default roster index file. {@value #DEFAULT_ROSTER_INDEX} 097 */ 098 public static final String DEFAULT_ROSTER_INDEX = "roster.xml"; // NOI18N 099 /** 100 * Name for the property change fired when adding a roster entry. 101 * {@value #ADD} 102 */ 103 public static final String ADD = "add"; // NOI18N 104 /** 105 * Name for the property change fired when removing a roster entry. 106 * {@value #REMOVE} 107 */ 108 public static final String REMOVE = "remove"; // NOI18N 109 /** 110 * Name for the property change fired when changing the ID of a roster 111 * entry. {@value #CHANGE} 112 */ 113 public static final String CHANGE = "change"; // NOI18N 114 /** 115 * Property change event fired when saving the roster. {@value #SAVED} 116 */ 117 public static final String SAVED = "saved"; // NOI18N 118 /** 119 * Property change fired when adding a roster group. 120 * {@value #ROSTER_GROUP_ADDED} 121 */ 122 public static final String ROSTER_GROUP_ADDED = "RosterGroupAdded"; // NOI18N 123 /** 124 * Property change fired when removing a roster group. 125 * {@value #ROSTER_GROUP_REMOVED} 126 */ 127 public static final String ROSTER_GROUP_REMOVED = "RosterGroupRemoved"; // NOI18N 128 /** 129 * Property change fired when renaming a roster group. 130 * {@value #ROSTER_GROUP_RENAMED} 131 */ 132 public static final String ROSTER_GROUP_RENAMED = "RosterGroupRenamed"; // NOI18N 133 /** 134 * String prefixed to roster group names in the roster entry XML. 135 * {@value #ROSTER_GROUP_PREFIX} 136 */ 137 public static final String ROSTER_GROUP_PREFIX = "RosterGroup:"; // NOI18N 138 /** 139 * Title of the "All Entries" roster group. As this varies by locale, do not 140 * rely on being able to store this value. 141 */ 142 public static final String ALLENTRIES = Bundle.getMessage("ALLENTRIES"); // NOI18N 143 144 /** 145 * Create a roster with default contents. 146 */ 147 public Roster() { 148 super(); 149 FileUtil.getDefault().addPropertyChangeListener(FileUtil.PREFERENCES, (PropertyChangeEvent evt) -> { 150 FileUtil.Property oldValue = (FileUtil.Property) evt.getOldValue(); 151 FileUtil.Property newValue = (FileUtil.Property) evt.getNewValue(); 152 Profile project = oldValue.getKey(); 153 if (this.equals(getRoster(project)) && getRosterLocation().equals(oldValue.getValue())) { 154 setRosterLocation(newValue.getValue()); 155 reloadRosterFile(); 156 } 157 }); 158 InstanceManager.getOptionalDefault(UserPreferencesManager.class).ifPresent((upm) -> { 159 // During JUnit testing, preferences is often null 160 this.setDefaultRosterGroup((String) upm.getProperty(Roster.class.getCanonicalName(), "defaultRosterGroup")); // NOI18N 161 }); 162 } 163 164 // should be private except that JUnit testing creates multiple Roster objects 165 public Roster(String rosterFilename) { 166 this(); 167 try { 168 // if the rosterFilename passed in is null, create a complete path 169 // to the default roster index before attempting to read 170 if (rosterFilename == null) { 171 rosterFilename = Roster.this.getRosterIndexPath(); 172 } 173 Roster.this.readFile(rosterFilename); 174 } catch (IOException | JDOMException e) { 175 log.error("Exception during reading while constructing roster", e); 176 try { 177 JOptionPane.showMessageDialog(null, 178 Bundle.getMessage("ErrorReadingText") + "\n" + e.getMessage(), 179 Bundle.getMessage("ErrorReadingTitle"), 180 JOptionPane.ERROR_MESSAGE); 181 } catch (HeadlessException he) { 182 // ignore inability to display dialog 183 } 184 } 185 } 186 187 /** 188 * Get the roster for the profile returned by 189 * {@link ProfileManager#getActiveProfile()}. 190 * 191 * @return the roster for the active profile 192 */ 193 public static synchronized Roster getDefault() { 194 return getRoster(ProfileManager.getDefault().getActiveProfile()); 195 } 196 197 /** 198 * Get the roster for the specified profile. 199 * 200 * @param profile the Profile to get the roster for 201 * @return the roster for the profile 202 */ 203 public static synchronized @Nonnull 204 Roster getRoster(@CheckForNull Profile profile) { 205 return InstanceManager.getDefault(RosterConfigManager.class).getRoster(profile); 206 } 207 208 /** 209 * Add a RosterEntry object to the in-memory Roster. 210 * 211 * @param e Entry to add 212 */ 213 public void addEntry(RosterEntry e) { 214 log.debug("Add entry {}", e); 215 synchronized (_list) { 216 int i = _list.size() - 1; // Last valid index 217 while (i >= 0) { 218 if (e.getId().compareToIgnoreCase(_list.get(i).getId()) > 0) { 219 break; // get out of the loop since the entry at I sorts 220 // before the new entry 221 } 222 i--; 223 } 224 _list.add(i + 1, e); 225 } 226 e.addPropertyChangeListener(this); 227 this.addRosterGroups(e.getGroups(this)); 228 setDirty(true); 229 firePropertyChange(ADD, null, e); 230 } 231 232 /** 233 * Remove a RosterEntry object from the in-memory Roster. This does not 234 * delete the file for the RosterEntry! 235 * 236 * @param e Entry to remove 237 */ 238 public void removeEntry(RosterEntry e) { 239 log.debug("Remove entry {}", e); 240 synchronized (_list) { 241 _list.remove(e); 242 } 243 e.removePropertyChangeListener(this); 244 setDirty(true); 245 firePropertyChange(REMOVE, e, null); 246 } 247 248 /** 249 * @return number of entries in the roster 250 */ 251 public int numEntries() { 252 synchronized (_list) { 253 return _list.size(); 254 } 255 } 256 257 /** 258 * @param group The group being queried or null for all entries in the 259 * roster. 260 * @return The Number of roster entries in the specified group or 0 if the 261 * group does not exist. 262 */ 263 public int numGroupEntries(String group) { 264 if (group != null 265 && !group.equals(Roster.ALLENTRIES) 266 && !group.equals(Roster.allEntries(Locale.getDefault()))) { 267 return (this.rosterGroups.get(group) != null) ? this.rosterGroups.get(group).getEntries().size() : 0; 268 } else { 269 return this.numEntries(); 270 } 271 } 272 273 /** 274 * Return RosterEntry from a "title" string, ala selection in 275 * matchingComboBox. 276 * 277 * @param title The title for the RosterEntry. 278 * @return The matching RosterEntry or null 279 */ 280 public RosterEntry entryFromTitle(String title) { 281 synchronized (_list) { 282 for (RosterEntry re : _list) { 283 if (re.titleString().equals(title)) { 284 return re; 285 } 286 } 287 } 288 return null; 289 } 290 291 /** 292 * Return RosterEntry from a "id" string. 293 * 294 * @param id The id for the RosterEntry. 295 * @return The matching RosterEntry or null 296 */ 297 public RosterEntry getEntryForId(String id) { 298 synchronized (_list) { 299 for (RosterEntry re : _list) { 300 if (re.getId().equals(id)) { 301 return re; 302 } 303 } 304 } 305 return null; 306 } 307 308 /** 309 * Return a list of RosterEntry which have a particular DCC address. 310 * 311 * @param a The address. 312 * @return a List of matching entries, empty if there are not matches. 313 */ 314 @Nonnull 315 public List<RosterEntry> getEntriesByDccAddress(String a) { 316 return findMatchingEntries( 317 (RosterEntry r) -> { 318 return r.getDccAddress().equals(a); 319 } 320 ); 321 } 322 323 /** 324 * Return a specific entry by index 325 * 326 * @param i The RosterEntry at position i in the roster. 327 * @return The matching RosterEntry 328 */ 329 @Nonnull 330 public RosterEntry getEntry(int i) { 331 synchronized (_list) { 332 return _list.get(i); 333 } 334 } 335 336 /** 337 * Get all roster entries. 338 * 339 * @return a list of roster entries; the list is empty if the roster is 340 * empty 341 */ 342 @Nonnull 343 public List<RosterEntry> getAllEntries() { 344 return this.getEntriesInGroup(null); 345 } 346 347 /** 348 * Get the Nth RosterEntry in the group 349 * 350 * @param group The group being queried. 351 * @param i The index within the group of the requested entry. 352 * @return The specified entry in the group or null if i is larger than the 353 * group, or the group does not exist. 354 */ 355 public RosterEntry getGroupEntry(String group, int i) { 356 List<RosterEntry> l = matchingList(null, null, null, null, null, null, null); 357 int num = 0; 358 for (RosterEntry r : l) { 359 if (group != null) { 360 if ((r.getAttribute(getRosterGroupProperty(group)) != null) 361 && r.getAttribute(getRosterGroupProperty(group)).equals("yes")) { // NOI18N 362 if (num == i) { 363 return r; 364 } 365 num++; 366 } 367 } else { 368 if (num == i) { 369 return r; 370 } 371 num++; 372 } 373 } 374 return null; 375 } 376 377 public int getGroupIndex(String group, RosterEntry re) { 378 List<RosterEntry> l = matchingList(null, null, null, null, null, null, null); 379 int num = 0; 380 for (RosterEntry r : l) { 381 if (group != null) { 382 if ((r.getAttribute(getRosterGroupProperty(group)) != null) 383 && r.getAttribute(getRosterGroupProperty(group)).equals("yes")) { // NOI18N 384 if (r == re) { 385 return num; 386 } 387 num++; 388 } 389 } else { 390 if (re == r) { 391 return num; 392 } 393 num++; 394 } 395 } 396 return -1; 397 } 398 399 /** 400 * Return filename from a "title" string, ala selection in matchingComboBox. 401 * 402 * @param title The title for the entry. 403 * @return The filename for the RosterEntry matching title, or null if no 404 * such RosterEntry exists. 405 */ 406 public String fileFromTitle(String title) { 407 RosterEntry r = entryFromTitle(title); 408 if (r != null) { 409 return r.getFileName(); 410 } 411 return null; 412 } 413 414 public List<RosterEntry> getEntriesWithAttributeKey(String key) { 415 ArrayList<RosterEntry> result = new ArrayList<>(); 416 synchronized (_list) { 417 _list.stream().filter((r) -> (r.getAttribute(key) != null)).forEachOrdered((r) -> { 418 result.add(r); 419 }); 420 } 421 return result; 422 } 423 424 public List<RosterEntry> getEntriesWithAttributeKeyValue(String key, String value) { 425 ArrayList<RosterEntry> result = new ArrayList<>(); 426 synchronized (_list) { 427 _list.stream().forEach((r) -> { 428 String v = r.getAttribute(key); 429 if (v != null && v.equals(value)) { 430 result.add(r); 431 } 432 }); 433 } 434 return result; 435 } 436 437 public Set<String> getAllAttributeKeys() { 438 Set<String> result = new TreeSet<>(); 439 synchronized (_list) { 440 _list.stream().forEach((r) -> { 441 result.addAll(r.getAttributes()); 442 }); 443 } 444 return result; 445 } 446 447 public List<RosterEntry> getEntriesInGroup(String group) { 448 if (group == null || group.equals(Roster.ALLENTRIES) || group.isEmpty()) { 449 return this.matchingList(null, null, null, null, null, null, null); 450 } else { 451 return this.getEntriesWithAttributeKeyValue(Roster.getRosterGroupProperty(group), "yes"); // NOI18N 452 } 453 } 454 455 /** 456 * Internal interface works with #findMatchingEntries to provide a common 457 * search-match-return capability. 458 */ 459 private interface RosterComparator { 460 461 public boolean check(RosterEntry r); 462 } 463 464 /** 465 * Internal method works with #RosterComparator to provide a common 466 * search-match-return capability. 467 */ 468 private List<RosterEntry> findMatchingEntries(RosterComparator c) { 469 List<RosterEntry> l = new ArrayList<>(); 470 synchronized (_list) { 471 _list.stream().filter((r) -> (c.check(r))).forEachOrdered((r) -> { 472 l.add(r); 473 }); 474 } 475 return l; 476 } 477 478 /** 479 * Get a List of {@link RosterEntry} objects in Roster matching some 480 * information. The list will be empty if there are no matches. 481 * 482 * @param roadName road name of entry or null for any road name 483 * @param roadNumber road number of entry of null for any number 484 * @param dccAddress address of entry or null for any address 485 * @param mfg manufacturer of entry or null for any manufacturer 486 * @param decoderModel decoder model of entry or null for any model 487 * @param decoderFamily decoder family of entry or null for any family 488 * @param id id of entry or null for any id 489 * @param group group entry is member of or null for any group 490 * @param developerID developerID of entry, or null for any developerID 491 * @param manufacturerID manufacturerID of entry, or null for any manufacturerID 492 * @param productID productID of entry, or null for any productID 493 * @return List of matching RosterEntries or an empty List 494 */ 495 @Nonnull 496 public List<RosterEntry> getEntriesMatchingCriteria(String roadName, String roadNumber, String dccAddress, 497 String mfg, String decoderModel, String decoderFamily, String id, String group, 498 String developerID, String manufacturerID, String productID) { 499 // specifically updated for SV2 500 return findMatchingEntries( 501 (RosterEntry r) -> { 502 return checkEntry(r, roadName, roadNumber, dccAddress, 503 mfg, decoderModel, decoderFamily, 504 id, group, developerID, manufacturerID, productID); 505 } 506 ); 507 } 508 509 /** 510 * Get a List of {@link RosterEntry} objects in Roster matching some 511 * information. The list will be empty if there are no matches. 512 * 513 * @param roadName road name of entry or null for any road name 514 * @param roadNumber road number of entry of null for any number 515 * @param dccAddress address of entry or null for any address 516 * @param mfg manufacturer of entry or null for any manufacturer 517 * @param decoderModel decoder model of entry or null for any model 518 * @param decoderFamily decoder family of entry or null for any family 519 * @param id id of entry or null for any id 520 * @param group group entry is member of or null for any group 521 * @return List of matching RosterEntries or an empty List 522 */ 523 @Nonnull 524 public List<RosterEntry> getEntriesMatchingCriteria(String roadName, String roadNumber, String dccAddress, 525 String mfg, String decoderModel, String decoderFamily, String id, String group) { 526 return findMatchingEntries( 527 (RosterEntry r) -> { 528 return checkEntry(r, roadName, roadNumber, dccAddress, 529 mfg, decoderModel, decoderFamily, 530 id, group, null, null, null); 531 } 532 ); 533 } 534 535 /** 536 * Get a List of {@link RosterEntry} objects in Roster matching some 537 * information. The list will be empty if there are no matches. 538 * <p> 539 * This method calls {@link #getEntriesMatchingCriteria(java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String) 540 * } 541 * with a null group. 542 * 543 * @param roadName road name of entry or null for any road name 544 * @param roadNumber road number of entry of null for any number 545 * @param dccAddress address of entry or null for any address 546 * @param mfg manufacturer of entry or null for any manufacturer 547 * @param decoderModel decoder model of entry or null for any model 548 * @param decoderFamily decoder family of entry or null for any family 549 * @param id id (unique name) of entry or null for any id 550 * @return List of matching RosterEntries or an empty List 551 * @see #getEntriesMatchingCriteria(java.lang.String, java.lang.String, 552 * java.lang.String, java.lang.String, java.lang.String, java.lang.String, 553 * java.lang.String, java.lang.String) 554 */ 555 @Nonnull 556 public List<RosterEntry> matchingList(String roadName, String roadNumber, String dccAddress, 557 String mfg, String decoderModel, String decoderFamily, String id) { 558 // specifically updated for SV2! 559 return this.getEntriesMatchingCriteria(roadName, roadNumber, dccAddress, 560 mfg, decoderModel, decoderFamily, id, null, null, null, null); 561 } 562 563 /** 564 * Get a List of {@link RosterEntry} objects in Roster matching some 565 * information. The list will be empty if there are no matches. 566 * <p> 567 * This method calls {@link #getEntriesMatchingCriteria(java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String) 568 * } 569 * with a null group. 570 * 571 * @param roadName road name of entry or null for any road name 572 * @param roadNumber road number of entry of null for any number 573 * @param dccAddress address of entry or null for any address 574 * @param mfg manufacturer of entry or null for any manufacturer 575 * @param decoderModel decoder model of entry or null for any model 576 * @param decoderFamily decoder family of entry or null for any family 577 * @param id id of entry or null for any id 578 * @param developerID developerID number 579 * @param manufacturerID manufacturerID number 580 * @param productID productID number 581 * @return List of matching RosterEntries or an empty List 582 * @see #getEntriesMatchingCriteria(java.lang.String, java.lang.String, 583 * java.lang.String, java.lang.String, java.lang.String, java.lang.String, 584 * java.lang.String, java.lang.String) 585 */ 586 @Nonnull 587 public List<RosterEntry> matchingList(String roadName, String roadNumber, String dccAddress, 588 String mfg, String decoderModel, String decoderFamily, String id, 589 String developerID, String manufacturerID, String productID) { 590 // specifically updated for SV2! 591 return this.getEntriesMatchingCriteria(roadName, roadNumber, dccAddress, 592 mfg, decoderModel, decoderFamily, id, null, developerID, 593 manufacturerID, productID); 594 } 595 596 /** 597 * Get a List of {@link RosterEntry} objects in Roster matching some 598 * information. The list will be empty if there are no matches. 599 * <p> 600 * This method calls {@link #getEntriesMatchingCriteria(java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String) 601 * } 602 * with a null group. 603 * This pattern is specifically for LNCV (since 4.22). 604 * 605 * @param dccAddress address of entry or null for any address 606 * @param productID productID number 607 * @return List of matching RosterEntries or an empty List 608 * @see #getEntriesMatchingCriteria(java.lang.String, java.lang.String, 609 * java.lang.String, java.lang.String, java.lang.String, java.lang.String, 610 * java.lang.String, java.lang.String) 611 */ 612 @Nonnull 613 public List<RosterEntry> matchingList(String dccAddress, String productID) { 614 return this.getEntriesMatchingCriteria(null, null, dccAddress, 615 null, null, null, null, null, null, 616 null, productID); 617 } 618 619 /** 620 * Check if an entry is consistent with specific properties. 621 * <p> 622 * A null String argument always matches. Strings are used for convenience 623 * in GUI building. 624 * 625 * @param i index in the roster for the RosterEntry 626 * @param roadName road name of entry or null for any road name 627 * @param roadNumber road number of entry of null for any number 628 * @param dccAddress address of entry or null for any address 629 * @param mfg manufacturer of entry or null for any manufacturer 630 * @param decoderModel decoder model of entry or null for any model 631 * @param decoderFamily decoder family of entry or null for any family 632 * @param id id of entry or null for any id 633 * @param group group entry is member of or null for any group 634 * @return true if the entry matches 635 */ 636 public boolean checkEntry(int i, String roadName, String roadNumber, String dccAddress, 637 String mfg, String decoderModel, String decoderFamily, 638 String id, String group) { 639 return this.checkEntry(_list, i, roadName, roadNumber, dccAddress, mfg, 640 decoderModel, decoderFamily, id, group); 641 } 642 643 /** 644 * Check if an entry is consistent with specific properties. 645 * <p> 646 * A null String argument always matches. Strings are used for convenience 647 * in GUI building. 648 * 649 * @param list the list of RosterEntrys being searched 650 * @param i the index of the roster entry in the list 651 * @param roadName road name of entry or null for any road name 652 * @param roadNumber road number of entry of null for any number 653 * @param dccAddress address of entry or null for any address 654 * @param mfg manufacturer of entry or null for any manufacturer 655 * @param decoderModel decoder model of entry or null for any model 656 * @param decoderFamily decoder family of entry or null for any family 657 * @param id id of entry or null for any id 658 * @param group group entry is member of or null for any group 659 * @return True if the entry matches 660 */ 661 public boolean checkEntry(List<RosterEntry> list, int i, String roadName, String roadNumber, String dccAddress, 662 String mfg, String decoderModel, String decoderFamily, 663 String id, String group) { 664 RosterEntry r = list.get(i); 665 return checkEntry(r, roadName, roadNumber, dccAddress, 666 mfg, decoderModel, decoderFamily, 667 id, group, null, null, null); 668 } 669 670 /** 671 * Check if an entry is consistent with specific properties. 672 * <p> 673 * A null String argument always matches. Strings are used for convenience 674 * in GUI building. 675 * 676 * @param r the roster entry being checked 677 * @param roadName road name of entry or null for any road name 678 * @param roadNumber road number of entry of null for any number 679 * @param dccAddress address of entry or null for any address 680 * @param mfg manufacturer of entry or null for any manufacturer 681 * @param decoderModel decoder model of entry or null for any model 682 * @param decoderFamily decoder family of entry or null for any family 683 * @param id id of entry or null for any id 684 * @param group group entry is member of or null for any group 685 * @param developerID developerID of entry, or null for any developerID 686 * @param manufacturerID manufacturerID of entry, or null for any manufacturerID 687 * @param productID productID of entry, or null for any productID 688 * @return True if the entry matches 689 */ 690 public boolean checkEntry(RosterEntry r, String roadName, String roadNumber, String dccAddress, 691 String mfg, String decoderModel, String decoderFamily, 692 String id, String group, String developerID, 693 String manufacturerID, String productID) { 694 // specifically updated for SV2! 695 696 if (id != null && !id.equals(r.getId())) { 697 return false; 698 } 699 if (roadName != null && !roadName.equals(r.getRoadName())) { 700 return false; 701 } 702 if (roadNumber != null && !roadNumber.equals(r.getRoadNumber())) { 703 return false; 704 } 705 if (dccAddress != null && !dccAddress.equals(r.getDccAddress())) { 706 return false; 707 } 708 if (mfg != null && !mfg.equals(r.getMfg())) { 709 return false; 710 } 711 if (decoderModel != null && !decoderModel.equals(r.getDecoderModel())) { 712 return false; 713 } 714 if (decoderFamily != null && !decoderFamily.equals(r.getDecoderFamily())) { 715 return false; 716 } 717 if (developerID != null && !developerID.equals(r.getDeveloperID())) { 718 return false; 719 } 720 if (manufacturerID != null && !manufacturerID.equals(r.getManufacturerID())) { 721 return false; 722 } 723 if (productID != null && !productID.equals(r.getProductID())) { 724 return false; 725 } 726 return (group == null 727 || Roster.ALLENTRIES.equals(group) 728 || (r.getAttribute(Roster.getRosterGroupProperty(group)) != null 729 && r.getAttribute(Roster.getRosterGroupProperty(group)).equals("yes"))); 730 } 731 732 /** 733 * Write the entire roster to a file. 734 * <p> 735 * Creates a new file with the given name, and then calls writeFile (File) 736 * to perform the actual work. 737 * 738 * @param name Filename for new file, including path info as needed. 739 * @throws java.io.FileNotFoundException if file does not exist 740 * @throws java.io.IOException if unable to write file 741 */ 742 void writeFile(String name) throws java.io.FileNotFoundException, java.io.IOException { 743 log.debug("writeFile {}", name); 744 File file = findFile(name); 745 if (file == null) { 746 file = new File(name); 747 } 748 749 writeFile(file); 750 } 751 752 /** 753 * Write the entire roster to a file object. This does not do backup; that 754 * has to be done separately. See writeRosterFile() for a public function 755 * that finds the default location, does a backup and then calls this. 756 * 757 * @param file the file to write to 758 * @throws java.io.IOException if unable to write file 759 */ 760 void writeFile(File file) throws java.io.IOException { 761 // create root element 762 Element root = new Element("roster-config"); // NOI18N 763 root.setAttribute("noNamespaceSchemaLocation", // NOI18N 764 "http://jmri.org/xml/schema/roster" + schemaVersion + ".xsd", // NOI18N 765 org.jdom2.Namespace.getNamespace("xsi", // NOI18N 766 "http://www.w3.org/2001/XMLSchema-instance")); // NOI18N 767 Document doc = newDocument(root); 768 769 // add XSLT processing instruction 770 // <?xml-stylesheet type="text/xsl" href="XSLT/roster.xsl"?> 771 java.util.Map<String, String> m = new java.util.HashMap<>(); 772 m.put("type", "text/xsl"); // NOI18N 773 m.put("href", xsltLocation + "roster2array.xsl"); // NOI18N 774 ProcessingInstruction p = new ProcessingInstruction("xml-stylesheet", m); // NOI18N 775 doc.addContent(0, p); 776 777 String newLocoString = SymbolicProgBundle.getMessage("LabelNewDecoder"); 778 779 //Check the Comment and Decoder Comment fields for line breaks and 780 //convert them to a processor directive for storage in XML 781 //Note: this is also done in the LocoFile.java class to do 782 //the same thing in the indidvidual locomotive roster files 783 //Note: these changes have to be undone after writing the file 784 //since the memory version of the roster is being changed to the 785 //file version for writing 786 synchronized (_list) { 787 _list.forEach((entry) -> { 788 //Extract the RosterEntry at this index and inspect the Comment and 789 //Decoder Comment fields to change any \n characters to <?p?> processor 790 //directives so they can be stored in the xml file and converted 791 //back when the file is read. 792 if (!entry.getId().equals(newLocoString)) { 793 String tempComment = entry.getComment(); 794 StringBuilder xmlComment = new StringBuilder(); 795 796 //transfer tempComment to xmlComment one character at a time, except 797 //when \n is found. In that case, insert <?p?> 798 for (int k = 0; k < tempComment.length(); k++) { 799 if (tempComment.startsWith("\n", k)) { // NOI18N 800 xmlComment.append("<?p?>"); // NOI18N 801 } else { 802 xmlComment.append(tempComment.substring(k, k + 1)); 803 } 804 } 805 entry.setComment(xmlComment.toString()); 806 807 //Now do the same thing for the decoderComment field 808 String tempDecoderComment = entry.getDecoderComment(); 809 StringBuilder xmlDecoderComment = new StringBuilder(); 810 811 for (int k = 0; k < tempDecoderComment.length(); k++) { 812 if (tempDecoderComment.startsWith("\n", k)) { // NOI18N 813 xmlDecoderComment.append("<?p?>"); // NOI18N 814 } else { 815 xmlDecoderComment.append(tempDecoderComment.substring(k, k + 1)); 816 } 817 } 818 entry.setDecoderComment(xmlDecoderComment.toString()); 819 } else { 820 log.debug("skip unsaved roster entry with default name {}", entry.getId()); 821 } 822 }); //All Comments and Decoder Comment line feeds have been changed to processor directives 823 } 824 // add top-level elements 825 Element values = new Element("roster"); // NOI18N 826 root.addContent(values); 827 // add entries 828 synchronized (_list) { 829 _list.stream().forEach((entry) -> { 830 if (!entry.getId().equals(newLocoString)) { 831 values.addContent(entry.store()); 832 } else { 833 log.debug("skip unsaved roster entry with default name {}", entry.getId()); 834 } 835 }); 836 } 837 if (!this.rosterGroups.isEmpty()) { 838 Element rosterGroup = new Element("rosterGroup"); // NOI18N 839 rosterGroups.keySet().stream().forEach((name) -> { 840 Element group = new Element("group"); // NOI18N 841 if (!name.equals(Roster.ALLENTRIES)) { 842 group.addContent(name); 843 rosterGroup.addContent(group); 844 } 845 }); 846 root.addContent(rosterGroup); 847 } 848 849 writeXML(file, doc); 850 851 //Now that the roster has been rewritten in file form we need to 852 //restore the RosterEntry object to its normal \n state for the 853 //Comment and Decoder comment fields, otherwise it can cause problems in 854 //other parts of the program (e.g. in copying a roster) 855 synchronized (_list) { 856 _list.stream().forEach((entry) -> { 857 if (!entry.getId().equals(newLocoString)) { 858 String xmlComment = entry.getComment(); 859 StringBuilder tempComment = new StringBuilder(); 860 861 for (int k = 0; k < xmlComment.length(); k++) { 862 if (xmlComment.startsWith("<?p?>", k)) { // NOI18N 863 tempComment.append("\n"); // NOI18N 864 k = k + 4; 865 } else { 866 tempComment.append(xmlComment.substring(k, k + 1)); 867 } 868 } 869 entry.setComment(tempComment.toString()); 870 871 String xmlDecoderComment = entry.getDecoderComment(); 872 StringBuilder tempDecoderComment = new StringBuilder(); // NOI18N 873 874 for (int k = 0; k < xmlDecoderComment.length(); k++) { 875 if (xmlDecoderComment.startsWith("<?p?>", k)) { // NOI18N 876 tempDecoderComment.append("\n"); // NOI18N 877 k = k + 4; 878 } else { 879 tempDecoderComment.append(xmlDecoderComment.substring(k, k + 1)); 880 } 881 } 882 entry.setDecoderComment(tempDecoderComment.toString()); 883 } else { 884 log.debug("skip unsaved roster entry with default name {}", entry.getId()); 885 } 886 }); 887 } 888 // done - roster now stored, so can't be dirty 889 setDirty(false); 890 firePropertyChange(SAVED, false, true); 891 } 892 893 /** 894 * Name a valid roster entry filename from an entry name. 895 * <ul> 896 * <li>Replaces all problematic characters with "_". 897 * <li>Append .xml suffix 898 * </ul> Does not check for duplicates. 899 * 900 * @return Filename for RosterEntry 901 * @param entry the getId() entry name from the RosterEntry 902 * @throws IllegalArgumentException if called with null or empty entry name 903 * @see RosterEntry#ensureFilenameExists() 904 * @since 2.1.5 905 */ 906 static public String makeValidFilename(String entry) { 907 if (entry == null) { 908 throw new IllegalArgumentException("makeValidFilename requires non-null argument"); 909 } 910 if (entry.isEmpty()) { 911 throw new IllegalArgumentException("makeValidFilename requires non-empty argument"); 912 } 913 914 // name sure there are no bogus chars in name 915 String cleanName = entry.replaceAll("[\\W]", "_"); // remove \W, all non-word (a-zA-Z0-9_) characters // NOI18N 916 917 // ensure suffix 918 return cleanName + ".xml"; // NOI18N 919 } 920 921 /** 922 * Read the contents of a roster XML file into this object. 923 * <p> 924 * Note that this does not clear any existing entries. 925 * 926 * @param name filename of roster file 927 * @throws org.jdom2.JDOMException if file is invalid XML 928 * @throws java.io.IOException if unable to read file 929 */ 930 void readFile(String name) throws org.jdom2.JDOMException, java.io.IOException { 931 // roster exists? 932 if (!(new File(name)).exists()) { 933 log.debug("no roster file found; this is normal if you haven't put decoders in your roster yet"); 934 return; 935 } 936 937 // find root 938 Element root = rootFromName(name); 939 if (root == null) { 940 log.error("Roster file exists, but could not be read; roster not available"); 941 return; 942 } 943 //if (log.isDebugEnabled()) XmlFile.dumpElement(root); 944 945 // decode type, invoke proper processing routine if a decoder file 946 if (root.getChild("roster") != null) { // NOI18N 947 List<Element> l = root.getChild("roster").getChildren("locomotive"); // NOI18N 948 if (log.isDebugEnabled()) { 949 log.debug("readFile sees {} children", l.size()); 950 } 951 l.stream().forEach((e) -> { 952 addEntry(new RosterEntry(e)); 953 }); 954 955 //Scan the object to check the Comment and Decoder Comment fields for 956 //any <?p?> processor directives and change them to back \n characters 957 synchronized (_list) { 958 _list.stream().map((entry) -> { 959 //Extract the Comment field and create a new string for output 960 String tempComment = entry.getComment(); 961 StringBuilder xmlComment = new StringBuilder(); 962 //transfer tempComment to xmlComment one character at a time, except 963 //when <?p?> is found. In that case, insert a \n and skip over those 964 //characters in tempComment. 965 for (int k = 0; k < tempComment.length(); k++) { 966 if (tempComment.startsWith("<?p?>", k)) { // NOI18N 967 xmlComment.append("\n"); // NOI18N 968 k = k + 4; 969 } else { 970 xmlComment.append(tempComment.substring(k, k + 1)); 971 } 972 } 973 entry.setComment(xmlComment.toString()); 974 return entry; 975 }).forEachOrdered((r) -> { 976 //Now do the same thing for the decoderComment field 977 String tempDecoderComment = r.getDecoderComment(); 978 StringBuilder xmlDecoderComment = new StringBuilder(); 979 980 for (int k = 0; k < tempDecoderComment.length(); k++) { 981 if (tempDecoderComment.startsWith("<?p?>", k)) { // NOI18N 982 xmlDecoderComment.append("\n"); // NOI18N 983 k = k + 4; 984 } else { 985 xmlDecoderComment.append(tempDecoderComment.substring(k, k + 1)); 986 } 987 } 988 989 r.setDecoderComment(xmlDecoderComment.toString()); 990 }); 991 } 992 } else { 993 log.error("Unrecognized roster file contents in file: {}", name); 994 } 995 if (root.getChild("rosterGroup") != null) { // NOI18N 996 List<Element> groups = root.getChild("rosterGroup").getChildren("group"); // NOI18N 997 groups.stream().forEach((group) -> { 998 addRosterGroup(group.getText()); 999 }); 1000 } 1001 } 1002 1003 void setDirty(boolean b) { 1004 dirty = b; 1005 } 1006 1007 boolean isDirty() { 1008 return dirty; 1009 } 1010 1011 public void dispose() { 1012 log.debug("dispose"); 1013 if (dirty) { 1014 log.error("Dispose invoked on dirty Roster"); 1015 } 1016 } 1017 1018 /** 1019 * Store the roster in the default place, including making a backup if 1020 * needed. 1021 * <p> 1022 * Uses writeFile(String), a protected method that can write to a specific 1023 * location. 1024 */ 1025 public void writeRoster() { 1026 this.makeBackupFile(this.getRosterIndexPath()); 1027 try { 1028 this.writeFile(this.getRosterIndexPath()); 1029 } catch (IOException e) { 1030 log.error("Exception while writing the new roster file, may not be complete", e); 1031 try { 1032 JOptionPane.showMessageDialog(null, 1033 Bundle.getMessage("ErrorSavingText") + "\n" + e.getMessage(), 1034 Bundle.getMessage("ErrorSavingTitle"), 1035 JOptionPane.ERROR_MESSAGE); 1036 } catch (HeadlessException he) { 1037 // silently ignore failure to display dialog 1038 } 1039 } 1040 } 1041 1042 /** 1043 * Rebuild the Roster index and store it. 1044 */ 1045 public void reindex() { 1046 Roster roster = new Roster(); 1047 for (String fileName : Roster.getAllFileNames()) { 1048 // Read file 1049 try { 1050 Element loco = (new LocoFile()).rootFromName(getRosterFilesLocation() + fileName).getChild("locomotive"); 1051 if (loco != null) { 1052 RosterEntry re = new RosterEntry(loco); 1053 re.setFileName(fileName); 1054 roster.addEntry(re); 1055 } 1056 } catch (JDOMException | IOException ex) { 1057 log.error("Exception while loading loco XML file: {}", fileName, ex); 1058 } 1059 } 1060 1061 this.makeBackupFile(this.getRosterIndexPath()); 1062 try { 1063 roster.writeFile(this.getRosterIndexPath()); 1064 } catch (IOException ex) { 1065 log.error("Exception while writing the new roster file, may not be complete", ex); 1066 } 1067 this.reloadRosterFile(); 1068 log.info("Roster rebuilt, stored in {}", this.getRosterIndexPath()); 1069 } 1070 1071 /** 1072 * Update the in-memory Roster to be consistent with the current roster 1073 * file. This removes any existing roster entries! 1074 */ 1075 public void reloadRosterFile() { 1076 // clear existing 1077 synchronized (_list) { 1078 1079 _list.clear(); 1080 } 1081 this.rosterGroups.clear(); 1082 // and read new 1083 try { 1084 this.readFile(this.getRosterIndexPath()); 1085 } catch (IOException | JDOMException e) { 1086 log.error("Exception during reading while reloading roster", e); 1087 } 1088 } 1089 1090 public void setRosterIndexFileName(String fileName) { 1091 this.rosterIndexFileName = fileName; 1092 } 1093 1094 public String getRosterIndexFileName() { 1095 return this.rosterIndexFileName; 1096 } 1097 1098 public String getRosterIndexPath() { 1099 return this.getRosterLocation() + this.getRosterIndexFileName(); 1100 } 1101 1102 /* 1103 * get the path to the file containing roster entry files. 1104 */ 1105 public String getRosterFilesLocation() { 1106 return getDefault().getRosterLocation() + "roster" + File.separator; 1107 } 1108 1109 /** 1110 * Set the default location for the Roster file, and all individual 1111 * locomotive files. 1112 * 1113 * @param f Absolute pathname to use. A null or "" argument flags a return 1114 * to the original default in the user's files directory. This 1115 * parameter must be a potentially valid path on the system. 1116 */ 1117 public void setRosterLocation(String f) { 1118 String oldRosterLocation = this.rosterLocation; 1119 String p = f; 1120 if (p != null) { 1121 if (p.isEmpty()) { 1122 p = null; 1123 } else { 1124 p = FileUtil.getAbsoluteFilename(p); 1125 if (!p.endsWith(File.separator)) { 1126 p = p + File.separator; 1127 } 1128 } 1129 } 1130 if (p == null) { 1131 p = FileUtil.getUserFilesPath(); 1132 } 1133 this.rosterLocation = p; 1134 log.debug("Setting roster location from {} to {}", oldRosterLocation, this.rosterLocation); 1135 if (this.rosterLocation.equals(FileUtil.getUserFilesPath())) { 1136 log.debug("Roster location reset to default"); 1137 } 1138 if (!this.rosterLocation.equals(oldRosterLocation)) { 1139 this.firePropertyChange(RosterConfigManager.DIRECTORY, oldRosterLocation, this.rosterLocation); 1140 } 1141 this.reloadRosterFile(); 1142 } 1143 1144 /** 1145 * Absolute path to roster file location. 1146 * <p> 1147 * Default is in the user's files directory, but can be set to anything. 1148 * 1149 * @return location of the Roster file 1150 * @see jmri.util.FileUtil#getUserFilesPath() 1151 */ 1152 @Nonnull 1153 public String getRosterLocation() { 1154 return this.rosterLocation; 1155 } 1156 1157 @Override 1158 public synchronized void addPropertyChangeListener(PropertyChangeListener l) { 1159 pcs.addPropertyChangeListener(l); 1160 } 1161 1162 @Override 1163 public synchronized void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) { 1164 pcs.addPropertyChangeListener(propertyName, listener); 1165 } 1166 1167 protected void firePropertyChange(String p, Object old, Object n) { 1168 pcs.firePropertyChange(p, old, n); 1169 } 1170 1171 @Override 1172 public synchronized void removePropertyChangeListener(PropertyChangeListener l) { 1173 pcs.removePropertyChangeListener(l); 1174 } 1175 1176 @Override 1177 public synchronized void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) { 1178 pcs.removePropertyChangeListener(propertyName, listener); 1179 } 1180 1181 @Override 1182 public PropertyChangeListener[] getPropertyChangeListeners() { 1183 return pcs.getPropertyChangeListeners(); 1184 } 1185 1186 @Override 1187 public PropertyChangeListener[] getPropertyChangeListeners(String propertyName) { 1188 return pcs.getPropertyChangeListeners(propertyName); 1189 } 1190 1191 /** 1192 * Notify that the ID of an entry has changed. This doesn't actually change 1193 * the roster contents, but triggers a reordering of the roster contents. 1194 * 1195 * @param r the entry with a changed Id 1196 */ 1197 public void entryIdChanged(RosterEntry r) { 1198 log.debug("EntryIdChanged"); 1199 synchronized (_list) { 1200 Collections.sort(_list, (RosterEntry o1, RosterEntry o2) -> o1.getId().compareToIgnoreCase(o2.getId())); 1201 } 1202 firePropertyChange(CHANGE, null, r); 1203 } 1204 1205 public static String getRosterGroupName(String rosterGroup) { 1206 if (rosterGroup == null) { 1207 return ALLENTRIES; 1208 } 1209 return rosterGroup; 1210 } 1211 1212 /** 1213 * Get the string for a RosterGroup property in a RosterEntry 1214 * 1215 * @param name The name of the rosterGroup 1216 * @return The full property string 1217 */ 1218 public static String getRosterGroupProperty(String name) { 1219 return ROSTER_GROUP_PREFIX + name; 1220 } 1221 1222 /** 1223 * Add a roster group, notifying all listeners of the change. 1224 * <p> 1225 * This method fires the property change notification 1226 * {@value #ROSTER_GROUP_ADDED}. 1227 * 1228 * @param rg The group to be added 1229 */ 1230 public void addRosterGroup(RosterGroup rg) { 1231 if (this.rosterGroups.containsKey(rg.getName())) { 1232 return; 1233 } 1234 this.rosterGroups.put(rg.getName(), rg); 1235 log.debug("firePropertyChange Roster Groups model: {}", rg.getName()); // test for panel redraw after duplication 1236 firePropertyChange(ROSTER_GROUP_ADDED, null, rg.getName()); 1237 } 1238 1239 /** 1240 * Add a roster group, notifying all listeners of the change. 1241 * <p> 1242 * This method creates a {@link jmri.jmrit.roster.rostergroup.RosterGroup}. 1243 * Use {@link #addRosterGroup(jmri.jmrit.roster.rostergroup.RosterGroup) } 1244 * if you need to add a subclass of RosterGroup. This method fires the 1245 * property change notification {@value #ROSTER_GROUP_ADDED}. 1246 * 1247 * @param rg The name of the group to be added 1248 */ 1249 public void addRosterGroup(String rg) { 1250 // do a quick return without creating a new RosterGroup object 1251 // if the roster group aleady exists 1252 if (this.rosterGroups.containsKey(rg)) { 1253 return; 1254 } 1255 this.addRosterGroup(new RosterGroup(rg)); 1256 } 1257 1258 /** 1259 * Add a list of {@link jmri.jmrit.roster.rostergroup.RosterGroup}. 1260 * RosterGroups that are already known to the Roster are ignored. 1261 * 1262 * @param groups RosterGroups to add to the roster. RosterGroups already in 1263 * the roster will not be added again. 1264 */ 1265 public void addRosterGroups(List<RosterGroup> groups) { 1266 groups.stream().forEach((rg) -> { 1267 this.addRosterGroup(rg); 1268 }); 1269 } 1270 1271 public void removeRosterGroup(RosterGroup rg) { 1272 this.delRosterGroupList(rg.getName()); 1273 } 1274 1275 /** 1276 * Delete a roster group, notifying all listeners of the change. 1277 * <p> 1278 * This method fires the property change notification 1279 * "{@value #ROSTER_GROUP_REMOVED}". 1280 * 1281 * @param rg The group to be deleted 1282 */ 1283 public void delRosterGroupList(String rg) { 1284 RosterGroup group = this.rosterGroups.remove(rg); 1285 String str = Roster.getRosterGroupProperty(rg); 1286 group.getEntries().stream().forEach((re) -> { 1287 re.deleteAttribute(str); 1288 }); 1289 firePropertyChange(ROSTER_GROUP_REMOVED, rg, null); 1290 } 1291 1292 /** 1293 * Copy a roster group, adding every entry in the roster group to the new 1294 * group. 1295 * <p> 1296 * If a roster group with the target name already exists, this method 1297 * silently fails to rename the roster group. The GUI method 1298 * CopyRosterGroupAction.performAction() catches this error and informs the 1299 * user. This method fires the property change 1300 * "{@value #ROSTER_GROUP_ADDED}". 1301 * 1302 * @param oldName Name of the roster group to be copied 1303 * @param newName Name of the new roster group 1304 * @see jmri.jmrit.roster.swing.RenameRosterGroupAction 1305 */ 1306 public void copyRosterGroupList(String oldName, String newName) { 1307 if (this.rosterGroups.containsKey(newName)) { 1308 return; 1309 } 1310 this.rosterGroups.put(newName, new RosterGroup(newName)); 1311 String newGroup = Roster.getRosterGroupProperty(newName); 1312 this.rosterGroups.get(oldName).getEntries().stream().forEach((re) -> { 1313 re.putAttribute(newGroup, "yes"); // NOI18N 1314 }); 1315 this.addRosterGroup(new RosterGroup(newName)); 1316 // the firePropertyChange event will be called by addRosterGroup() 1317 } 1318 1319 public void rosterGroupRenamed(String oldName, String newName) { 1320 this.firePropertyChange(Roster.ROSTER_GROUP_RENAMED, oldName, newName); 1321 } 1322 1323 /** 1324 * Rename a roster group, while keeping every entry in the roster group. 1325 * <p> 1326 * If a roster group with the target name already exists, this method 1327 * silently fails to rename the roster group. The GUI method 1328 * RenameRosterGroupAction.performAction() catches this error and informs 1329 * the user. This method fires the property change 1330 * "{@value #ROSTER_GROUP_RENAMED}". 1331 * 1332 * @param oldName Name of the roster group to be renamed 1333 * @param newName New name for the roster group 1334 * @see jmri.jmrit.roster.swing.RenameRosterGroupAction 1335 */ 1336 public void renameRosterGroupList(String oldName, String newName) { 1337 if (this.rosterGroups.containsKey(newName)) { 1338 return; 1339 } 1340 this.rosterGroups.get(oldName).setName(newName); 1341 } 1342 1343 /** 1344 * Get a list of the user defined roster group names. 1345 * <p> 1346 * Strings are immutable, so deleting an item from the copy should not 1347 * affect the system-wide list of roster groups. 1348 * 1349 * @return A list of the roster group names. 1350 */ 1351 public ArrayList<String> getRosterGroupList() { 1352 ArrayList<String> list = new ArrayList<>(this.rosterGroups.keySet()); 1353 Collections.sort(list); 1354 return list; 1355 } 1356 1357 /** 1358 * Get the identifier for all entries in the roster. 1359 * 1360 * @param locale The desired locale 1361 * @return "All Entries" in the specified locale 1362 */ 1363 public static String allEntries(Locale locale) { 1364 return Bundle.getMessage(locale, "ALLENTRIES"); // NOI18N 1365 } 1366 1367 /** 1368 * Get the default roster group. 1369 * <p> 1370 * This method ensures adherence to the RosterGroupSelector protocol 1371 * 1372 * @return The entire roster 1373 */ 1374 @Override 1375 public String getSelectedRosterGroup() { 1376 return getDefaultRosterGroup(); 1377 } 1378 1379 /** 1380 * @return the defaultRosterGroup 1381 */ 1382 public String getDefaultRosterGroup() { 1383 return defaultRosterGroup; 1384 } 1385 1386 /** 1387 * @param defaultRosterGroup the defaultRosterGroup to set 1388 */ 1389 public void setDefaultRosterGroup(String defaultRosterGroup) { 1390 this.defaultRosterGroup = defaultRosterGroup; 1391 InstanceManager.getOptionalDefault(UserPreferencesManager.class).ifPresent((upm) -> { 1392 upm.setProperty(Roster.class.getCanonicalName(), "defaultRosterGroup", defaultRosterGroup); // NOI18N 1393 }); 1394 } 1395 1396 /** 1397 * Get an array of all the RosterEntry-containing files in the target 1398 * directory. 1399 * 1400 * @return the list of file names for entries in this roster 1401 */ 1402 static String[] getAllFileNames() { 1403 // ensure preferences will be found for read 1404 FileUtil.createDirectory(getDefault().getRosterFilesLocation()); 1405 1406 // create an array of file names from roster dir in preferences, count entries 1407 int i; 1408 int np = 0; 1409 String[] sp = null; 1410 if (log.isDebugEnabled()) { 1411 log.debug("search directory {}", getDefault().getRosterFilesLocation()); 1412 } 1413 File fp = new File(getDefault().getRosterFilesLocation()); 1414 if (fp.exists()) { 1415 sp = fp.list(); 1416 if (sp != null) { 1417 for (i = 0; i < sp.length; i++) { 1418 if (sp[i].endsWith(".xml") || sp[i].endsWith(".XML")) { 1419 np++; 1420 } 1421 } 1422 } else { 1423 log.warn("expected directory, but {} was a file", getDefault().getRosterFilesLocation()); 1424 } 1425 } else { 1426 log.warn("{}roster directory was missing, though tried to create it", FileUtil.getUserFilesPath()); 1427 } 1428 1429 // Copy the entries to the final array 1430 String sbox[] = new String[np]; 1431 int n = 0; 1432 if (sp != null && np > 0) { 1433 for (i = 0; i < sp.length; i++) { 1434 if (sp[i].endsWith(".xml") || sp[i].endsWith(".XML")) { 1435 sbox[n++] = sp[i]; 1436 } 1437 } 1438 } 1439 // The resulting array is now sorted on file-name to make it easier 1440 // for humans to read 1441 java.util.Arrays.sort(sbox); 1442 1443 if (log.isDebugEnabled()) { 1444 log.debug("filename list:"); 1445 for (i = 0; i < sbox.length; i++) { 1446 log.debug(" name: {}", sbox[i]); 1447 } 1448 } 1449 return sbox; 1450 } 1451 1452 /** 1453 * Get the groups known to the roster itself. Note that changes to the 1454 * returned Map will not be reflected in the Roster. 1455 * 1456 * @return the rosterGroups 1457 */ 1458 @Nonnull 1459 public HashMap<String, RosterGroup> getRosterGroups() { 1460 return new HashMap<>(rosterGroups); 1461 } 1462 1463 /** 1464 * Changes the key used to lookup a RosterGroup by name. This is a helper 1465 * method that does not fire a notification to any propertyChangeListeners. 1466 * <p> 1467 * To rename a RosterGroup, use 1468 * {@link jmri.jmrit.roster.rostergroup.RosterGroup#setName(java.lang.String)}. 1469 * 1470 * @param group The group being associated with newKey and will be 1471 * disassociated with the key matching 1472 * {@link RosterGroup#getName()}. 1473 * @param newKey The new key by which group can be found in the map of 1474 * RosterGroups. This should match the intended new name of 1475 * group. 1476 */ 1477 public void remapRosterGroup(RosterGroup group, String newKey) { 1478 this.rosterGroups.remove(group.getName()); 1479 this.rosterGroups.put(newKey, group); 1480 } 1481 1482 @Override 1483 public void propertyChange(PropertyChangeEvent evt) { 1484 if (evt.getSource() instanceof RosterEntry) { 1485 if (evt.getPropertyName().equals(RosterEntry.ID)) { 1486 this.entryIdChanged((RosterEntry) evt.getSource()); 1487 } 1488 } 1489 } 1490 1491 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Roster.class); 1492}