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