001package jmri.jmrit.roster; 002 003import com.fasterxml.jackson.databind.util.StdDateFormat; 004 005import java.awt.HeadlessException; 006import java.awt.Image; 007import java.io.File; 008import java.io.FileNotFoundException; 009import java.io.IOException; 010import java.io.Writer; 011import java.text.*; 012import java.util.*; 013 014import javax.annotation.CheckForNull; 015import javax.annotation.Nonnull; 016import javax.swing.ImageIcon; 017import javax.swing.JLabel; 018 019import jmri.BasicRosterEntry; 020import jmri.DccLocoAddress; 021import jmri.InstanceManager; 022import jmri.LocoAddress; 023import jmri.beans.ArbitraryBean; 024import jmri.jmrit.roster.rostergroup.RosterGroup; 025import jmri.jmrit.symbolicprog.CvTableModel; 026import jmri.jmrit.symbolicprog.VariableTableModel; 027import jmri.util.FileUtil; 028import jmri.util.StringUtil; 029import jmri.util.davidflanagan.HardcopyWriter; 030import jmri.util.jdom.LocaleSelector; 031import jmri.util.swing.JmriJOptionPane; 032 033import org.jdom2.Attribute; 034import org.jdom2.Element; 035import org.jdom2.JDOMException; 036 037/** 038 * RosterEntry represents a single element in a locomotive roster, including 039 * information on how to locate it from decoder information. 040 * <p> 041 * The RosterEntry is the central place to find information about a locomotive's 042 * configuration, including CV and "programming variable" information. 043 * RosterEntry handles persistence through the LocoFile class. Creating a 044 * RosterEntry does not necessarily read the corresponding file (which might not 045 * even exist), please see readFile(), writeFile() member functions. 046 * <p> 047 * All the data attributes have a content, not null. FileName, however, is 048 * special. A null value for it indicates that no physical file is (yet) 049 * associated with this entry. 050 * <p> 051 * When the filePath attribute is non-null, the user has decided to organize the 052 * roster into directories. 053 * <p> 054 * Each entry can have one or more "Attributes" associated with it. These are 055 * (key, value) pairs. The key has to be unique, and currently both objects have 056 * to be Strings. 057 * <p> 058 * All properties, including the "Attributes", are bound. 059 * 060 * @author Bob Jacobsen Copyright (C) 2001, 2002, 2004, 2005, 2009 061 * @author Dennis Miller Copyright 2004 062 * @author Egbert Broerse Copyright (C) 2018 063 * @author Dave Heap Copyright (C) 2019 064 * @see jmri.jmrit.roster.LocoFile 065 */ 066public class RosterEntry extends ArbitraryBean implements RosterObject, BasicRosterEntry { 067 068 // identifiers for property change events and some XML elements 069 public static final String ID = "id"; // NOI18N 070 public static final String FILENAME = "filename"; // NOI18N 071 public static final String ROADNAME = "roadname"; // NOI18N 072 public static final String MFG = "mfg"; // NOI18N 073 public static final String MODEL = "model"; // NOI18N 074 public static final String OWNER = "owner"; // NOI18N 075 public static final String DCC_ADDRESS = "dccaddress"; // NOI18N 076 public static final String LONG_ADDRESS = "longaddress"; // NOI18N 077 public static final String PROTOCOL = "protocol"; // NOI18N 078 public static final String COMMENT = "comment"; // NOI18N 079 public static final String DECODER_MODEL = "decodermodel"; // NOI18N 080 public static final String DECODER_DEVELOPERID = "developerID"; // NOI18N 081 public static final String DECODER_MANUFACTURERID = "manufacturerID"; // NOI18N 082 public static final String DECODER_PRODUCTID = "productID"; // NOI18N 083 public static final String PROGRAMMING = "programming"; // NOI18N 084 public static final String DECODER_FAMILY = "decoderfamily"; // NOI18N 085 public static final String DECODER_MODES = "decoderModes"; // NOI18N 086 public static final String DECODER_COMMENT = "decodercomment"; // NOI18N 087 public static final String DECODER_MAXFNNUM = "decodermaxFnNum"; // NOI18N 088 public static final String DEFAULT_MAXFNNUM = "28"; // NOI18N 089 public static final String IMAGE_FILE_PATH = "imagefilepath"; // NOI18N 090 public static final String ICON_FILE_PATH = "iconfilepath"; // NOI18N 091 public static final String URL = "url"; // NOI18N 092 public static final String DATE_UPDATED = "dateupdated"; // NOI18N 093 public static final String FUNCTION_IMAGE = "functionImage"; // NOI18N 094 public static final String FUNCTION_LABEL = "functionlabel"; // NOI18N 095 public static final String FUNCTION_LOCKABLE = "functionLockable"; // NOI18N 096 public static final String FUNCTION_SELECTED_IMAGE = "functionSelectedImage"; // NOI18N 097 public static final String ATTRIBUTE_UPDATED = "attributeUpdated:"; // NOI18N 098 public static final String ATTRIBUTE_DELETED = "attributeDeleted"; // NOI18N 099 public static final String MAX_SPEED = "maxSpeed"; // NOI18N 100 public static final String SHUNTING_FUNCTION = "IsShuntingOn"; // NOI18N 101 public static final String SPEED_PROFILE = "speedprofile"; // NOI18N 102 public static final String SOUND_LABEL = "soundlabel"; // NOI18N 103 public static final String ATTRIBUTE_OPERATING_DURATION = "OperatingDuration"; // NOI18N 104 public static final String ATTRIBUTE_LAST_OPERATED = "LastOperated"; // NOI18N 105 106 // members to remember all the info 107 protected String _fileName = null; 108 109 protected String _id = ""; 110 protected String _roadName = ""; 111 protected String _roadNumber = ""; 112 protected String _mfg = ""; 113 protected String _owner = ""; 114 protected String _model = ""; 115 protected String _dccAddress = "3"; 116 protected LocoAddress.Protocol _protocol = LocoAddress.Protocol.DCC_SHORT; 117 protected String _comment = ""; 118 protected String _decoderModel = ""; 119 protected String _decoderFamily = ""; 120 protected String _decoderComment = ""; 121 protected String _maxFnNum = DEFAULT_MAXFNNUM; 122 protected String _dateUpdated = ""; 123 protected Date dateModified = null; 124 protected int _maxSpeedPCT = 100; 125 protected String _developerID = ""; 126 protected String _manufacturerID = ""; 127 protected String _productID = ""; 128 protected String _programmingModes = ""; 129 130 /** 131 * Get the highest valid Fn key number for this roster entry. 132 * <dl> 133 * <dt>The default value (28) can be overridden by a "maxFnNum" attribute in 134 * the "model" element of a decoder definition file</dt> 135 * <dd><ul> 136 * <li>A European standard (RCN-212) extends NMRA S9.2.1 up to F68.</li> 137 * <li>ESU LokSound 5 already uses up to F31.</li> 138 * </ul></dd> 139 * </dl> 140 * 141 * @return the highest function number (Fn) supported by this roster entry. 142 * 143 * @see "http://normen.railcommunity.de/RCN-212.pdf" 144 */ 145 public int getMaxFnNumAsInt() { 146 return Integer.parseInt(getMaxFnNum()); 147 } 148 149 protected Map<Integer, String> functionLabels; 150 protected Map<Integer, String> soundLabels; 151 protected Map<Integer, String> functionSelectedImages; 152 protected Map<Integer, String> functionImages; 153 protected Map<Integer, Boolean> functionLockables; 154 protected Map<Integer, Boolean> functionVisibles; 155 protected String _isShuntingOn = ""; 156 157 protected final TreeMap<String, String> attributePairs = new TreeMap<>(); 158 159 protected String _imageFilePath = null; 160 protected String _iconFilePath = null; 161 protected String _URL = ""; 162 163 protected RosterSpeedProfile _sp = null; 164 165 /** 166 * Construct a blank object. 167 */ 168 public RosterEntry() { 169 functionLabels = Collections.synchronizedMap(new HashMap<>()); 170 soundLabels = Collections.synchronizedMap(new HashMap<>()); 171 functionSelectedImages = Collections.synchronizedMap(new HashMap<>()); 172 functionImages = Collections.synchronizedMap(new HashMap<>()); 173 functionLockables = Collections.synchronizedMap(new HashMap<>()); 174 } 175 176 /** 177 * Constructor based on a given file name. 178 * 179 * @param fileName xml file name for the user's Roster entry 180 */ 181 public RosterEntry(String fileName) { 182 this(); 183 _fileName = fileName; 184 } 185 186 /** 187 * Constructor based on a given RosterEntry object and name/ID. 188 * 189 * @param pEntry RosterEntry object 190 * @param pID unique name/ID for the roster entry 191 */ 192 public RosterEntry(RosterEntry pEntry, String pID) { 193 this(); 194 // The ID is different for this element 195 _id = pID; 196 197 // The filename is not set here, rather later 198 _fileName = null; 199 200 // All other items are copied 201 _roadName = pEntry._roadName; 202 _roadNumber = pEntry._roadNumber; 203 _mfg = pEntry._mfg; 204 _model = pEntry._model; 205 _dccAddress = pEntry._dccAddress; 206 _protocol = pEntry._protocol; 207 _comment = pEntry._comment; 208 _decoderModel = pEntry._decoderModel; 209 _decoderFamily = pEntry._decoderFamily; 210 _developerID = pEntry._developerID; 211 _manufacturerID = pEntry._manufacturerID; 212 _productID = pEntry._productID; 213 _programmingModes = pEntry._programmingModes; 214 _decoderComment = pEntry._decoderComment; 215 _owner = pEntry._owner; 216 _imageFilePath = pEntry._imageFilePath; 217 _iconFilePath = pEntry._iconFilePath; 218 _URL = pEntry._URL; 219 _maxSpeedPCT = pEntry._maxSpeedPCT; 220 _isShuntingOn = pEntry._isShuntingOn; 221 222 if (pEntry.functionLabels != null) { 223 pEntry.functionLabels.forEach((key, value) -> { 224 if (value != null) { 225 functionLabels.put(key, value); 226 } 227 }); 228 } 229 if (pEntry.soundLabels != null) { 230 pEntry.soundLabels.forEach((key, value) -> { 231 if (value != null) { 232 soundLabels.put(key, value); 233 } 234 }); 235 } 236 if (pEntry.functionSelectedImages != null) { 237 pEntry.functionSelectedImages.forEach((key, value) -> { 238 if (value != null) { 239 functionSelectedImages.put(key, value); 240 } 241 }); 242 } 243 if (pEntry.functionImages != null) { 244 pEntry.functionImages.forEach((key, value) -> { 245 if (value != null) { 246 functionImages.put(key, value); 247 } 248 }); 249 } 250 if (pEntry.functionLockables != null) { 251 pEntry.functionLockables.forEach((key, value) -> { 252 if (value != null) { 253 functionLockables.put(key, value); 254 } 255 }); 256 } 257 } 258 259 /** 260 * Set the roster ID for this roster entry. 261 * 262 * @param s new ID 263 */ 264 public void setId(String s) { 265 String oldID = _id; 266 _id = s; 267 if (oldID == null || !oldID.equals(s)) { 268 firePropertyChange(RosterEntry.ID, oldID, s); 269 } 270 } 271 272 @Override 273 public String getId() { 274 return _id; 275 } 276 277 /** 278 * Set the file name for this roster entry. 279 * 280 * @param s the new roster entry file name 281 */ 282 public void setFileName(String s) { 283 String oldName = _fileName; 284 _fileName = s; 285 firePropertyChange(RosterEntry.FILENAME, oldName, s); 286 } 287 288 public String getFileName() { 289 return _fileName; 290 } 291 292 public String getPathName() { 293 return Roster.getDefault().getRosterFilesLocation() + _fileName; 294 } 295 296 /** 297 * Ensure the entry has a valid filename. 298 * <p> 299 * If none exists, create one based on the ID string. Does _not_ enforce any 300 * particular naming; you have to check separately for {@literal "<none>"} 301 * or whatever your convention is for indicating an invalid name. Does 302 * replace the space, period, colon, slash and backslash characters so that 303 * the filename will be generally usable. 304 */ 305 public void ensureFilenameExists() { 306 // if there isn't a filename, store using the id 307 if (getFileName() == null || getFileName().isEmpty()) { 308 309 String newFilename = Roster.makeValidFilename(getId()); 310 311 // we don't want to overwrite a file that exists, whether or not 312 // it's in the roster 313 File testFile = new File(Roster.getDefault().getRosterFilesLocation() + newFilename); 314 int count = 0; 315 String oldFilename = newFilename; 316 while (testFile.exists()) { 317 // oops - change filename and try again 318 newFilename = oldFilename.substring(0, oldFilename.length() - 4) + count + ".xml"; 319 count++; 320 log.debug("try to use {} as filename instead of {}", newFilename, oldFilename); 321 testFile = new File(Roster.getDefault().getRosterFilesLocation() + newFilename); 322 } 323 setFileName(newFilename); 324 log.debug("new filename: {}", getFileName()); 325 } 326 } 327 328 public void setRoadName(String s) { 329 String old = _roadName; 330 _roadName = s; 331 firePropertyChange(RosterEntry.ROADNAME, old, s); 332 } 333 334 public String getRoadName() { 335 return _roadName; 336 } 337 338 public void setRoadNumber(String s) { 339 String old = _roadNumber; 340 _roadNumber = s; 341 firePropertyChange(RosterEntry.ROADNAME, old, s); 342 } 343 344 public String getRoadNumber() { 345 return _roadNumber; 346 } 347 348 public void setMfg(String s) { 349 String old = _mfg; 350 _mfg = s; 351 firePropertyChange(RosterEntry.MFG, old, s); 352 } 353 354 public String getMfg() { 355 return _mfg; 356 } 357 358 public void setModel(String s) { 359 String old = _model; 360 _model = s; 361 firePropertyChange(RosterEntry.MODEL, old, s); 362 } 363 364 public String getModel() { 365 return _model; 366 } 367 368 public void setOwner(String s) { 369 String old = _owner; 370 _owner = s; 371 firePropertyChange(RosterEntry.OWNER, old, s); 372 } 373 374 public String getOwner() { 375 if (_owner.isEmpty()) { 376 RosterConfigManager manager = InstanceManager.getNullableDefault(RosterConfigManager.class); 377 if (manager != null) { 378 _owner = manager.getDefaultOwner(); 379 } 380 } 381 return _owner; 382 } 383 384 public void setDccAddress(String s) { 385 String old = _dccAddress; 386 _dccAddress = s; 387 firePropertyChange(RosterEntry.DCC_ADDRESS, old, s); 388 } 389 390 @Override 391 public String getDccAddress() { 392 return _dccAddress; 393 } 394 395 public void setLongAddress(boolean b) { 396 boolean old = false; 397 if (_protocol == LocoAddress.Protocol.DCC_LONG) { 398 old = true; 399 } 400 if (b) { 401 _protocol = LocoAddress.Protocol.DCC_LONG; 402 } else { 403 _protocol = LocoAddress.Protocol.DCC_SHORT; 404 } 405 firePropertyChange(RosterEntry.LONG_ADDRESS, old, b); 406 } 407 408 public RosterSpeedProfile getSpeedProfile() { 409 return _sp; 410 } 411 412 public void setSpeedProfile(RosterSpeedProfile sp) { 413 if (sp.getRosterEntry() != this) { 414 log.error("Attempting to set a speed profile against the wrong roster entry"); 415 return; 416 } 417 RosterSpeedProfile old = this._sp; 418 _sp = sp; 419 this.firePropertyChange(RosterEntry.SPEED_PROFILE, old, this._sp); 420 } 421 422 @Override 423 public boolean isLongAddress() { 424 return _protocol == LocoAddress.Protocol.DCC_LONG; 425 } 426 427 public void setProtocol(LocoAddress.Protocol protocol) { 428 LocoAddress.Protocol old = _protocol; 429 _protocol = protocol; 430 firePropertyChange(RosterEntry.PROTOCOL, old, _protocol); 431 } 432 433 public LocoAddress.Protocol getProtocol() { 434 return _protocol; 435 } 436 437 public String getProtocolAsString() { 438 return _protocol.getPeopleName(); 439 } 440 441 public void setComment(String s) { 442 String old = _comment; 443 _comment = s; 444 firePropertyChange(RosterEntry.COMMENT, old, s); 445 } 446 447 public String getComment() { 448 return _comment; 449 } 450 451 public void setDecoderModel(String s) { 452 String old = _decoderModel; 453 _decoderModel = s; 454 firePropertyChange(RosterEntry.DECODER_MODEL, old, s); 455 } 456 457 public String getDecoderModel() { 458 return _decoderModel; 459 } 460 461 public void setDeveloperID(String s) { 462 String old = _developerID; 463 _developerID = s; 464 firePropertyChange(DECODER_DEVELOPERID, old, s); 465 } 466 467 public String getDeveloperID() { 468 return _developerID; 469 } 470 471 public void setManufacturerID(String s) { 472 String old = _manufacturerID; 473 _manufacturerID = s; 474 firePropertyChange(DECODER_MANUFACTURERID, old, s); 475 } 476 477 public String getManufacturerID() { 478 return _manufacturerID; 479 } 480 481 public void setProductID(@CheckForNull String s) { 482 String old = _productID; 483 if (s == null) {s = "";} 484 _productID = s; 485 firePropertyChange(DECODER_PRODUCTID, old, s); 486 } 487 488 public String getProductID() { 489 return _productID; 490 } 491 492 /** 493 * Set programming modes as defined in a roster entry's decoder definition. 494 * @param s a comma separated string of predefined mode elements 495 */ 496 public void setProgrammingModes(@CheckForNull String s) { 497 String old = _programmingModes; 498 if (s == null) {s = "";} 499 _programmingModes = s; 500 firePropertyChange(DECODER_MODES, old, s); 501 } 502 503 /** 504 * Get the modes as defined in a roster entry's decoder definition. 505 * @return a comma separated string of predefined mode elements 506 */ 507 public String getProgrammingModes() { 508 return _programmingModes; 509 } 510 511 public void setDecoderFamily(String s) { 512 String old = _decoderFamily; 513 _decoderFamily = s; 514 firePropertyChange(RosterEntry.DECODER_FAMILY, old, s); 515 } 516 517 public String getDecoderFamily() { 518 return _decoderFamily; 519 } 520 521 public void setDecoderComment(String s) { 522 String old = _decoderComment; 523 _decoderComment = s; 524 firePropertyChange(RosterEntry.DECODER_COMMENT, old, s); 525 } 526 527 public String getDecoderComment() { 528 return _decoderComment; 529 } 530 531 public void setMaxFnNum(String s) { 532 String old = _maxFnNum; 533 _maxFnNum = s; 534 firePropertyChange(RosterEntry.DECODER_MAXFNNUM, old, s); 535 } 536 537 public String getMaxFnNum() { 538 return _maxFnNum; 539 } 540 541 @Override 542 public DccLocoAddress getDccLocoAddress() { 543 int n; 544 try { 545 n = Integer.parseInt(getDccAddress()); 546 } catch (NumberFormatException e) { 547 log.error("Illegal format for DCC address roster entry: \"{}\" value: \"{}\"", getId(), getDccAddress()); 548 n = 0; 549 } 550 return new DccLocoAddress(n, _protocol); 551 } 552 553 public void setImagePath(String s) { 554 String old = _imageFilePath; 555 _imageFilePath = s; 556 firePropertyChange(RosterEntry.IMAGE_FILE_PATH, old, s); 557 } 558 559 public String getImagePath() { 560 return _imageFilePath; 561 } 562 563 public void setIconPath(String s) { 564 String old = _iconFilePath; 565 _iconFilePath = s; 566 firePropertyChange(RosterEntry.ICON_FILE_PATH, old, s); 567 } 568 569 public String getIconPath() { 570 return _iconFilePath; 571 } 572 573 public void setShuntingFunction(String fn) { 574 String old = this._isShuntingOn; 575 _isShuntingOn = fn; 576 this.firePropertyChange(RosterEntry.SHUNTING_FUNCTION, old, this._isShuntingOn); 577 } 578 579 @Override 580 public String getShuntingFunction() { 581 return _isShuntingOn; 582 } 583 584 public void setURL(String s) { 585 String old = _URL; 586 _URL = s; 587 firePropertyChange(RosterEntry.URL, old, s); 588 } 589 590 public String getURL() { 591 return _URL; 592 } 593 594 public void setDateModified(@Nonnull Date date) { 595 Date old = this.dateModified; 596 this.dateModified = new Date(date.getTime()); 597 this.firePropertyChange(RosterEntry.DATE_UPDATED, old, date); 598 } 599 600 /** 601 * Set the date modified given a string representing a date. 602 * <p> 603 * Tries ISO 8601 and the current Java defaults as formats for parsing a 604 * date. 605 * 606 * @param date the string to parse into a date 607 * @throws ParseException if the date cannot be parsed 608 */ 609 public void setDateModified(@Nonnull String date) throws ParseException { 610 try { 611 // parse using ISO 8601 date format(s) 612 setDateModified(new StdDateFormat().parse(date)); 613 } catch (ParseException ex) { 614 log.debug("ParseException in setDateModified ISO attempt: \"{}\"", date); 615 // next, try parse using defaults since thats how it was saved if saved 616 // by earlier versions of JMRI 617 try { 618 setDateModified(DateFormat.getDateTimeInstance().parse(date)); 619 } catch (ParseException ex2) { 620 // then try with a specific format to handle e.g. "Apr 1, 2016 9:13:36 AM" 621 DateFormat customFmt = new SimpleDateFormat("MMM dd, yyyy hh:mm:ss a"); 622 try { 623 setDateModified(customFmt.parse(date)); 624 } catch (ParseException ex3) { 625 // then try with a specific format to handle e.g. "01-Oct-2016 21:13:36" 626 customFmt = new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss"); 627 setDateModified(customFmt.parse(date)); 628 } 629 } 630 } catch (IllegalArgumentException ex2) { 631 // warn that there's perhaps something wrong with the classpath 632 log.error( 633 "IllegalArgumentException in RosterEntry.setDateModified - this may indicate a problem with the classpath, specifically multiple copies of the 'jackson` library. See release notes"); 634 // parse using defaults since that is how it was saved if saved 635 // by earlier versions of JMRI 636 this.setDateModified(DateFormat.getDateTimeInstance().parse(date)); 637 } 638 } 639 640 @CheckForNull 641 public Date getDateModified() { 642 return this.dateModified; 643 } 644 645 /** 646 * Set the date last updated. 647 * 648 * @param s the string to parse into a date 649 */ 650 protected void setDateUpdated(String s) { 651 String old = _dateUpdated; 652 _dateUpdated = s; 653 try { 654 this.setDateModified(s); 655 } catch (ParseException ex) { 656 log.warn("Unable to parse \"{}\" as a date in roster entry \"{}\".", s, getId()); 657 // property change is fired by setDateModified if s parses as a date 658 firePropertyChange(RosterEntry.DATE_UPDATED, old, s); 659 } 660 } 661 662 /** 663 * Get the date this entry was last modified. Returns the value of 664 * {@link #getDateModified()} in ISO 8601 format if that is not null, 665 * otherwise returns the raw value for the last modified date from the XML 666 * file for the roster entry. 667 * <p> 668 * Use getDateModified() if control over formatting is required 669 * 670 * @return the string representation of the date last modified 671 */ 672 public String getDateUpdated() { 673 Date date = this.getDateModified(); 674 if (date == null) { 675 return _dateUpdated; 676 } else { 677 return new StdDateFormat().format(date); 678 } 679 } 680 681 //openCounter is used purely to indicate if the roster entry has been opened in an editing mode. 682 int openCounter = 0; 683 684 @Override 685 public void setOpen(boolean boo) { 686 if (boo) { 687 openCounter++; 688 } else { 689 openCounter--; 690 } 691 if (openCounter < 0) { 692 openCounter = 0; 693 } 694 } 695 696 @Override 697 public boolean isOpen() { 698 return openCounter != 0; 699 } 700 701 /** 702 * Construct this Entry from XML. 703 * <p> 704 * This member has to remain synchronized with the detailed schema in 705 * xml/schema/locomotive-config.xsd. 706 * 707 * @param e Locomotive XML element 708 */ 709 public RosterEntry(Element e) { 710 functionLabels = Collections.synchronizedMap(new HashMap<>()); 711 soundLabels = Collections.synchronizedMap(new HashMap<>()); 712 functionSelectedImages = Collections.synchronizedMap(new HashMap<>()); 713 functionImages = Collections.synchronizedMap(new HashMap<>()); 714 functionLockables = Collections.synchronizedMap(new HashMap<>()); 715 log.debug("ctor from element {}", e); 716 Attribute a; 717 if ((a = e.getAttribute("id")) != null) { 718 _id = a.getValue(); 719 } else { 720 log.warn("no id attribute in locomotive element when reading roster"); 721 } 722 if ((a = e.getAttribute("fileName")) != null) { 723 _fileName = a.getValue(); 724 } 725 if ((a = e.getAttribute("roadName")) != null) { 726 _roadName = a.getValue(); 727 } 728 if ((a = e.getAttribute("roadNumber")) != null) { 729 _roadNumber = a.getValue(); 730 } 731 if ((a = e.getAttribute("owner")) != null) { 732 _owner = a.getValue(); 733 } 734 if ((a = e.getAttribute("mfg")) != null) { 735 _mfg = a.getValue(); 736 } 737 if ((a = e.getAttribute("model")) != null) { 738 _model = a.getValue(); 739 } 740 if ((a = e.getAttribute("dccAddress")) != null) { 741 _dccAddress = a.getValue(); 742 } 743 744 // file path was saved without default xml config path 745 if ((a = e.getAttribute("imageFilePath")) != null && !a.getValue().isEmpty()) { 746 try { 747 if (FileUtil.getFile(a.getValue()).isFile()) { 748 _imageFilePath = FileUtil.getAbsoluteFilename(a.getValue()); 749 } 750 } catch (FileNotFoundException ex) { 751 try { 752 if (FileUtil.getFile(FileUtil.getUserResourcePath() + a.getValue()).isFile()) { 753 _imageFilePath = FileUtil.getUserResourcePath() + a.getValue(); 754 } 755 } catch (FileNotFoundException ex1) { 756 _imageFilePath = null; 757 } 758 } 759 } 760 if ((a = e.getAttribute("iconFilePath")) != null && !a.getValue().isEmpty()) { 761 try { 762 if (FileUtil.getFile(a.getValue()).isFile()) { 763 _iconFilePath = FileUtil.getAbsoluteFilename(a.getValue()); 764 } 765 } catch (FileNotFoundException ex) { 766 try { 767 if (FileUtil.getFile(FileUtil.getUserResourcePath() + a.getValue()).isFile()) { 768 _iconFilePath = FileUtil.getUserResourcePath() + a.getValue(); 769 } 770 } catch (FileNotFoundException ex1) { 771 _iconFilePath = null; 772 } 773 } 774 } 775 if ((a = e.getAttribute("URL")) != null) { 776 _URL = a.getValue(); 777 } 778 if ((a = e.getAttribute(RosterEntry.SHUNTING_FUNCTION)) != null) { 779 _isShuntingOn = a.getValue(); 780 } 781 if ((a = e.getAttribute(RosterEntry.MAX_SPEED)) != null) { 782 try { 783 _maxSpeedPCT = Integer.parseInt(a.getValue()); 784 } catch ( NumberFormatException ex ) { 785 log.error("Could not set maxSpeedPCT from {} , {}", a.getValue(), ex.getMessage()); 786 } 787 } 788 789 if ((a = e.getAttribute(DECODER_DEVELOPERID)) != null) { 790 _developerID = a.getValue(); 791 } 792 793 if ((a = e.getAttribute(DECODER_MANUFACTURERID)) != null) { 794 _manufacturerID = a.getValue(); 795 } 796 797 if ((a = e.getAttribute(DECODER_PRODUCTID)) != null) { 798 _productID = a.getValue(); 799 } 800 801 if ((a = e.getAttribute(DECODER_MODES)) != null) { 802 _programmingModes = a.getValue(); 803 } 804 805 Element e3; 806 if ((e3 = e.getChild("dateUpdated")) != null) { 807 this.setDateUpdated(e3.getText()); 808 } 809 if ((e3 = e.getChild("locoaddress")) != null) { 810 DccLocoAddress la = (DccLocoAddress) ((new jmri.configurexml.LocoAddressXml()).getAddress(e3)); 811 if (la != null) { 812 _dccAddress = "" + la.getNumber(); 813 _protocol = la.getProtocol(); 814 } else { 815 _dccAddress = ""; 816 _protocol = LocoAddress.Protocol.DCC_SHORT; 817 } 818 } else { // Did not find "locoaddress" element carrying the short/long, probably 819 // because this is an older-format file, so try to use system default. 820 // This is generally the best we can do without parsing the decoder file now 821 // but may give the wrong answer in some cases (low value long addresses on NCE) 822 823 jmri.ThrottleManager tf = jmri.InstanceManager.getNullableDefault(jmri.ThrottleManager.class); 824 int address; 825 try { 826 address = Integer.parseInt(_dccAddress); 827 } catch (NumberFormatException e2) { 828 address = 3; 829 } // ignore, accepting the default value 830 if (tf != null && tf.canBeLongAddress(address) && !tf.canBeShortAddress(address)) { 831 // if it has to be long, handle that 832 _protocol = LocoAddress.Protocol.DCC_LONG; 833 } else if (tf != null && !tf.canBeLongAddress(address) && tf.canBeShortAddress(address)) { 834 // if it has to be short, handle that 835 _protocol = LocoAddress.Protocol.DCC_SHORT; 836 } else { 837 // else guess short address 838 // These people should resave their roster, so we'll warn them 839 warnShortLong(_id); 840 _protocol = LocoAddress.Protocol.DCC_SHORT; 841 842 } 843 } 844 if ((a = e.getAttribute("comment")) != null) { 845 _comment = a.getValue(); 846 } 847 Element d = e.getChild("decoder"); 848 if (d != null) { 849 if ((a = d.getAttribute("model")) != null) { 850 _decoderModel = a.getValue(); 851 } 852 if ((a = d.getAttribute("family")) != null) { 853 _decoderFamily = a.getValue(); 854 } 855 if ((a = d.getAttribute(DECODER_DEVELOPERID)) != null) { 856 _developerID = a.getValue(); 857 } 858 if ((a = d.getAttribute(DECODER_MANUFACTURERID)) != null) { 859 _manufacturerID = a.getValue(); 860 } 861 if ((a = d.getAttribute(DECODER_PRODUCTID)) != null) { 862 _productID = a.getValue(); 863 } 864 if ((a = d.getAttribute("comment")) != null) { 865 _decoderComment = a.getValue(); 866 } 867 if ((a = d.getAttribute("maxFnNum")) != null) { 868 _maxFnNum = a.getValue(); 869 } 870 } 871 872 loadFunctions(e.getChild("functionlabels"), "RosterEntry"); 873 loadSounds(e.getChild("soundlabels"), "RosterEntry"); 874 loadAttributes(e.getChild("attributepairs")); 875 876 if (e.getChild(RosterEntry.SPEED_PROFILE) != null) { 877 _sp = new RosterSpeedProfile(this); 878 _sp.load(e.getChild(RosterEntry.SPEED_PROFILE)); 879 } 880 } 881 882 boolean loadedOnce = false; 883 884 /** 885 * Load function names from a JDOM element. 886 * <p> 887 * Does not change values that are already present! 888 * 889 * @param e3 the XML element containing functions 890 */ 891 public void loadFunctions(Element e3) { 892 this.loadFunctions(e3, "family"); 893 } 894 895 /** 896 * Loads function names from a JDOM element. Does not change values that are 897 * already present! 898 * 899 * @param e3 the XML element containing the functions 900 * @param source "family" if source is the decoder definition, or "model" if 901 * source is the roster entry itself 902 */ 903 public void loadFunctions(Element e3, String source) { 904 /* 905 * Load flag once, means that when the roster entry is edited only the 906 * first set of function labels are displayed ie those saved in the 907 * roster file, rather than those being left blank rather than being 908 * over-written by the defaults linked to the decoder def 909 */ 910 if (loadedOnce) { 911 return; 912 } 913 if (e3 != null) { 914 // load function names 915 List<Element> l = e3.getChildren(RosterEntry.FUNCTION_LABEL); 916 for (Element fn : l) { 917 int num = Integer.parseInt(fn.getAttribute("num").getValue()); 918 String lock = fn.getAttribute("lockable").getValue(); 919 String val = LocaleSelector.getAttribute(fn, "text"); 920 if (val == null) { 921 val = fn.getText(); 922 } 923 if ((this.getFunctionLabel(num) == null) || (source.equalsIgnoreCase("model"))) { 924 this.setFunctionLabel(num, val); 925 this.setFunctionLockable(num, "true".equals(lock)); 926 Attribute a; 927 if ((a = fn.getAttribute("functionImage")) != null && !a.getValue().isEmpty()) { 928 try { 929 if (FileUtil.getFile(a.getValue()).isFile()) { 930 this.setFunctionImage(num, FileUtil.getAbsoluteFilename(a.getValue())); 931 } 932 } catch (FileNotFoundException ex) { 933 try { 934 if (FileUtil.getFile(FileUtil.getUserResourcePath() + a.getValue()).isFile()) { 935 this.setFunctionImage(num, FileUtil.getUserResourcePath() + a.getValue()); 936 } 937 } catch (FileNotFoundException ex1) { 938 this.setFunctionImage(num, null); 939 } 940 } 941 } 942 if ((a = fn.getAttribute("functionImageSelected")) != null && !a.getValue().isEmpty()) { 943 try { 944 if (FileUtil.getFile(a.getValue()).isFile()) { 945 this.setFunctionSelectedImage(num, FileUtil.getAbsoluteFilename(a.getValue())); 946 } 947 } catch (FileNotFoundException ex) { 948 try { 949 if (FileUtil.getFile(FileUtil.getUserResourcePath() + a.getValue()).isFile()) { 950 this.setFunctionSelectedImage(num, FileUtil.getUserResourcePath() + a.getValue()); 951 } 952 } catch (FileNotFoundException ex1) { 953 this.setFunctionSelectedImage(num, null); 954 } 955 } 956 } 957 } 958 } 959 } 960 if (source.equalsIgnoreCase("RosterEntry")) { 961 loadedOnce = true; 962 } 963 } 964 965 private boolean soundLoadedOnce = false; 966 967 /** 968 * Loads sound names from a JDOM element. Does not change values that are 969 * already present! 970 * 971 * @param e3 the XML element containing sound names 972 * @param source "family" if source is the decoder definition, or "model" if 973 * source is the roster entry itself 974 */ 975 public void loadSounds(Element e3, String source) { 976 /* 977 * Load flag once, means that when the roster entry is edited only the 978 * first set of sound labels are displayed ie those saved in the roster 979 * file, rather than those being left blank rather than being 980 * over-written by the defaults linked to the decoder def 981 */ 982 if (soundLoadedOnce) { 983 return; 984 } 985 if (e3 != null) { 986 // load sound names 987 List<Element> l = e3.getChildren(RosterEntry.SOUND_LABEL); 988 for (Element fn : l) { 989 int num = Integer.parseInt(fn.getAttribute("num").getValue()); 990 String val = LocaleSelector.getAttribute(fn, "text"); 991 if (val == null) { 992 val = fn.getText(); 993 } 994 if ((this.getSoundLabel(num) == null) || (source.equalsIgnoreCase("model"))) { 995 this.setSoundLabel(num, val); 996 } 997 } 998 } 999 if (source.equalsIgnoreCase("RosterEntry")) { 1000 soundLoadedOnce = true; 1001 } 1002 } 1003 1004 /** 1005 * Load attribute key/value pairs from a JDOM element. 1006 * 1007 * @param e3 XML element containing roster entry attributes 1008 */ 1009 public void loadAttributes(Element e3) { 1010 if (e3 != null) { 1011 List<Element> l = e3.getChildren("keyvaluepair"); 1012 for (Element fn : l) { 1013 String key = fn.getChild("key").getText(); 1014 String value = fn.getChild("value").getText(); 1015 this.putAttribute(key, value); 1016 } 1017 } 1018 } 1019 1020 /** 1021 * Set the label for a specific function. 1022 * 1023 * @param fn function number, starting with 0 1024 * @param label the label to use 1025 */ 1026 public void setFunctionLabel(int fn, String label) { 1027 if (functionLabels == null) { 1028 functionLabels = Collections.synchronizedMap(new HashMap<>()); 1029 } 1030 String old = functionLabels.get(fn); 1031 functionLabels.put(fn, label); 1032 this.firePropertyChange(RosterEntry.FUNCTION_LABEL + fn, old, label); 1033 } 1034 1035 /** 1036 * If a label has been defined for a specific function, return it, otherwise 1037 * return null. 1038 * 1039 * @param fn function number, starting with 0 1040 * @return function label or null if not defined 1041 */ 1042 public String getFunctionLabel(int fn) { 1043 if (functionLabels == null) { 1044 return null; 1045 } 1046 return functionLabels.get(fn); 1047 } 1048 1049 /** 1050 * Define label for a specific sound. 1051 * 1052 * @param fn sound number, starting with 0 1053 * @param label display label for the sound function 1054 */ 1055 public void setSoundLabel(int fn, String label) { 1056 if (soundLabels == null) { 1057 soundLabels = Collections.synchronizedMap(new HashMap<>()); 1058 } 1059 String old = soundLabels.get(fn); 1060 soundLabels.put(fn, label); 1061 this.firePropertyChange(RosterEntry.SOUND_LABEL + fn, old, label); 1062 } 1063 1064 /** 1065 * If a label has been defined for a specific sound, return it, otherwise 1066 * return null. 1067 * 1068 * @param fn sound number, starting with 0 1069 * @return sound label or null 1070 */ 1071 public String getSoundLabel(int fn) { 1072 if (soundLabels == null) { 1073 return null; 1074 } 1075 return soundLabels.get(fn); 1076 } 1077 1078 public void setFunctionImage(int fn, String s) { 1079 if (functionImages == null) { 1080 functionImages = Collections.synchronizedMap(new HashMap<>()); 1081 } 1082 String old = functionImages.get(fn); 1083 functionImages.put(fn, s); 1084 firePropertyChange(RosterEntry.FUNCTION_IMAGE + fn, old, s); 1085 } 1086 1087 public String getFunctionImage(int fn) { 1088 if (functionImages == null) { 1089 return null; 1090 } 1091 return functionImages.get(fn); 1092 } 1093 1094 public void setFunctionSelectedImage(int fn, String s) { 1095 if (functionSelectedImages == null) { 1096 functionSelectedImages = Collections.synchronizedMap(new HashMap<>()); 1097 } 1098 String old = functionSelectedImages.get(fn); 1099 functionSelectedImages.put(fn, s); 1100 firePropertyChange(RosterEntry.FUNCTION_SELECTED_IMAGE + fn, old, s); 1101 } 1102 1103 public String getFunctionSelectedImage(int fn) { 1104 if (functionSelectedImages == null) { 1105 return null; 1106 } 1107 return functionSelectedImages.get(fn); 1108 } 1109 1110 /** 1111 * Define whether a specific function is lockable. 1112 * 1113 * @param fn function number, starting with 0 1114 * @param lockable true if function is continuous; false if momentary 1115 */ 1116 public void setFunctionLockable(int fn, boolean lockable) { 1117 if (functionLockables == null) { 1118 functionLockables = Collections.synchronizedMap(new HashMap<>()); 1119 functionLockables.put(fn, true); 1120 } 1121 boolean old = ((functionLockables.get(fn) != null) ? functionLockables.get(fn) : true); 1122 functionLockables.put(fn, lockable); 1123 this.firePropertyChange(RosterEntry.FUNCTION_LOCKABLE + fn, old, lockable); 1124 } 1125 1126 /** 1127 * Return the lockable/latchable state of a specific function. Defaults to true. 1128 * 1129 * @param fn function number, starting with 0 1130 * @return true if function is lockable/latchable 1131 */ 1132 public boolean getFunctionLockable(int fn) { 1133 if (functionLockables == null) { 1134 return true; 1135 } 1136 return ((functionLockables.get(fn) != null) ? functionLockables.get(fn) : true); 1137 } 1138 1139 /** 1140 * Define whether a specific function button is visible. 1141 * 1142 * @param fn function number, starting with 0 1143 * @param visible true if function button is visible; false to hide 1144 */ 1145 public void setFunctionVisible(int fn, boolean visible) { 1146 if (functionVisibles == null) { 1147 functionVisibles = Collections.synchronizedMap(new HashMap<>()); 1148 functionVisibles.put(fn, true); 1149 } 1150 boolean old = ((functionVisibles.get(fn) != null) ? functionVisibles.get(fn) : true); 1151 functionVisibles.put(fn, visible); 1152 this.firePropertyChange(RosterEntry.FUNCTION_LOCKABLE + fn, old, visible); 1153 } 1154 1155 /** 1156 * Return the UI visibility of a specific function button. Defaults to true. 1157 * 1158 * @param fn function number, starting with 0 1159 * @return true if function button is visible 1160 */ 1161 public boolean getFunctionVisible(int fn) { 1162 if (functionVisibles == null) { 1163 return true; 1164 } 1165 return ((functionVisibles.get(fn) != null) ? functionVisibles.get(fn) : true); 1166 } 1167 1168 @Override 1169 public void putAttribute(String key, String value) { 1170 String oldValue = getAttribute(key); 1171 attributePairs.put(key, value); 1172 firePropertyChange(RosterEntry.ATTRIBUTE_UPDATED + key, oldValue, value); 1173 } 1174 1175 @Override 1176 public String getAttribute(String key) { 1177 return attributePairs.get(key); 1178 } 1179 1180 @Override 1181 public void deleteAttribute(String key) { 1182 if (attributePairs.containsKey(key)) { 1183 attributePairs.remove(key); 1184 firePropertyChange(RosterEntry.ATTRIBUTE_DELETED, key, null); 1185 } 1186 } 1187 1188 /** 1189 * Provide access to the set of attributes. 1190 * <p> 1191 * This is directly backed access, so e.g. removing an item from this Set 1192 * removes it from the RosterEntry too. 1193 * 1194 * @return a set of attribute keys 1195 */ 1196 public java.util.Set<String> getAttributes() { 1197 return attributePairs.keySet(); 1198 } 1199 1200 @Override 1201 public String[] getAttributeList() { 1202 return attributePairs.keySet().toArray(new String[0]); 1203 } 1204 1205 /** 1206 * List the roster groups this entry is a member of, returning existing 1207 * {@link jmri.jmrit.roster.rostergroup.RosterGroup}s from the default 1208 * {@link jmri.jmrit.roster.Roster} if they exist. 1209 * 1210 * @return list of roster groups 1211 */ 1212 public List<RosterGroup> getGroups() { 1213 return this.getGroups(Roster.getDefault()); 1214 } 1215 1216 /** 1217 * List the roster groups this entry is a member of, returning existing 1218 * {@link jmri.jmrit.roster.rostergroup.RosterGroup}s from the specified 1219 * {@link jmri.jmrit.roster.Roster} if they exist. 1220 * 1221 * @param roster the roster to get matching groups from 1222 * @return list of roster groups 1223 */ 1224 public List<RosterGroup> getGroups(Roster roster) { 1225 List<RosterGroup> groups = new ArrayList<>(); 1226 if (!this.getAttributes().isEmpty()) { 1227 for (String attribute : this.getAttributes()) { 1228 if (attribute.startsWith(Roster.ROSTER_GROUP_PREFIX)) { 1229 String name = attribute.substring(Roster.ROSTER_GROUP_PREFIX.length()); 1230 if (roster.getRosterGroups().containsKey(name)) { 1231 groups.add(roster.getRosterGroups().get(name)); 1232 } else { 1233 groups.add(new RosterGroup(name)); 1234 } 1235 } 1236 } 1237 } 1238 return groups; 1239 } 1240 1241 @Override 1242 public int getMaxSpeedPCT() { 1243 return _maxSpeedPCT; 1244 } 1245 1246 public void setMaxSpeedPCT(int maxSpeedPCT) { 1247 int old = this._maxSpeedPCT; 1248 _maxSpeedPCT = maxSpeedPCT; 1249 this.firePropertyChange(RosterEntry.MAX_SPEED, old, this._maxSpeedPCT); 1250 } 1251 1252 /** 1253 * Warn user that the roster entry needs to be resaved. 1254 * 1255 * @param id roster ID to warn about 1256 */ 1257 protected void warnShortLong(String id) { 1258 log.warn("Roster entry \"{}\" should be saved again to store the short/long address value", id); 1259 } 1260 1261 /** 1262 * Create an XML element to represent this Entry. 1263 * <p> 1264 * This member has to remain synchronized with the detailed schema in 1265 * xml/schema/locomotive-config.xsd. 1266 * 1267 * @return Contents in a JDOM Element 1268 */ 1269 @Override 1270 public Element store() { 1271 Element e = new Element("locomotive"); 1272 e.setAttribute("id", getId()); 1273 e.setAttribute("fileName", getFileName()); 1274 e.setAttribute("roadNumber", getRoadNumber()); 1275 e.setAttribute("roadName", getRoadName()); 1276 e.setAttribute("mfg", getMfg()); 1277 e.setAttribute("owner", getOwner()); 1278 e.setAttribute("model", getModel()); 1279 e.setAttribute("dccAddress", getDccAddress()); 1280 //e.setAttribute("protocol", "" + getProtocol()); 1281 e.setAttribute("comment", getComment()); 1282 e.setAttribute(DECODER_DEVELOPERID, getDeveloperID()); 1283 e.setAttribute(DECODER_MANUFACTURERID, getManufacturerID()); 1284 e.setAttribute(DECODER_PRODUCTID, getProductID()); 1285 e.setAttribute(DECODER_MODES, getProgrammingModes()); 1286 e.setAttribute(RosterEntry.MAX_SPEED, (Integer.toString(getMaxSpeedPCT()))); 1287 // file path are saved without default xml config path 1288 e.setAttribute("imageFilePath", 1289 (this.getImagePath() != null) ? FileUtil.getPortableFilename(this.getImagePath()) : ""); 1290 e.setAttribute("iconFilePath", 1291 (this.getIconPath() != null) ? FileUtil.getPortableFilename(this.getIconPath()) : ""); 1292 e.setAttribute("URL", getURL()); 1293 e.setAttribute(RosterEntry.SHUNTING_FUNCTION, getShuntingFunction()); 1294 if (_dateUpdated.isEmpty()) { 1295 // set date updated to now if never set previously 1296 this.changeDateUpdated(); 1297 } 1298 e.addContent(new Element("dateUpdated").addContent(this.getDateUpdated())); 1299 Element d = new Element("decoder"); 1300 d.setAttribute("model", getDecoderModel()); 1301 d.setAttribute("family", getDecoderFamily()); 1302 d.setAttribute("comment", getDecoderComment()); 1303 d.setAttribute("maxFnNum", getMaxFnNum()); 1304 1305 e.addContent(d); 1306 if (_dccAddress.isEmpty()) { 1307 e.addContent((new jmri.configurexml.LocoAddressXml()).store(null)); // store a null address 1308 } else { 1309 e.addContent((new jmri.configurexml.LocoAddressXml()) 1310 .store(new DccLocoAddress(Integer.parseInt(_dccAddress), _protocol))); 1311 } 1312 1313 if (functionLabels != null) { 1314 Element s = new Element("functionlabels"); 1315 1316 // loop to copy non-null elements 1317 functionLabels.forEach((key, value) -> { 1318 if (value != null && !value.isEmpty()) { 1319 Element fne = new Element(RosterEntry.FUNCTION_LABEL); 1320 fne.setAttribute("num", "" + key); 1321 fne.setAttribute("lockable", getFunctionLockable(key) ? "true" : "false"); 1322 fne.setAttribute("functionImage", 1323 (getFunctionImage(key) != null) ? FileUtil.getPortableFilename(getFunctionImage(key)) : ""); 1324 fne.setAttribute("functionImageSelected", (getFunctionSelectedImage(key) != null) 1325 ? FileUtil.getPortableFilename(getFunctionSelectedImage(key)) : ""); 1326 fne.addContent(value); 1327 s.addContent(fne); 1328 } 1329 }); 1330 e.addContent(s); 1331 } 1332 1333 if (soundLabels != null) { 1334 Element s = new Element("soundlabels"); 1335 1336 // loop to copy non-null elements 1337 soundLabels.forEach((key, value) -> { 1338 if (value != null && !value.isEmpty()) { 1339 Element fne = new Element(RosterEntry.SOUND_LABEL); 1340 fne.setAttribute("num", "" + key); 1341 fne.addContent(value); 1342 s.addContent(fne); 1343 } 1344 }); 1345 e.addContent(s); 1346 } 1347 1348 if (!getAttributes().isEmpty()) { 1349 d = new Element("attributepairs"); 1350 for (String key : getAttributes()) { 1351 d.addContent(new Element("keyvaluepair") 1352 .addContent(new Element("key") 1353 .addContent(key)) 1354 .addContent(new Element("value") 1355 .addContent(getAttribute(key)))); 1356 } 1357 e.addContent(d); 1358 } 1359 if (_sp != null) { 1360 _sp.store(e); 1361 } 1362 return e; 1363 } 1364 1365 @Override 1366 public String titleString() { 1367 return getId(); 1368 } 1369 1370 @Override 1371 public String toString() { 1372 return new StringBuilder() 1373 .append("[RosterEntry: ") 1374 .append(_id) 1375 .append(" ") 1376 .append(_fileName != null ? _fileName : "<null>") 1377 .append(" ") 1378 .append(_roadName) 1379 .append(" ") 1380 .append(_roadNumber) 1381 .append(" ") 1382 .append(_mfg) 1383 .append(" ") 1384 .append(_owner) 1385 .append(" ") 1386 .append(_model) 1387 .append(" ") 1388 .append(_dccAddress) 1389 .append(" ") 1390 .append(_comment) 1391 .append(" ") 1392 .append(_decoderModel) 1393 .append(" ") 1394 .append(_decoderFamily) 1395 .append(" ") 1396 .append(_developerID) 1397 .append(" ") 1398 .append(_manufacturerID) 1399 .append(" ") 1400 .append(_productID) 1401 .append(" ") 1402 .append(_programmingModes) 1403 .append(" ") 1404 .append(_decoderComment) 1405 .append("]") 1406 .toString(); 1407 } 1408 1409 /** 1410 * Write the contents of this RosterEntry back to a file, preserving all 1411 * existing decoder CV content. 1412 * <p> 1413 * This writes the file back in place, with the same decoder-specific 1414 * content. 1415 */ 1416 public void updateFile() { 1417 LocoFile df = new LocoFile(); 1418 1419 String fullFilename = Roster.getDefault().getRosterFilesLocation() + getFileName(); 1420 1421 // read in the content 1422 try { 1423 mRootElement = df.rootFromName(fullFilename); 1424 } catch (JDOMException 1425 | IOException e) { 1426 log.error("Exception while loading loco XML file: {} exception", getFileName(), e); 1427 } 1428 1429 try { 1430 File f = new File(fullFilename); 1431 // do backup 1432 df.makeBackupFile(Roster.getDefault().getRosterFilesLocation() + getFileName()); 1433 1434 // and finally write the file 1435 df.writeFile(f, mRootElement, this.store()); 1436 1437 } catch (Exception e) { 1438 log.error("error during locomotive file output", e); 1439 try { 1440 JmriJOptionPane.showMessageDialog(null, 1441 Bundle.getMessage("ErrorSavingText") + "\n" 1442 + e.getMessage(), 1443 Bundle.getMessage("ErrorSavingTitle"), 1444 JmriJOptionPane.ERROR_MESSAGE); 1445 } catch (HeadlessException he) { 1446 // silently ignore inability to display dialog 1447 } 1448 } 1449 } 1450 1451 /** 1452 * Write the contents of this RosterEntry to a file. 1453 * <p> 1454 * Information on the contents is passed through the parameters, as the 1455 * actual XML creation is done in the LocoFile class. 1456 * 1457 * @param cvModel CV contents to include in file 1458 * @param variableModel Variable contents to include in file 1459 * 1460 */ 1461 public void writeFile(CvTableModel cvModel, VariableTableModel variableModel) { 1462 LocoFile df = new LocoFile(); 1463 1464 // do I/O 1465 FileUtil.createDirectory(Roster.getDefault().getRosterFilesLocation()); 1466 1467 try { 1468 String fullFilename = Roster.getDefault().getRosterFilesLocation() + getFileName(); 1469 File f = new File(fullFilename); 1470 // do backup 1471 df.makeBackupFile(Roster.getDefault().getRosterFilesLocation() + getFileName()); 1472 1473 // changed 1474 changeDateUpdated(); 1475 1476 // and finally write the file 1477 df.writeFile(f, cvModel, variableModel, this); 1478 1479 } catch (Exception e) { 1480 log.error("error during locomotive file output", e); 1481 try { 1482 JmriJOptionPane.showMessageDialog(null, 1483 Bundle.getMessage("ErrorSavingText") + "\n" 1484 + e.getMessage(), 1485 Bundle.getMessage("ErrorSavingTitle"), 1486 JmriJOptionPane.ERROR_MESSAGE); 1487 } catch (HeadlessException he) { 1488 // silently ignore inability to display dialog 1489 } 1490 } 1491 } 1492 1493 /** 1494 * Mark the date updated, e.g. from storing this roster entry. 1495 */ 1496 public void changeDateUpdated() { 1497 // used to create formatted string of now using defaults 1498 this.setDateModified(new Date()); 1499 } 1500 1501 /** 1502 * Store the root element of the JDOM tree representing this RosterEntry. 1503 */ 1504 private Element mRootElement = null; 1505 1506 /** 1507 * Load pre-existing Variable and CvTableModel object with the contents of 1508 * this entry. 1509 * 1510 * @param varModel the variable model to load 1511 * @param cvModel CV contents to load 1512 */ 1513 public void loadCvModel(VariableTableModel varModel, CvTableModel cvModel) { 1514 if (cvModel == null) { 1515 log.error("loadCvModel must be given a non-null argument"); 1516 return; 1517 } 1518 if (mRootElement == null) { 1519 log.error("loadCvModel called before readFile() succeeded"); 1520 return; 1521 } 1522 try { 1523 if (varModel != null) { 1524 LocoFile.loadVariableModel(mRootElement.getChild("locomotive"), varModel); 1525 } 1526 1527 LocoFile.loadCvModel(mRootElement.getChild("locomotive"), cvModel, getManufacturerID(), getDecoderFamily()); 1528 } catch (Exception ex) { 1529 log.error("Error reading roster entry", ex); 1530 try { 1531 JmriJOptionPane.showMessageDialog(null, 1532 Bundle.getMessage("ErrorReadingText") + "\n" + _fileName, 1533 Bundle.getMessage("ErrorReadingTitle"), 1534 JmriJOptionPane.ERROR_MESSAGE); 1535 } catch (HeadlessException he) { 1536 // silently ignore inability to display dialog 1537 } 1538 } 1539 } 1540 1541 /** 1542 * Ultra-compact list view of roster entries. Shows text from fields as 1543 * initially visible in the Roster frame table. 1544 * <p> 1545 * Header is created in 1546 * {@link PrintListAction#actionPerformed(java.awt.event.ActionEvent)} so 1547 * keep column widths identical with values of colWidth below. 1548 * 1549 * @param w writer providing output 1550 */ 1551 public void printEntryLine(HardcopyWriter w) { 1552 // no image 1553 // @see #printEntryDetails(w); 1554 1555 try { 1556 //int textSpace = w.getCharactersPerLine() - 1; // could be used to truncate line. 1557 // for now, text just flows to next line 1558 String thisText; 1559 String thisLine = ""; 1560 1561 // start each entry on a new line 1562 w.write(newLine, 0, 1); 1563 1564 int colWidth = 15; 1565 // roster entry ID (not the filname) 1566 if (_id != null) { 1567 thisText = String.format("%-" + colWidth + "s", _id.substring(0, Math.min(_id.length(), colWidth))); // %- = left align 1568 log.debug("thisText = |{}|, length = {}", thisText, thisText.length()); 1569 } else { 1570 thisText = String.format("%-" + colWidth + "s", "<null>"); 1571 } 1572 thisLine += thisText; 1573 colWidth = 6; 1574 // _dccAddress 1575 thisLine += StringUtil.padString(_dccAddress, colWidth); 1576 colWidth = 6; 1577 // _roadName 1578 thisLine += StringUtil.padString(_roadName, colWidth); 1579 colWidth = 6; 1580 // _roadNumber 1581 thisLine += StringUtil.padString(_roadNumber, colWidth); 1582 colWidth = 6; 1583 // _mfg 1584 thisLine += StringUtil.padString(_mfg, colWidth); 1585 colWidth = 10; 1586 // _model 1587 thisLine += StringUtil.padString(_model, colWidth); 1588 colWidth = 10; 1589 // _decoderModel 1590 thisLine += StringUtil.padString(_decoderModel, colWidth); 1591 colWidth = 12; 1592 // _protocol (type) 1593 thisLine += StringUtil.padString(_protocol.toString(), colWidth); 1594 colWidth = 6; 1595 // _owner 1596 thisLine += StringUtil.padString(_owner, colWidth); 1597 colWidth = 10; 1598 1599 // dateModified (type) 1600 if (dateModified != null) { 1601 DateFormat.getDateTimeInstance().format(dateModified); 1602 thisText = String.format("%-" + colWidth + "s", 1603 dateModified.toString().substring(0, Math.min(dateModified.toString().length(), colWidth))); 1604 thisLine += thisText; 1605 } 1606 // don't include comment and decoder family 1607 1608 w.write(thisLine); 1609 // extra whitespace line after each entry would miss goal of a compact listing 1610 // w.write(newLine, 0, 1); 1611 } catch (IOException e) { 1612 log.error("Error printing RosterEntry: ", e); 1613 } 1614 } 1615 1616 public void printEntry(HardcopyWriter w) { 1617 if (getIconPath() != null) { 1618 ImageIcon icon = new ImageIcon(getIconPath()); 1619 // We use an ImageIcon because it's guaranteed to have been loaded when ctor is complete. 1620 // We set the imagesize to 150x150 pixels 1621 int imagesize = 150; 1622 1623 Image img = icon.getImage(); 1624 int width = img.getWidth(null); 1625 int height = img.getHeight(null); 1626 double widthratio = (double) width / imagesize; 1627 double heightratio = (double) height / imagesize; 1628 double ratio = Math.max(widthratio, heightratio); 1629 width = (int) (width / ratio); 1630 height = (int) (height / ratio); 1631 Image newImg = img.getScaledInstance(width, height, java.awt.Image.SCALE_SMOOTH); 1632 1633 ImageIcon newIcon = new ImageIcon(newImg); 1634 w.writeNoScale(newIcon.getImage(), new JLabel(newIcon)); 1635 // Work out the number of line approx that the image takes up. 1636 // We might need to pad some areas of the roster out, so that things 1637 // look correct and text doesn't overflow into the image. 1638 blanks = (newImg.getHeight(null) - w.getLineAscent()) / w.getLineHeight(); 1639 textSpaceWithIcon 1640 = w.getCharactersPerLine() - ((newImg.getWidth(null) / w.getCharWidth())) - indentWidth - 1; 1641 1642 } 1643 printEntryDetails(w); 1644 } 1645 1646 private int blanks = 0; 1647 private int textSpaceWithIcon = 0; 1648 String indent = " "; 1649 int indentWidth = indent.length(); 1650 String newLine = "\n"; 1651 1652 /** 1653 * Print the roster entry information. 1654 * <p> 1655 * Updated to allow for multiline comment and decoder comment fields. 1656 * Separate write statements for text and line feeds to work around the 1657 * HardcopyWriter bug that misplaces borders. 1658 * 1659 * @param w the HardcopyWriter used to print 1660 */ 1661 public void printEntryDetails(Writer w) { 1662 if (!(w instanceof HardcopyWriter)) { 1663 throw new IllegalArgumentException("No HardcopyWriter instance passed"); 1664 } 1665 int linesAdded = -1; 1666 String title; 1667 String leftMargin = " "; // 3 spaces in front of legend labels 1668 int labelColumn = 19; // pad remaining spaces for legend using fixed width font, forms "%-19s" in line 1669 try { 1670 HardcopyWriter ww = (HardcopyWriter) w; 1671 int textSpace = ww.getCharactersPerLine() - indentWidth - 1; 1672 title = String.format("%-" + labelColumn + "s", 1673 (Bundle.getMessage("MakeLabel", Bundle.getMessage("FieldID")))); // I18N ID: 1674 if ((textSpaceWithIcon != 0) && (linesAdded < blanks)) { 1675 linesAdded = writeWrappedComment(w, _id, leftMargin + title, textSpaceWithIcon) + linesAdded; 1676 } else { 1677 linesAdded = writeWrappedComment(w, _id, leftMargin + title, textSpace) + linesAdded; 1678 } 1679 title = String.format("%-" + labelColumn + "s", 1680 (Bundle.getMessage("MakeLabel", Bundle.getMessage("FieldFilename")))); // I18N Filename: 1681 if ((textSpaceWithIcon != 0) && (linesAdded < blanks)) { 1682 linesAdded = writeWrappedComment(w, _fileName != null ? _fileName : "<null>", leftMargin + title, 1683 textSpaceWithIcon) + linesAdded; 1684 } else { 1685 linesAdded = writeWrappedComment(w, _fileName != null ? _fileName : "<null>", leftMargin + title, 1686 textSpace) + linesAdded; 1687 } 1688 1689 if (!(_roadName.isEmpty())) { 1690 title = String.format("%-" + labelColumn + "s", 1691 (Bundle.getMessage("MakeLabel", Bundle.getMessage("FieldRoadName")))); // I18N Road name: 1692 if ((textSpaceWithIcon != 0) && (linesAdded < blanks)) { 1693 linesAdded = writeWrappedComment(w, _roadName, leftMargin + title, textSpaceWithIcon) + linesAdded; 1694 } else { 1695 linesAdded = writeWrappedComment(w, _roadName, leftMargin + title, textSpace) + linesAdded; 1696 } 1697 } 1698 if (!(_roadNumber.isEmpty())) { 1699 title = String.format("%-" + labelColumn + "s", 1700 (Bundle.getMessage("MakeLabel", Bundle.getMessage("FieldRoadNumber")))); // I18N Road number: 1701 1702 if ((textSpaceWithIcon != 0) && (linesAdded < blanks)) { 1703 linesAdded 1704 = writeWrappedComment(w, _roadNumber, leftMargin + title, textSpaceWithIcon) + linesAdded; 1705 } else { 1706 linesAdded = writeWrappedComment(w, _roadNumber, leftMargin + title, textSpace) + linesAdded; 1707 } 1708 } 1709 if (!(_mfg.isEmpty())) { 1710 title = String.format("%-" + labelColumn + "s", 1711 (Bundle.getMessage("MakeLabel", Bundle.getMessage("FieldManufacturer")))); // I18N Manufacturer: 1712 1713 if ((textSpaceWithIcon != 0) && (linesAdded < blanks)) { 1714 linesAdded = writeWrappedComment(w, _mfg, leftMargin + title, textSpaceWithIcon) + linesAdded; 1715 } else { 1716 linesAdded = writeWrappedComment(w, _mfg, leftMargin + title, textSpace) + linesAdded; 1717 } 1718 } 1719 if (!(_owner.isEmpty())) { 1720 title = String.format("%-" + labelColumn + "s", 1721 (Bundle.getMessage("MakeLabel", Bundle.getMessage("FieldOwner")))); // I18N Owner: 1722 1723 if ((textSpaceWithIcon != 0) && (linesAdded < blanks)) { 1724 linesAdded = writeWrappedComment(w, _owner, leftMargin + title, textSpaceWithIcon) + linesAdded; 1725 } else { 1726 linesAdded = writeWrappedComment(w, _owner, leftMargin + title, textSpace) + linesAdded; 1727 } 1728 } 1729 if (!(_model.isEmpty())) { 1730 title = String.format("%-" + labelColumn + "s", 1731 (Bundle.getMessage("MakeLabel", Bundle.getMessage("FieldModel")))); // I18N Model: 1732 if ((textSpaceWithIcon != 0) && (linesAdded < blanks)) { 1733 linesAdded = writeWrappedComment(w, _model, leftMargin + title, textSpaceWithIcon) + linesAdded; 1734 } else { 1735 linesAdded = writeWrappedComment(w, _model, leftMargin + title, textSpace) + linesAdded; 1736 } 1737 } 1738 if (!(_dccAddress.isEmpty())) { 1739 w.write(newLine, 0, 1); 1740 title = String.format("%-" + labelColumn + "s", 1741 (Bundle.getMessage("MakeLabel", Bundle.getMessage("FieldDCCAddress")))); // I18N DCC Address: 1742 String s = leftMargin + title + _dccAddress; 1743 w.write(s, 0, s.length()); 1744 linesAdded++; 1745 } 1746 1747 // If there is a comment field, then wrap it using the new wrapCommment() 1748 // method and print it 1749 if (!(_comment.isEmpty())) { 1750 // Because the text will fill the width if the roster entry has an icon 1751 // then we need to add some blank lines to prevent the comment text going 1752 // through the picture. 1753 for (int i = 0; i < (blanks - linesAdded); i++) { 1754 w.write(newLine, 0, 1); 1755 } 1756 // As we have added the blank lines to pad out the comment we will 1757 // reset the number of blanks to 0. 1758 if (blanks != 0) { 1759 blanks = 0; 1760 } 1761 title = String.format("%-" + labelColumn + "s", 1762 (Bundle.getMessage("MakeLabel", Bundle.getMessage("FieldComment")))); // I18N Comment: 1763 linesAdded = writeWrappedComment(w, _comment, leftMargin + title, textSpace) + linesAdded; 1764 } 1765 if (!(_decoderModel.isEmpty())) { 1766 title = String.format("%-" + labelColumn + "s", 1767 (Bundle.getMessage("MakeLabel", Bundle.getMessage("FieldDecoderModel")))); // I18N Decoder Model: 1768 if ((textSpaceWithIcon != 0) && (linesAdded < blanks)) { 1769 linesAdded 1770 = writeWrappedComment(w, _decoderModel, leftMargin + title, textSpaceWithIcon) + linesAdded; 1771 } else { 1772 linesAdded = writeWrappedComment(w, _decoderModel, leftMargin + title, textSpace) + linesAdded; 1773 } 1774 } 1775 if (!(_decoderFamily.isEmpty())) { 1776 title = String.format("%-" + labelColumn + "s", 1777 (Bundle.getMessage("MakeLabel", Bundle.getMessage("FieldDecoderFamily")))); // I18N Decoder Family: 1778 if ((textSpaceWithIcon != 0) && (linesAdded < blanks)) { 1779 linesAdded 1780 = writeWrappedComment(w, _decoderFamily, leftMargin + title, textSpaceWithIcon) + linesAdded; 1781 } else { 1782 linesAdded = writeWrappedComment(w, _decoderFamily, leftMargin + title, textSpace) + linesAdded; 1783 } 1784 } 1785 if (!(_programmingModes.isEmpty())) { 1786 title = String.format("%-" + labelColumn + "s", 1787 (Bundle.getMessage("MakeLabel", Bundle.getMessage("FieldDecoderModes")))); // I18N Programming Mode(s): 1788 if ((textSpaceWithIcon != 0) && (linesAdded < blanks)) { 1789 linesAdded 1790 = writeWrappedComment(w, _programmingModes, leftMargin + title, textSpaceWithIcon) + linesAdded; 1791 } else { 1792 linesAdded = writeWrappedComment(w, _programmingModes, leftMargin + title, textSpace) + linesAdded; 1793 } 1794 } 1795 1796 // If there is a decoderComment field, need to wrap it 1797 if (!(_decoderComment.isEmpty())) { 1798 // Because the text will fill the width if the roster entry has an icon 1799 // then we need to add some blank lines to prevent the comment text going 1800 // through the picture. 1801 for (int i = 0; i < (blanks - linesAdded); i++) { 1802 w.write(newLine, 0, 1); 1803 } 1804 // As we have added the blank lines to pad out the comment we will 1805 // reset the number of blanks to 0. 1806 if (blanks != 0) { 1807 blanks = 0; 1808 } 1809 title = String.format("%-" + labelColumn + "s", 1810 (Bundle.getMessage("MakeLabel", Bundle.getMessage("FieldDecoderComment")))); // I18N Decoder Comment: 1811 linesAdded = writeWrappedComment(w, _decoderComment, leftMargin + title, textSpace) + linesAdded; 1812 } 1813 w.write(newLine, 0, 1); 1814 for (int i = -1; i < (blanks - linesAdded); i++) { 1815 w.write(newLine, 0, 1); 1816 } 1817 } catch (IOException e) { 1818 log.error("Error printing RosterEntry", e); 1819 } 1820 } 1821 1822 private int writeWrappedComment(Writer w, String text, String title, int textSpace) { 1823 Vector<String> commentVector = wrapComment(text, textSpace); 1824 1825 // Now have a vector of text pieces and line feeds that will all 1826 // fit in the allowed space. Print each piece, prefixing the first one 1827 // with the label and indenting any remaining. 1828 String s; 1829 int k = 0; 1830 try { 1831 w.write(newLine, 0, 1); 1832 s = title + commentVector.elementAt(k); 1833 w.write(s, 0, s.length()); 1834 k++; 1835 while (k < commentVector.size()) { 1836 String token = commentVector.elementAt(k); 1837 if (!token.equals("\n")) { 1838 s = indent + token; 1839 } else { 1840 s = token; 1841 } 1842 w.write(s, 0, s.length()); 1843 k++; 1844 } 1845 } catch (IOException e) { 1846 log.error("Error printing RosterEntry", e); 1847 } 1848 return k; 1849 } 1850 1851 /** 1852 * Line wrap a comment. 1853 * 1854 * @param comment the comment to wrap at word boundaries 1855 * @param textSpace the width of the space to print 1856 * 1857 * @return comment wrapped to fit given width 1858 */ 1859 public Vector<String> wrapComment(String comment, int textSpace) { 1860 //Tokenize the string using \n to separate the text on mulitple lines 1861 //and create a vector to hold the processed text pieces 1862 StringTokenizer commentTokens = new StringTokenizer(comment, "\n", true); 1863 Vector<String> textVector = new Vector<>(commentTokens.countTokens()); 1864 while (commentTokens.hasMoreTokens()) { 1865 String commentToken = commentTokens.nextToken(); 1866 int startIndex = 0; 1867 int endIndex; 1868 //Check each token to see if it needs to have a line wrap. 1869 //Get a piece of the token, either the size of the allowed space or 1870 //a shorter piece if there isn't enough text to fill the space 1871 if (commentToken.length() < startIndex + textSpace) { 1872 //the piece will fit so extract it and put it in the vector 1873 textVector.addElement(commentToken); 1874 } else { 1875 //Piece too long to fit. Extract a piece the size of the textSpace 1876 //and check for farthest right space for word wrapping. 1877 log.debug("token: /{}/", commentToken); 1878 1879 while (startIndex < commentToken.length()) { 1880 String tokenPiece = commentToken.substring(startIndex, startIndex + textSpace); 1881 if (log.isDebugEnabled()) { 1882 log.debug("loop: /{}/ {}", tokenPiece, tokenPiece.lastIndexOf(" ")); 1883 } 1884 if (tokenPiece.lastIndexOf(" ") == -1) { 1885 //If no spaces, put the whole piece in the vector and add a line feed, then 1886 //increment the startIndex to reposition for extracting next piece 1887 textVector.addElement(tokenPiece); 1888 textVector.addElement(newLine); 1889 startIndex += textSpace; 1890 } else { 1891 //If there is at least one space, extract up to and including the 1892 //last space and put in the vector as well as a line feed 1893 endIndex = tokenPiece.lastIndexOf(" ") + 1; 1894 log.debug("tokenPiece /{}/ {} {}", tokenPiece, startIndex, endIndex); 1895 1896 textVector.addElement(tokenPiece.substring(0, endIndex)); 1897 textVector.addElement(newLine); 1898 startIndex += endIndex; 1899 } 1900 //Check the remaining piece to see if it fits - startIndex now points 1901 //to the start of the next piece 1902 if (commentToken.substring(startIndex).length() < textSpace) { 1903 //It will fit so just insert it, otherwise will cycle through the 1904 //while loop and the checks above will take care of the remainder. 1905 //Line feed is not required as this is the last part of the token. 1906 textVector.addElement(commentToken.substring(startIndex)); 1907 startIndex += textSpace; 1908 } 1909 } 1910 } 1911 } 1912 return textVector; 1913 } 1914 1915 /** 1916 * Read a file containing the contents of this RosterEntry. 1917 * <p> 1918 * This has to be done before a call to loadCvModel, for example. 1919 */ 1920 public void readFile() { 1921 if (getFileName() == null) { 1922 log.warn("readFile invoked with null filename"); 1923 return; 1924 } else { 1925 log.debug("readFile invoked with filename {}", getFileName()); 1926 } 1927 1928 LocoFile lf = new LocoFile(); // used as a temporary 1929 String file = Roster.getDefault().getRosterFilesLocation() + getFileName(); 1930 if (!(new File(file).exists())) { 1931 // try without prefix 1932 file = getFileName(); 1933 } 1934 try { 1935 mRootElement = lf.rootFromName(file); 1936 } catch (JDOMException | IOException e) { 1937 log.error("Exception while loading loco XML file: {} from {}", getFileName(), file, e); 1938 } 1939 } 1940 1941 /** 1942 * Create a RosterEntry from a file. 1943 * 1944 * @param file The file containing the RosterEntry 1945 * @return a new RosterEntry 1946 * @throws JDOMException if unable to parse file 1947 * @throws IOException if unable to read file 1948 */ 1949 public static RosterEntry fromFile(@Nonnull File file) throws JDOMException, IOException { 1950 Element loco = (new LocoFile()).rootFromFile(file).getChild("locomotive"); 1951 if (loco == null) { 1952 throw new JDOMException("missing expected element"); 1953 } 1954 RosterEntry re = new RosterEntry(loco); 1955 re.setFileName(file.getName()); 1956 return re; 1957 } 1958 1959 @Override 1960 public String getDisplayName() { 1961 if (this.getRoadName() != null && !this.getRoadName().isEmpty()) { // NOI18N 1962 return Bundle.getMessage("RosterEntryDisplayName", this.getDccAddress(), this.getRoadName(), 1963 this.getRoadNumber()); // NOI18N 1964 } else { 1965 return Bundle.getMessage("RosterEntryDisplayName", this.getDccAddress(), this.getId(), ""); // NOI18N 1966 } 1967 } 1968 1969 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(RosterEntry.class); 1970 1971}