001package jmri.jmrit.decoderdefn; 002 003import java.awt.GraphicsEnvironment; 004import java.io.File; 005import java.io.IOException; 006import java.io.FileNotFoundException; 007import java.net.URL; 008import java.util.ArrayList; 009import java.util.Arrays; 010import java.util.Collections; 011import java.util.HashMap; 012import java.util.List; 013import java.util.Set; 014import javax.annotation.Nonnull; 015import javax.swing.JComboBox; 016import javax.swing.JDialog; 017import javax.swing.JProgressBar; 018import javax.swing.JOptionPane; 019import jmri.InstanceInitializer; 020import jmri.InstanceManager; 021import jmri.implementation.AbstractInstanceInitializer; 022import jmri.jmrit.XmlFile; 023import jmri.util.FileUtil; 024import jmri.util.ThreadingUtil; 025import org.jdom2.Attribute; 026import org.jdom2.Comment; 027import org.jdom2.Document; 028import org.jdom2.Element; 029import org.jdom2.JDOMException; 030import org.jdom2.ProcessingInstruction; 031import org.openide.util.lookup.ServiceProvider; 032import org.slf4j.Logger; 033import org.slf4j.LoggerFactory; 034 035/** 036 * DecoderIndex represents the decoderIndex.xml (decoder types) and 037 * nmra_mfg_list.xml (Manufacturer ID list) files in memory. 038 * <p> 039 * This allows a program to navigate to various decoder descriptions without 040 * having to manipulate files. 041 * <p> 042 * This class doesn't provide tools for defining the index; that's done 043 * by {@link jmri.jmrit.decoderdefn.DecoderIndexCreateAction}, which 044 * rebuilds it from the decoder files. 045 * <p> 046 * Multiple DecoderIndexFile objects don't make sense, so we use an "instance" 047 * member to navigate to a single one. 048 * <p> 049 * Previous to JMRI 4.19.1, the manufacturer information was kept in the 050 * decoderIndex.xml file. Starting with that version it's in the separate 051 * nmra_mfg_list.xml file, but still written to decoderIndex.xml when 052 * one is created. 053 * 054 * @author Bob Jacobsen Copyright (C) 2001, 2019, 2025 055 * @see jmri.jmrit.decoderdefn.DecoderIndexCreateAction 056 */ 057public class DecoderIndexFile extends XmlFile { 058 059 public static final String MANUFACTURER = "manufacturer"; 060 public static final String MFG_ID = "mfgID"; 061 public static final String DECODER_INDEX = "decoderIndex"; 062 public static final String VERSION = "version"; 063 public static final String LOW_VERSION_ID = "lowVersionID"; 064 public static final String HIGH_VERSION_ID = "highVersionID"; 065 // fill in abstract members 066 protected List<DecoderFile> decoderList = new ArrayList<>(); 067 068 public int numDecoders() { 069 return decoderList.size(); 070 } 071 072 int fileVersion = -1; 073 074 // map mfg ID numbers from & to mfg names 075 protected HashMap<String, String> _mfgIdFromNameHash = new HashMap<>(); 076 protected HashMap<String, String> _mfgNameFromIdHash = new HashMap<>(); 077 078 protected ArrayList<String> mMfgNameList = new ArrayList<>(); 079 080 public List<String> getMfgNameList() { 081 return mMfgNameList; 082 } 083 084 public String mfgIdFromName(String name) { 085 return _mfgIdFromNameHash.get(name); 086 } 087 088 /** 089 * 090 * @param idNum String containing the manufacturer's NMRA 091 * manufacturer ID number 092 * @return String containing the "friendly" name of the manufacturer 093 */ 094 095 public String mfgNameFromID(String idNum) { 096 return _mfgNameFromIdHash.get(idNum); 097 } 098 099 /** 100 * Get a List of decoders matching (only) the programming mode. 101 * 102 * @param progMode decoder programming mode as defined in a decoder's programming element 103 * @return a list, possibly empty, of matching decoders 104 */ 105 @Nonnull 106 public List<DecoderFile> matchingDecoderList(String progMode) { 107 return (matchingDecoderList(null, null, null, null, null, 108 null, null, null, null, progMode)); 109 } 110 111 /** 112 * Get a List of decoders matching basic characteristics. 113 * 114 * @param mfg decoder manufacturer 115 * @param family decoder family 116 * @param decoderMfgID NMRA decoder manufacturer ID 117 * @param decoderVersionID decoder version ID 118 * @param decoderProductID decoder product ID 119 * @param model decoder model 120 * @return a list, possibly empty, of matching decoders 121 */ 122 @Nonnull 123 public List<DecoderFile> matchingDecoderList(String mfg, String family, 124 String decoderMfgID, String decoderVersionID, String decoderProductID, 125 String model) { 126 return (matchingDecoderList(mfg, family, decoderMfgID, decoderVersionID, decoderProductID, model, 127 null, null, null, null)); 128 } 129 130 /** 131 * Get a List of decoders matching basic characteristics + product ID etc. 132 * 133 * @param mfg decoder manufacturer 134 * @param family decoder family 135 * @param decoderMfgID NMRA decoder manufacturer ID 136 * @param decoderVersionID decoder version ID 137 * @param decoderProductID decoder product ID 138 * @param model decoder model 139 * @param developerID developer ID number 140 * @param manufacturerID manufacturerID number 141 * @param productID productID number 142 * @return a list, possibly empty, of matching decoders 143 */ 144 @Nonnull 145 public List<DecoderFile> matchingDecoderList(String mfg, String family, 146 String decoderMfgID, String decoderVersionID, 147 String decoderProductID, String model, String developerID, String manufacturerID, String productID) { 148 return (matchingDecoderList(mfg, family, decoderMfgID, decoderVersionID, decoderProductID, model, 149 null, null, null, null)); 150 } 151 152 /** 153 * Get a List of decoders matching basic characteristics + product ID etc. + programming mode. 154 * 155 * @param mfg decoder manufacturer 156 * @param family decoder family 157 * @param decoderMfgID NMRA decoder manufacturer ID 158 * @param decoderVersionID decoder version ID 159 * @param decoderProductID decoder product ID 160 * @param model decoder model 161 * @param developerID developer ID number 162 * @param manufacturerID manufacturerID number 163 * @param productID productID number 164 * @param progMode programming mode as defined in a decoder's programming element 165 * @return a list, possibly empty, of matching decoders 166 */ 167 @Nonnull 168 public List<DecoderFile> matchingDecoderList(String mfg, String family, 169 String decoderMfgID, String decoderVersionID, 170 String decoderProductID, String model, String developerID, 171 String manufacturerID, String productID, String progMode) { 172 List<DecoderFile> l = new ArrayList<>(); 173 for (int i = 0; i < numDecoders(); i++) { 174 if (checkEntry(i, mfg, family, decoderMfgID, decoderVersionID, decoderProductID, model, developerID, 175 manufacturerID, productID, progMode)) { 176 l.add(decoderList.get(i)); 177 } 178 } 179 return l; 180 } 181 182 /** 183 * Get a JComboBox representing the choices that match basic characteristics. 184 * 185 * @param mfg decoder manufacturer 186 * @param family decoder family 187 * @param decoderMfgID NMRA decoder manufacturer ID 188 * @param decoderVersionID decoder version ID 189 * @param decoderProductID decoder product ID 190 * @param model decoder model 191 * @return a combo box populated with matching decoders 192 */ 193 public JComboBox<String> matchingComboBox(String mfg, String family, String decoderMfgID, String decoderVersionID, 194 String decoderProductID, String model) { 195 List<DecoderFile> l = matchingDecoderList(mfg, family, decoderMfgID, decoderVersionID, decoderProductID, model); 196 return jComboBoxFromList(l); 197 } 198 199 /** 200 * Get a new JComboBox made with the titles from a list of DecoderFile. 201 * 202 * @param l list of decoders 203 * @return a combo box populated with the list 204 */ 205 public static JComboBox<String> jComboBoxFromList(List<DecoderFile> l) { 206 return new JComboBox<>(jComboBoxModelFromList(l)); 207 } 208 209 /** 210 * Get a new ComboBoxModel made with the titles from a list of DecoderFile. 211 * entries. 212 * 213 * @param l list of decoders 214 * @return a combo box model populated with the list 215 */ 216 public static javax.swing.ComboBoxModel<String> jComboBoxModelFromList(List<DecoderFile> l) { 217 javax.swing.DefaultComboBoxModel<String> b = new javax.swing.DefaultComboBoxModel<>(); 218 for (DecoderFile r : l) { 219 b.addElement(r.titleString()); 220 } 221 return b; 222 } 223 224 /** 225 * Get a DecoderFile from a "title" string, typically a selection in a 226 * matching ComboBox. 227 * 228 * @param title the decoder title 229 * @return the decoder file 230 */ 231 public DecoderFile fileFromTitle(String title) { 232 for (int i = numDecoders() - 1; i >= 0; i--) { 233 DecoderFile r = decoderList.get(i); 234 if (r.titleString().equals(title)) { 235 return r; 236 } 237 } 238 return null; 239 } 240 241 /** 242 * Check if an entry consistent with specific properties. A null String 243 * entry always matches. Strings are used for convenience in GUI building. 244 * Don't bother asking about the model number... 245 * 246 * @param i index of entry 247 * @param mfgName decoder manufacturer 248 * @param family decoder family 249 * @param mfgID NMRA decoder manufacturer ID 250 * @param decoderVersionID decoder version ID 251 * @param decoderProductID decoder product ID 252 * @param model decoder model 253 * @param developerID developer ID number 254 * @param manufacturerID manufacturer ID number 255 * @param productID product ID number 256 * @param progMode programming mode as defined in a decoder's programming element 257 * @return true if entry at i matches the other parameters; false otherwise 258 */ 259 public boolean checkEntry(int i, String mfgName, String family, String mfgID, 260 String decoderVersionID, String decoderProductID, String model, 261 String developerID, String manufacturerID, String productID, String progMode) { 262 DecoderFile r = decoderList.get(i); 263 if (mfgName != null && !mfgName.equals(r.getMfg())) { 264 return false; 265 } 266 if (family != null && !family.equals(r.getFamily())) { 267 return false; 268 } 269 if (mfgID != null && !mfgID.equals(r.getMfgID())) { 270 return false; 271 } 272 if (model != null && !model.equals(r.getModel())) { 273 return false; 274 } 275 // check version ID - no match if a range specified and out of range 276 if (decoderVersionID != null) { 277 int versionID = Integer.parseInt(decoderVersionID); 278 if (!r.isVersion(versionID)) { 279 return false; 280 } 281 } 282 283 if (decoderProductID != null && !checkInCommaDelimString(decoderProductID, r.getProductID())) { 284 return false; 285 } 286 287 if (developerID != null) { 288 // must have a (LocoNet SV2) developerID value that matches to consider this entry a match 289 if (!developerID.equals(r.getDeveloperID())) { 290 // didn't match the getDeveloperID() value, so check the model developerID value 291 if (r.getModelElement().getAttribute("developerID") == null) { 292 // no model developerID value, so not a match! 293 return false; 294 } 295 if (!("," + r.getModelElement().getAttribute("developerID").getValue() + ",").contains("," + developerID + ",")) { 296 return false; 297 } 298 } 299 log.debug("developerID match"); 300 } 301 302 if (manufacturerID != null) { 303 log.debug("checking manufactureriD {}, mfgID {}, modelElement[manufacturerID] {}", 304 manufacturerID, r._mfgID, r.getModelElement().getAttribute("manufacturerID")); 305 // must have a manufacturerID value that matches to consider this entry a match 306 307 if ((r._mfgID == null) || (manufacturerID.compareTo(r._mfgID) != 0)) { 308 // ID number from manufacturer name isn't identical; try another way 309 if (!manufacturerID.equals(r.getManufacturerID())) { 310 // no match to the manufacturerID attribute at the (family?) level, so try model level 311 Attribute a = r.getModelElement().getAttribute("manufacturerID"); 312 if ((a == null) || (a.getValue() == null) || 313 (manufacturerID.compareTo(a.getValue())!=0)) { 314 // no model manufacturerID value, or model manufacturerID 315 // value does not match so this decoder is not a match! 316 return false; 317 } 318 } 319 } 320 log.debug("manufacturerID match"); 321 } 322 323 if (productID != null) { 324 // must have a (LocoNet SV2 or the Uhlenbrock LNCV protocol) productID value that matches to consider this entry a match 325 if (!productID.equals(r.getProductID())) { 326 // didn't match the getProductID() value, so check the model productID value 327 if (r.getModelElement().getAttribute("productID") == null) { 328 // no model productID value, so not a match! 329 return false; 330 } 331 if (!("," + r.getModelElement().getAttribute("productID").getValue() + ",").contains("," + productID + ",")) { 332 return false; 333 } 334 } 335 log.debug("productID match"); 336 } 337 338 if (progMode != null) { 339 // must have a progMode value that matches to consider this entry a match 340 return r.isProgrammingMode(progMode); // simplified logic while this is the last if in method 341 } 342 343 return true; 344 } 345 346 /** 347 * Replace the managed instance with a new instance. 348 */ 349 public static synchronized void resetInstance() { 350 InstanceManager.getDefault().clear(DecoderIndexFile.class); 351 } 352 353 /** 354 * Check whether the user's version of the decoder index file needs to be 355 * updated; if it does, then forces the update. 356 * 357 * @return true is the index should be reloaded because it was updated 358 * @throws JDOMException if unable to parse decoder index 359 * @throws IOException if unable to read decoder index 360 */ 361 public static boolean updateIndexIfNeeded() throws JDOMException, IOException { 362 switch (FileUtil.findFiles(defaultDecoderIndexFilename(), ".").size()) { 363 case 0: 364 log.debug("creating decoder index"); 365 forceCreationOfNewIndex(); 366 return true; // no index exists, so create one 367 case 1: 368 return false; // only one index, so nothing to compare 369 default: 370 // multiple indexes, so continue with more specific checks 371 break; 372 } 373 374 // get version from master index; if not found, give up 375 String masterVersion = null; 376 DecoderIndexFile masterXmlFile = new DecoderIndexFile(); 377 URL masterFile = FileUtil.findURL("xml/" + defaultDecoderIndexFilename(), FileUtil.Location.INSTALLED); 378 if (masterFile == null) { 379 return false; 380 } 381 log.debug("checking for master file at {}", masterFile); 382 Element masterRoot = masterXmlFile.rootFromURL(masterFile); 383 if (masterRoot.getChild(DECODER_INDEX) != null) { 384 if (masterRoot.getChild(DECODER_INDEX).getAttribute(VERSION) != null) { 385 masterVersion = masterRoot.getChild(DECODER_INDEX).getAttribute(VERSION).getValue(); 386 } 387 log.debug("master version found, is {}", masterVersion); 388 } else { 389 return false; 390 } 391 392 // get from user index. Unless they are equal, force an update. 393 // note we find this file via the search path; if not exists, so that 394 // the master is found, we still do the right thing (nothing). 395 String userVersion = null; 396 DecoderIndexFile userXmlFile = new DecoderIndexFile(); 397 log.debug("checking for user file at {}", defaultDecoderIndexFilename()); 398 Element userRoot = userXmlFile.rootFromName(defaultDecoderIndexFilename()); 399 if (userRoot.getChild(DECODER_INDEX) != null) { 400 if (userRoot.getChild(DECODER_INDEX).getAttribute(VERSION) != null) { 401 userVersion = userRoot.getChild(DECODER_INDEX).getAttribute(VERSION).getValue(); 402 } 403 log.debug("user version found, is {}", userVersion); 404 } 405 if (masterVersion != null && masterVersion.equals(userVersion)) { 406 return false; 407 } 408 409 // force the update, with the version number located earlier is available 410 log.debug("forcing update of decoder index due to {} and {}", masterVersion, userVersion); 411 forceCreationOfNewIndex(); 412 // and force it to be used 413 return true; 414 } 415 416 /** 417 * Force creation of a new user index without incrementing version 418 */ 419 public static void forceCreationOfNewIndex() { 420 forceCreationOfNewIndex(false); 421 } 422 423 /** 424 * Force creation of a new user index. 425 * 426 * @param increment true to increment the version of the decoder index 427 */ 428 public static void forceCreationOfNewIndex(boolean increment) { 429 log.info("update decoder index"); 430 // make sure we're using only the default manufacturer info 431 // to keep from propagating wrong, old stuff 432 File oldfile = new File(FileUtil.getUserFilesPath() + DECODER_INDEX_FILE_NAME); 433 if (oldfile.exists()) { 434 log.debug("remove existing user decoderIndex.xml file"); 435 if (!oldfile.delete()) // delete file, check for success 436 { 437 log.error("Failed to delete old index file"); 438 } 439 // force read from distributed file on next access 440 resetInstance(); 441 } 442 443 // create an array of file names from decoders dir in preferences, count entries 444 ArrayList<String> al = new ArrayList<>(); 445 FileUtil.createDirectory(FileUtil.getUserFilesPath() + DecoderFile.fileLocation); 446 File fp = new File(FileUtil.getUserFilesPath() + DecoderFile.fileLocation); 447 448 if (fp.exists()) { 449 String[] list = fp.list(); 450 if (list !=null) { 451 for (String sp : list) { 452 if (sp.endsWith(".xml") || sp.endsWith(".XML")) { 453 al.add(sp); 454 } 455 } 456 } 457 } else { 458 log.debug("{}decoders was missing, though tried to create it", FileUtil.getUserFilesPath()); 459 } 460 // create an array of file names from xml/decoders, count entries 461 String[] fileList = (new File(XmlFile.xmlDir() + DecoderFile.fileLocation)).list(); 462 if (fileList != null) { 463 for (String sx : fileList ) { 464 if (sx.endsWith(".xml") || sx.endsWith(".XML")) { 465 // Valid name. Does it exist in preferences xml/decoders? 466 if (!al.contains(sx)) { 467 // no, include it! 468 al.add(sx); 469 } 470 } 471 } 472 } else { 473 log.error("Could not access decoder definition directory {}{}", XmlFile.xmlDir(), DecoderFile.fileLocation); 474 } 475 // copy the decoder entries to the final array 476 String[] sbox = al.toArray(new String[0]); 477 478 //the resulting array is now sorted on file-name to make it easier 479 // for humans to read 480 Arrays.sort(sbox); 481 482 // create a new decoderIndex 483 DecoderIndexFile index = new DecoderIndexFile(); 484 485 // For user operations the existing version is used, so that a new master file 486 // with a larger one will force an update 487 if (increment) { 488 index.fileVersion = InstanceManager.getDefault(DecoderIndexFile.class).fileVersion + 2; 489 } else { 490 index.fileVersion = InstanceManager.getDefault(DecoderIndexFile.class).fileVersion; 491 } 492 493 // If not many entries, or headless, just recreate index without updating the UI 494 // Also block if not on the GUI (event dispatch) thread 495 if (sbox.length < 30 || GraphicsEnvironment.isHeadless() || !ThreadingUtil.isGUIThread()) { 496 try { 497 index.writeFile(DECODER_INDEX_FILE_NAME, 498 InstanceManager.getDefault(DecoderIndexFile.class), sbox, null, null); 499 } catch (IOException ex) { 500 log.error("Error writing new decoder index file: {}", ex.getMessage()); 501 } 502 return; 503 } 504 505 // Create a dialog with a progress bar and a cancel button 506 String message = Bundle.getMessage("DecoderProgressMessage", "..."); // NOI18N 507 String title = Bundle.getMessage("DecoderProgressMessage", ""); 508 String cancel = Bundle.getMessage("ButtonCancel"); // NOI18N 509 // HACK: add long blank space to message to make dialog wider. 510 JOptionPane pane = new JOptionPane(message + " \t", 511 JOptionPane.PLAIN_MESSAGE, 512 JOptionPane.OK_CANCEL_OPTION, 513 null, 514 new String[]{cancel}); 515 JProgressBar pb = new JProgressBar(0, sbox.length); 516 pb.setValue(0); 517 pane.add(pb, 1); 518 JDialog dialog = pane.createDialog(null, title); 519 520 ThreadingUtil.newThread(() -> { 521 try { 522 index.writeFile(DECODER_INDEX_FILE_NAME, 523 InstanceManager.getDefault(DecoderIndexFile.class), sbox, pane, pb); 524 // catch all exceptions, so progress dialog will close 525 } catch (IOException e) { 526 // TODO: show message in progress dialog? 527 log.error("Error writing new decoder index file: {}", e.getMessage()); 528 } 529 dialog.setVisible(false); 530 dialog.dispose(); 531 }, "decoderIndexer").start(); 532 533 // improve visibility if any always on top frames present 534 dialog.setAlwaysOnTop(true); 535 dialog.toFront(); 536 // this will block until the thread completes, either by 537 // finishing or by being cancelled 538 dialog.setVisible(true); 539 } 540 541 /** 542 * Read the contents of a decoderIndex XML file into this object. Note that 543 * this does not clear any existing entries; reset the instance to do that. 544 * 545 * @param name the name of the decoder index file 546 * @throws JDOMException if unable to parse to decoder index file 547 * @throws IOException if unable to read decoder index file 548 */ 549 void readFile(String name) throws JDOMException, IOException { 550 log.debug("readFile {}", name); 551 552 // read file, find root 553 Element root = rootFromName(name); 554 555 // decode type, invoke proper processing routine if a decoder file 556 if (root.getChild(DECODER_INDEX) != null) { 557 if (root.getChild(DECODER_INDEX).getAttribute(VERSION) != null) { 558 fileVersion = Integer.parseInt(root.getChild(DECODER_INDEX) 559 .getAttribute(VERSION) 560 .getValue() 561 ); 562 } 563 log.debug("found fileVersion of {}", fileVersion); 564 readMfgSection(); 565 readFamilySection(root.getChild(DECODER_INDEX)); 566 } else { 567 log.error("Unrecognized decoderIndex file contents in file: {}", name); 568 } 569 } 570 571 void readMfgSection() throws JDOMException, IOException { 572 // always reads the NMRA manufacturer file distributed with JMRI 573 Element mfgList = rootFromName("nmra_mfg_list.xml"); 574 575 if (mfgList != null) { 576 577 Attribute a; 578 a = mfgList.getAttribute("nmraListDate"); 579 if (a != null) { 580 nmraListDate = a.getValue(); 581 } 582 a = mfgList.getAttribute("updated"); 583 if (a != null) { 584 updated = a.getValue(); 585 } 586 a = mfgList.getAttribute("lastadd"); 587 if (a != null) { 588 lastAdd = a.getValue(); 589 } 590 591 List<Element> l = mfgList.getChildren(MANUFACTURER); 592 log.debug("readMfgSection sees {} children",l.size()); 593 for (Element el : l) { 594 // handle each entry 595 String mfg = el.getAttribute("mfg").getValue(); 596 mMfgNameList.add(mfg); 597 Attribute attr = el.getAttribute(MFG_ID); 598 if (attr != null) { 599 _mfgIdFromNameHash.put(mfg, attr.getValue()); 600 _mfgNameFromIdHash.put(attr.getValue(), mfg); 601 } 602 } 603 } else { 604 log.debug("no mfgList found"); 605 } 606 } 607 608 void readFamilySection(Element decoderIndex) { 609 Element familyList = decoderIndex.getChild("familyList"); 610 if (familyList != null) { 611 612 List<Element> l = familyList.getChildren("family"); 613 log.trace("readFamilySection sees {} children", l.size()); 614 for (Element el : l) { 615 // handle each entry 616 readFamily(el); 617 } 618 } else { 619 log.debug("no familyList found in decoderIndexFile"); 620 } 621 } 622 623 void readFamily(Element family) { 624 Attribute attr; 625 String filename = family.getAttribute("file").getValue(); 626 String parentLowVersID = ((attr = family.getAttribute(LOW_VERSION_ID)) != null ? attr.getValue() : null); 627 String parentHighVersID = ((attr = family.getAttribute(HIGH_VERSION_ID)) != null ? attr.getValue() : null); 628 String ParentReplacementFamilyName = ((attr = family.getAttribute("replacementFamily")) != null ? attr.getValue() : null); 629 String familyName = ((attr = family.getAttribute("name")) != null ? attr.getValue() : null); 630 String mfg = ((attr = family.getAttribute("mfg")) != null ? attr.getValue() : null); 631 String developerID = ((attr = family.getAttribute("developerID")) != null ? attr.getValue() : null); 632 String manufacturerID = ((attr = family.getAttribute("manufacturerID")) != null ? attr.getValue() : null); 633 String productID = ((attr = family.getAttribute("productID")) != null ? attr.getValue() : null); 634 String mfgID = null; 635 if (mfg != null) { 636 mfgID = mfgIdFromName(mfg); 637 } else { 638 log.error("Did not find required mfg attribute, may not find proper manufacturer"); 639 } 640 641 // extract <programming> modes of a family's parent <decoder> element 642 String modes = ((attr = family.getAttribute("modes")) != null ? attr.getValue() : null); 643 644 List<Element> l = family.getChildren("model"); 645 log.trace("readFamily sees {} children", l.size()); 646 Element modelElement; 647 if (l.isEmpty()) { 648 log.error("Did not find at least one model in the {} family", familyName); 649 modelElement = null; 650 } else { 651 modelElement = l.get(0); 652 } 653 654 // Record the family as a specific model, which allows you to select the 655 // family as a possible thing to program 656 DecoderFile vFamilyDecoderFile 657 = new DecoderFile(mfg, mfgID, familyName, 658 parentLowVersID, parentHighVersID, 659 familyName, 660 filename, 661 (developerID != null) ? developerID : "-1", 662 (manufacturerID != null) ? manufacturerID : "-1", 663 (productID != null) ? productID : "-1", 664 -1, -1, modelElement, 665 ParentReplacementFamilyName, ParentReplacementFamilyName, 666 modes); // numFns, numOuts, XML element equal 667 // add family model as the first decoder 668 decoderList.add(vFamilyDecoderFile); 669 670 // record each of the decoders 671 for (Element decoder : l) { 672 // handle each entry by creating a DecoderFile object containing all it knows 673 String loVersID = ((attr = decoder.getAttribute(LOW_VERSION_ID)) != null ? attr.getValue() : parentLowVersID); 674 String hiVersID = ((attr = decoder.getAttribute(HIGH_VERSION_ID)) != null ? attr.getValue() : parentHighVersID); 675 String replacementModelName = ((attr = decoder.getAttribute("replacementModel")) != null ? attr.getValue() : null); 676 String replacementFamilyName = ((attr = decoder.getAttribute("replacementFamily")) != null ? attr.getValue() : ParentReplacementFamilyName); 677 int numFns = ((attr = decoder.getAttribute("numFns")) != null ? Integer.parseInt(attr.getValue()) : -1); 678 int numOuts = ((attr = decoder.getAttribute("numOuts")) != null ? Integer.parseInt(attr.getValue()) : -1); 679 String devId = ((attr = decoder.getAttribute("developerID")) != null ? attr.getValue() : "-1"); 680 String manufId = ((attr = decoder.getAttribute("manufacturerID")) != null ? attr.getValue() : "-1"); 681 String prodId = ((attr = decoder.getAttribute("productID")) != null ? attr.getValue() : "-1"); 682 683 DecoderFile df = new DecoderFile(mfg, mfgID, 684 ((attr = decoder.getAttribute("model")) != null ? attr.getValue() : null), 685 loVersID, hiVersID, familyName, filename, devId, manufId, prodId, numFns, numOuts, decoder, 686 replacementModelName, replacementFamilyName, modes); 687 // and store it 688 decoderList.add(df); 689 // if there are additional version numbers defined, handle them too 690 List<Element> vcodes = decoder.getChildren("versionCV"); 691 for (Element vcv : vcodes) { 692 // for each versionCV element 693 String vLoVersID = ((attr = vcv.getAttribute(LOW_VERSION_ID)) != null ? attr.getValue() : loVersID); 694 String vHiVersID = ((attr = vcv.getAttribute(HIGH_VERSION_ID)) != null ? attr.getValue() : hiVersID); 695 df.setVersionRange(vLoVersID, vHiVersID); 696 } 697 } 698 } 699 700 /** 701 * Check if target string is in a comma-delimited string 702 * <p> 703 * Example: 704 * findString = "47" 705 * inString = "1,4,53,97" 706 * return value is 'false' 707 * <p> 708 * Example: 709 * findString = "47" 710 * inString = "1,31,47,51" 711 * return value is 'true' 712 * <p> 713 * Example: 714 * findString = "47" 715 * inString = "47" 716 * return value is true 717 * 718 * @param findString string to find 719 * @param inString comma-delimited string of sub-strings 720 * @return true if target string is found as sub-string within comma- 721 * delimited string 722 */ 723 public boolean checkInCommaDelimString(String findString, String inString) { 724 String bracketedFindString = ","+findString+","; 725 String bracketedInString = ","+inString+","; 726 return bracketedInString.contains(bracketedFindString); 727 } 728 729 /** 730 * Build and write the decoder index file, based on a set of decoder files. 731 * <p> 732 * This creates the full DOM object for the decoder index based on reading the 733 * supplied decoder xml files. It then saves the decoder index out to a new file. 734 * 735 * @param name name of the new index file 736 * @param oldIndex old decoder index file 737 * @param files array of files to read for new index 738 * @param pane optional JOptionPane to check for cancellation 739 * @param pb optional JProgressBar to update during operations 740 * @throws IOException for errors writing the decoder index file 741 */ 742 public void writeFile(String name, DecoderIndexFile oldIndex, 743 String[] files, JOptionPane pane, JProgressBar pb) throws IOException { 744 log.debug("writeFile {}",name); 745 746 // This is taken in large part from "Java and XML" page 368 747 File file = new File(FileUtil.getUserFilesPath() + name); 748 749 // create root element and document 750 Element root = new Element("decoderIndex-config"); 751 root.setAttribute("noNamespaceSchemaLocation", 752 "http://jmri.org/xml/schema/decoder-4-15-2.xsd", 753 org.jdom2.Namespace.getNamespace("xsi", 754 "http://www.w3.org/2001/XMLSchema-instance")); 755 756 Document doc = newDocument(root); 757 758 // add XSLT processing instruction 759 // <?xml-stylesheet type="text/xsl" href="XSLT/DecoderID.xsl"?> 760 java.util.Map<String, String> m = new java.util.HashMap<>(); 761 m.put("type", "text/xsl"); 762 m.put("href", xsltLocation + "DecoderID.xsl"); 763 ProcessingInstruction p = new ProcessingInstruction("xml-stylesheet", m); 764 doc.addContent(0, p); 765 766 // add top-level elements 767 Element index; 768 root.addContent(index = new Element(DECODER_INDEX)); 769 index.setAttribute(VERSION, Integer.toString(fileVersion)); 770 log.debug("version written to file as {}", fileVersion); 771 772 // add mfg list from existing DecoderIndexFile item 773 Element mfgList = new Element("mfgList"); 774 // copy dates from original mfgList element 775 if (oldIndex.nmraListDate != null) { 776 mfgList.setAttribute("nmraListDate", oldIndex.nmraListDate); 777 } 778 if (oldIndex.updated != null) { 779 mfgList.setAttribute("updated", oldIndex.updated); 780 } 781 if (oldIndex.lastAdd != null) { 782 mfgList.setAttribute("lastadd", oldIndex.lastAdd); 783 } 784 785 // We treat "NMRA" special... 786 Element mfg = new Element(MANUFACTURER); 787 mfg.setAttribute("mfg", "NMRA"); 788 mfg.setAttribute(MFG_ID, "999"); 789 mfgList.addContent(mfg); 790 // start working on the rest of the entries 791 List<String> keys = new ArrayList<>(oldIndex._mfgIdFromNameHash.keySet()); 792 Collections.sort(keys); 793 for (Object item : keys) { 794 String mfgName = (String) item; 795 if (!mfgName.equals("NMRA")) { 796 mfg = new Element(MANUFACTURER); 797 mfg.setAttribute("mfg", mfgName); 798 mfg.setAttribute(MFG_ID, oldIndex._mfgIdFromNameHash.get(mfgName)); 799 mfgList.addContent(mfg); 800 } 801 } 802 803 // add family list by scanning files 804 Element familyList = new Element("familyList"); 805 int fileNum = 0; 806 for (String fileName : files) { 807 // update progress monitor, if passed in 808 if (pb != null) { 809 pb.setValue(fileNum++); 810 } 811 if (pane != null && pane.getValue() != JOptionPane.UNINITIALIZED_VALUE) { 812 log.info("Decoder index recreation cancelled"); 813 return; 814 } 815 DecoderFile d = new DecoderFile(); 816 try { 817 // get <family> element and add the file name 818 Element droot = d.rootFromName(DecoderFile.fileLocation + fileName); 819 Element family = droot.getChild("decoder").getChild("family").clone(); 820 // get decoder element's child programming and copy the mode children 821 Element prog = droot.getChild("decoder").getChild("programming"); 822 if (prog != null) { 823 List<Element> modes = prog.getChildren("mode"); 824 if (modes != null) { 825 StringBuilder supportedModes = new StringBuilder(); 826 for (Element md : modes) { // typically only 1 mode element in a definition 827 String modeName = md.getText(); // example: LOCONETLNCVMODE 828 if (supportedModes.length() > 0) supportedModes.append(","); 829 supportedModes.append(modeName); 830 } 831 if (supportedModes.length() > 0) { 832 family.setAttribute("modes", supportedModes.toString()); 833 } 834 } 835 } 836 family.setAttribute("file", fileName); 837 838 // drop the decoder implementation content 839 // comment is kept, so it displays 840 // don't remove "outputs" due to use by ESU function map pane 841 // family.removeChildren("output"); 842 // family.removeChildren("functionlabels"); 843 844 // and drop content of model elements 845 for (Element element : family.getChildren()) { // model elements 846 element.removeAttribute("maxInputVolts"); 847 element.removeAttribute("maxMotorCurrent"); 848 element.removeAttribute("maxTotalCurrent"); 849 element.removeAttribute("formFactor"); 850 element.removeAttribute("connector"); 851 // comment is kept so it displays 852 element.removeAttribute("nmraWarrant"); 853 element.removeAttribute("nmraWarrantStart"); 854 855 // element.removeContent(); 856 element.removeChildren("size"); 857 858 //element.removeChildren("functionlabels"); 859 860 // don't remove "output" due to use by ESU function map pane 861 for (Element output : element.getChildren()) { 862 output.removeAttribute("connection"); 863 output.removeAttribute("maxcurrent"); 864 output.removeChildren("label"); 865 } 866 } 867 868 // and store to output 869 familyList.addContent(family); 870 } catch (JDOMException exj) { 871 log.error("could not parse {}: {}", fileName, exj.getMessage()); 872 } catch (FileNotFoundException exj) { 873 log.error("could not read {}: {}", fileName, exj.getMessage()); 874 } catch (IOException exj) { 875 log.error("other exception while dealing with {}: {}", fileName, exj.getMessage()); 876 } 877 } 878 879 index.addContent(new Comment("The manufacturer list is from the nmra_mfg_list.xml file")); 880 index.addContent(mfgList); 881 index.addContent(familyList); 882 883 log.debug("Writing decoderIndex"); 884 try { 885 writeXML(file, doc); 886 } catch (Exception e) { 887 log.error("Error writing file: {}", file, e); 888 } 889 890 // force a read of the new file next time 891 resetInstance(); 892 } 893 894 String nmraListDate = null; 895 String updated = null; 896 String lastAdd = null; 897 898 /** 899 * Get the filename for the default decoder index file, including location. 900 * This is here to allow easy override in tests. 901 * 902 * @return the complete path to the decoder index 903 */ 904 protected static String defaultDecoderIndexFilename() { 905 return DECODER_INDEX_FILE_NAME; 906 } 907 908 protected static final String DECODER_INDEX_FILE_NAME = "decoderIndex.xml"; 909 private static final Logger log = LoggerFactory.getLogger(DecoderIndexFile.class); 910 911 @ServiceProvider(service = InstanceInitializer.class) 912 public static class Initializer extends AbstractInstanceInitializer { 913 914 @Override 915 @Nonnull 916 public <T> Object getDefault(Class<T> type) { 917 if (type.equals(DecoderIndexFile.class)) { 918 // create and load 919 DecoderIndexFile instance = new DecoderIndexFile(); 920 log.debug("DecoderIndexFile creating instance"); 921 try { 922 instance.readFile(defaultDecoderIndexFilename()); 923 } catch (IOException | JDOMException e) { 924 log.error("Exception during decoder index reading: ", e); 925 } 926 // see if needs to be updated 927 try { 928 if (updateIndexIfNeeded()) { 929 try { 930 instance = new DecoderIndexFile(); 931 instance.readFile(defaultDecoderIndexFilename()); 932 } catch (IOException | JDOMException e) { 933 log.error("Exception during decoder index reload: ", e); 934 } 935 } 936 } catch (IOException | JDOMException e) { 937 log.error("Exception during decoder index update: ", e); 938 } 939 log.debug("DecoderIndexFile returns instance {}", instance); 940 return instance; 941 } 942 return super.getDefault(type); 943 } 944 945 @Override 946 @Nonnull 947 public Set<Class<?>> getInitalizes() { 948 Set<Class<?>> set = super.getInitalizes(); 949 set.add(DecoderIndexFile.class); 950 return set; 951 } 952 } 953 954}