001package jmri.jmrix.nce.consist; 002 003import java.io.File; 004import java.io.IOException; 005import java.util.ArrayList; 006import java.util.List; 007import javax.swing.JComboBox; 008import jmri.InstanceManagerAutoDefault; 009import jmri.InstanceManagerAutoInitialize; 010import jmri.jmrit.XmlFile; 011import jmri.jmrit.roster.Roster; 012import org.jdom2.Document; 013import org.jdom2.Element; 014import org.jdom2.JDOMException; 015import org.jdom2.ProcessingInstruction; 016import org.slf4j.Logger; 017import org.slf4j.LoggerFactory; 018 019/** 020 * NCE Consist Roster manages and manipulates a roster of consists. 021 * <p> 022 * It works with the "consist-roster-config" XML DTD to load and store its 023 * information. 024 * <p> 025 * This is an in-memory representation of the roster xml file (see below for 026 * constants defining name and location). As such, this class is also 027 * responsible for the "dirty bit" handling to ensure it gets written. As a 028 * temporary reliability enhancement, all changes to this structure are now 029 * being written to a backup file, and a copy is made when the file is opened. 030 * <p> 031 * Multiple Roster objects don't make sense, so we use an "instance" member to 032 * navigate to a single one. 033 * <p> 034 * This predates the "XmlFile" base class, so doesn't use it. Not sure whether 035 * it should... 036 * <p> 037 * The only bound property is the list of s; a PropertyChangedEvent is fired 038 * every time that changes. 039 * <p> 040 * The entries are stored in an ArrayList, sorted alphabetically. That sort is 041 * done manually each time an entry is added. 042 * 043 * @author Bob Jacobsen Copyright (C) 2001; Dennis Miller Copyright 2004 044 * @author Daniel Boudreau (C) 2008 045 * @see NceConsistRosterEntry 046 */ 047public class NceConsistRoster extends XmlFile implements InstanceManagerAutoDefault, InstanceManagerAutoInitialize { 048 049 public NceConsistRoster() { 050 } 051 052 /** 053 * Add a RosterEntry object to the in-memory Roster. 054 * 055 * @param e Entry to add 056 */ 057 public void addEntry(NceConsistRosterEntry e) { 058 log.debug("Add entry {}", e); 059 int i = _list.size() - 1;// Last valid index 060 while (i >= 0) { 061 if (e.getId().compareTo(_list.get(i).getId())> 0) { 062 break; // I can never remember whether I want break or continue here 063 } 064 i--; 065 } 066 _list.add(i + 1, e); 067 setDirty(true); 068 firePropertyChange("add", null, e); 069 } 070 071 /** 072 * Remove a RosterEntry object from the in-memory Roster. This does not 073 * delete the file for the RosterEntry! 074 * 075 * @param e Entry to remove 076 */ 077 public void removeEntry(NceConsistRosterEntry e) { 078 log.debug("Remove entry {}", e); 079 _list.remove(e); 080 setDirty(true); 081 firePropertyChange("remove", null, e); 082 } 083 084 /** 085 * @return Number of entries in the Roster 086 */ 087 public int numEntries() { 088 return _list.size(); 089 } 090 091 /** 092 * Return a combo box containing the entire ConsistRoster. 093 * <p> 094 * This is based on a single model, so it can be updated when the 095 * ConsistRoster changes. 096 * @return combo box of whole roster 097 * 098 */ 099 public JComboBox<String> fullRosterComboBox() { 100 return matchingComboBox(null, null, null, null, 101 null, null, null, null, null, 102 null); 103 } 104 105 /** 106 * Get a JComboBox representing the choices that match. There's 10 elements. 107 * @param roadName value to match against roster roadname field 108 * @param roadNumber value to match against roster roadnumber field 109 * @param consistNumber value to match against roster consist number field 110 * @param eng1Address value to match against roster 1st engine address field 111 * @param eng2Address value to match against roster 2nd engine address field 112 * @param eng3Address value to match against roster 3rd engine address field 113 * @param eng4Address value to match against roster 4th engine address field 114 * @param eng5Address value to match against roster 5th engine address field 115 * @param eng6Address value to match against roster 6th engine address field 116 * @param id value to match against roster id field 117 * @return combo box of matching roster entries 118 */ 119 public JComboBox<String> matchingComboBox(String roadName, String roadNumber, 120 String consistNumber, String eng1Address, String eng2Address, 121 String eng3Address, String eng4Address, String eng5Address, 122 String eng6Address, String id) { 123 List<NceConsistRosterEntry> l = matchingList(roadName, roadNumber, consistNumber, eng1Address, 124 eng2Address, eng3Address, eng4Address, eng5Address, 125 eng6Address, id); 126 JComboBox<String> b = new JComboBox<>(); 127 for (int i = 0; i < l.size(); i++) { 128 NceConsistRosterEntry r = _list.get(i); 129 b.addItem(r.titleString()); 130 } 131 return b; 132 } 133 134 public void updateComboBox(JComboBox<String> box) { 135 List<NceConsistRosterEntry> l = matchingList(null, null, null, 136 null, null, null, null, null, 137 null, null); 138 box.removeAllItems(); 139 for (int i = 0; i < l.size(); i++) { 140 NceConsistRosterEntry r = _list.get(i); 141 box.addItem(r.titleString()); 142 } 143 } 144 145 /** 146 * Return RosterEntry from a "title" string, ala selection in 147 * matchingComboBox 148 * @param title title to search for in consist roster 149 * @return matching consist roster entry 150 */ 151 public NceConsistRosterEntry entryFromTitle(String title) { 152 for (int i = 0; i < numEntries(); i++) { 153 NceConsistRosterEntry r = _list.get(i); 154 if (r.titleString().equals(title)) { 155 return r; 156 } 157 } 158 return null; 159 } 160 161 /** 162 * List of contained RosterEntry elements. 163 */ 164 protected List<NceConsistRosterEntry> _list = new ArrayList<>(); 165 166 /** 167 * Get a List of entries matching some information. The list may have null 168 * contents. 169 * @param roadName value to match against roster roadname field 170 * @param roadNumber value to match against roster roadnumber field 171 * @param consistNumber value to match against roster consist number field 172 * @param eng1Address value to match against roster 1st engine address field 173 * @param eng2Address value to match against roster 2nd engine address field 174 * @param eng3Address value to match against roster 3rd engine address field 175 * @param eng4Address value to match against roster 4th engine address field 176 * @param eng5Address value to match against roster 5th engine address field 177 * @param eng6Address value to match against roster 6th engine address field 178 * @param id value to match against roster id field 179 * @return list of consist roster entries matching request 180 */ 181 public List<NceConsistRosterEntry> matchingList(String roadName, String roadNumber, 182 String consistNumber, String eng1Address, String eng2Address, 183 String eng3Address, String eng4Address, String eng5Address, 184 String eng6Address, String id) { 185 List<NceConsistRosterEntry> l = new ArrayList<>(); 186 for (int i = 0; i < numEntries(); i++) { 187 if (checkEntry(i, roadName, roadNumber, consistNumber, eng1Address, 188 eng2Address, eng3Address, eng4Address, eng5Address, 189 eng6Address, id)) { 190 l.add(_list.get(i)); 191 } 192 } 193 return l; 194 } 195 196 /** 197 * Check if an entry consistent with specific properties. A null String 198 * entry always matches. Strings are used for convenience in GUI building. 199 * @param i index to consist roster entry 200 * @param roadName value to match against roster roadname field 201 * @param roadNumber value to match against roster roadnumber field 202 * @param consistNumber value to match against roster consist number field 203 * @param loco1Address value to match against roster 1st engine address field 204 * @param loco2Address value to match against roster 2nd engine address field 205 * @param loco3Address value to match against roster 3rd engine address field 206 * @param loco4Address value to match against roster 4th engine address field 207 * @param loco5Address value to match against roster 5th engine address field 208 * @param loco6Address value to match against roster 6th engine address field 209 * @param id value to match against roster id field 210 * @return true if values provided matches indexed entry 211 */ 212 public boolean checkEntry(int i, String roadName, String roadNumber, 213 String consistNumber, String loco1Address, String loco2Address, 214 String loco3Address, String loco4Address, String loco5Address, 215 String loco6Address, String id) { 216 NceConsistRosterEntry r = _list.get(i); 217 if (id != null && !id.equals(r.getId())) { 218 return false; 219 } 220 if (roadName != null && !roadName.equals(r.getRoadName())) { 221 return false; 222 } 223 if (roadNumber != null && !roadNumber.equals(r.getRoadNumber())) { 224 return false; 225 } 226 if (consistNumber != null && !consistNumber.equals(r.getConsistNumber())) { 227 return false; 228 } 229 if (loco1Address != null && !loco1Address.equals(r.getLoco1DccAddress())) { 230 return false; 231 } 232 if (loco2Address != null && !loco2Address.equals(r.getLoco2DccAddress())) { 233 return false; 234 } 235 if (loco3Address != null && !loco3Address.equals(r.getLoco3DccAddress())) { 236 return false; 237 } 238 if (loco4Address != null && !loco4Address.equals(r.getLoco4DccAddress())) { 239 return false; 240 } 241 if (loco5Address != null && !loco5Address.equals(r.getLoco5DccAddress())) { 242 return false; 243 } 244 if (loco6Address != null && !loco6Address.equals(r.getLoco6DccAddress())) { 245 return false; 246 } 247 return true; 248 } 249 250 /** 251 * Write the entire roster to a file. This does not do backup; that has to 252 * be done separately. See writeRosterFile() for a function that finds the 253 * default location, does a backup and then calls this. 254 * 255 * @param name Filename for new file, including path info as needed. 256 * @throws java.io.FileNotFoundException when file not found 257 * @throws java.io.IOException when fault accessing file 258 */ 259 void writeFile(String name) throws java.io.FileNotFoundException, java.io.IOException { 260 log.debug("writeFile {}", name); 261 // This is taken in large part from "Java and XML" page 368 262 File file = findFile(name); 263 if (file == null) { 264 file = new File(name); 265 } 266 // create root element 267 Element root = new Element("consist-roster-config"); 268 Document doc = newDocument(root, dtdLocation + "consist-roster-config.dtd"); 269 270 // add XSLT processing instruction 271 java.util.Map<String, String> m = new java.util.HashMap<>(); 272 m.put("type", "text/xsl"); 273 m.put("href", xsltLocation + "consistRoster.xsl"); 274 ProcessingInstruction p = new ProcessingInstruction("xml-stylesheet", m); 275 doc.addContent(0, p); 276 277 //Check the Comment and Decoder Comment fields for line breaks and 278 //convert them to a processor directive for storage in XML 279 //Note: this is also done in the LocoFile.java class to do 280 //the same thing in the indidvidual locomotive roster files 281 //Note: these changes have to be undone after writing the file 282 //since the memory version of the roster is being changed to the 283 //file version for writing 284 for (int i = 0; i < numEntries(); i++) { 285 286 //Extract the RosterEntry at this index and inspect the Comment and 287 //Decoder Comment fields to change any \n characters to <?p?> processor 288 //directives so they can be stored in the xml file and converted 289 //back when the file is read. 290 NceConsistRosterEntry r = _list.get(i); 291 String tempComment = r.getComment(); 292 StringBuilder buf = new StringBuilder(); 293 294 //transfer tempComment to xmlComment one character at a time, except 295 //when \n is found. In that case, insert <?p?> 296 for (int k = 0; k < tempComment.length(); k++) { 297 if (tempComment.startsWith("\n", k)) { 298 buf.append("<?p?>"); 299 } else { 300 buf.append(tempComment.charAt(k)); 301 } 302 } 303 r.setComment(buf.toString()); 304 } 305 // All Comments and Decoder Comment line feeds have been changed to processor directives 306 307 // add top-level elements 308 Element values; 309 root.addContent(values = new Element("roster")); 310 // add entries 311 for (int i = 0; i < numEntries(); i++) { 312 values.addContent(_list.get(i).store()); 313 } 314 writeXML(file, doc); 315 316 //Now that the roster has been rewritten in file form we need to 317 //restore the RosterEntry object to its normal \n state for the 318 //Comment and Decoder comment fields, otherwise it can cause problems in 319 //other parts of the program (e.g. in copying a roster) 320 for (int i = 0; i < numEntries(); i++) { 321 NceConsistRosterEntry r = _list.get(i); 322 String xmlComment = r.getComment(); 323 StringBuilder buf = new StringBuilder(); 324 325 for (int k = 0; k < xmlComment.length(); k++) { 326 if (xmlComment.startsWith("<?p?>", k)) { 327 buf.append("\n"); 328 k = k + 4; 329 } else { 330 buf.append(xmlComment.charAt(k)); 331 } 332 } 333 r.setComment(buf.toString()); 334 } 335 336 // done - roster now stored, so can't be dirty 337 setDirty(false); 338 } 339 340 /** 341 * Read the contents of a roster XML file into this object. Note that this 342 * does not clear any existing entries. 343 * @param name file name for consist roster 344 * @throws org.jdom2.JDOMException other errors 345 * @throws java.io.IOException error accessing file 346 */ 347 void readFile(String name) throws org.jdom2.JDOMException, java.io.IOException { 348 // find root 349 Element root = rootFromName(name); 350 if (root == null) { 351 log.debug("ConsistRoster file could not be read"); 352 return; 353 } 354 //if (log.isDebugEnabled()) XmlFile.dumpElement(root); 355 356 // decode type, invoke proper processing routine if a decoder file 357 if (root.getChild("roster") != null) { 358 List<Element> l = root.getChild("roster").getChildren("consist"); 359 if (log.isDebugEnabled()) { 360 log.debug("readFile sees {} children", l.size()); 361 } 362 for (Element element : l) { 363 addEntry(new NceConsistRosterEntry(element)); 364 } 365 366 //Scan the object to check the Comment and Decoder Comment fields for 367 //any <?p?> processor directives and change them to back \n characters 368 for (int i = 0; i < numEntries(); i++) { 369 //Get a RosterEntry object for this index 370 NceConsistRosterEntry r = _list.get(i); 371 372 //Extract the Comment field and create a new string for output 373 String tempComment = r.getComment(); 374 StringBuilder buf = new StringBuilder(); 375 376 //transfer tempComment to xmlComment one character at a time, except 377 //when <?p?> is found. In that case, insert a \n and skip over those 378 //characters in tempComment. 379 for (int k = 0; k < tempComment.length(); k++) { 380 if (tempComment.startsWith("<?p?>", k)) { 381 buf.append("\n"); 382 k = k + 4; 383 } else { 384 buf.append(tempComment.charAt(k)); 385 } 386 } 387 r.setComment(buf.toString()); 388 } 389 390 } else { 391 log.error("Unrecognized ConsistRoster file contents in file: {}", name); // NOI18N 392 } 393 } 394 395 private boolean dirty = false; 396 397 void setDirty(boolean b) { 398 dirty = b; 399 } 400 401 boolean isDirty() { 402 return dirty; 403 } 404 405 public void dispose() { 406 log.debug("dispose"); 407 if (dirty) { 408 log.error("Dispose invoked on dirty ConsistRoster"); 409 } 410 } 411 412 /** 413 * Store the roster in the default place, including making a backup if 414 * needed 415 */ 416 public void writeRosterFile() { 417 makeBackupFile(defaultNceConsistRosterFilename()); 418 try { 419 writeFile(defaultNceConsistRosterFilename()); 420 } catch (IOException e) { 421 log.error("Exception while writing the new ConsistRoster file, may not be complete: {}", e.getMessage()); 422 } 423 } 424 425 /** 426 * update the in-memory Roster to be consistent with the current roster 427 * file. This removes the existing roster entries! 428 */ 429 public void reloadRosterFile() { 430 // clear existing 431 _list.clear(); 432 // and read new 433 try { 434 readFile(defaultNceConsistRosterFilename()); 435 } catch (IOException | JDOMException e) { 436 log.error("Exception during ConsistRoster reading: {}", e.getMessage()); // NOI18N 437 } 438 } 439 440 /** 441 * Return the filename String for the default ConsistRoster file, including 442 * location. 443 * @return consist roster file name 444 */ 445 public static String defaultNceConsistRosterFilename() { 446 return Roster.getDefault().getRosterLocation() + nceConsistRosterFileName; 447 } 448 449 public static void setNceConsistRosterFileName(String name) { 450 nceConsistRosterFileName = name; 451 } 452 private static String nceConsistRosterFileName = "ConsistRoster.xml"; 453 454 // since we can't do a "super(this)" in the ctor to inherit from PropertyChangeSupport, we'll 455 // reflect to it. 456 // Note that dispose() doesn't act on these. Its not clear whether it should... 457 java.beans.PropertyChangeSupport pcs = new java.beans.PropertyChangeSupport(this); 458 459 public synchronized void addPropertyChangeListener(java.beans.PropertyChangeListener l) { 460 pcs.addPropertyChangeListener(l); 461 } 462 463 protected void firePropertyChange(String p, Object old, Object n) { 464 pcs.firePropertyChange(p, old, n); 465 } 466 467 public synchronized void removePropertyChangeListener(java.beans.PropertyChangeListener l) { 468 pcs.removePropertyChangeListener(l); 469 } 470 471 /** 472 * Notify that the ID of an entry has changed. This doesn't actually change 473 * the ConsistRoster per se, but triggers recreation. 474 * @param r consist roster to recreate due to changes 475 */ 476 public void entryIdChanged(NceConsistRosterEntry r) { 477 log.debug("EntryIdChanged"); 478 479 // order may be wrong! Sort 480 NceConsistRosterEntry[] rarray = new NceConsistRosterEntry[_list.size()]; 481 for (int i = 0; i < rarray.length; i++) { 482 rarray[i] = _list.get(i); 483 } 484 jmri.util.StringUtil.sortUpperCase(rarray); 485 for (int i = 0; i < rarray.length; i++) { 486 _list.set(i, rarray[i]); 487 } 488 489 firePropertyChange("change", null, r); 490 } 491 492 @Override 493 public void initialize() { 494 if (checkFile(defaultNceConsistRosterFilename())) { 495 try { 496 readFile(defaultNceConsistRosterFilename()); 497 } catch (IOException | JDOMException e) { 498 log.error("Exception during ConsistRoster reading: {}", e.getMessage()); 499 } 500 } 501 } 502 503 // initialize logging 504 private final static Logger log = LoggerFactory.getLogger(NceConsistRoster.class); 505 506}