001package jmri.jmrit.decoderdefn; 002 003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 004import java.io.File; 005import java.util.ArrayList; 006import java.util.Arrays; 007import java.util.List; 008import java.util.Objects; 009 010import javax.annotation.Nonnull; 011import javax.swing.JLabel; 012 013import jmri.LocoAddress; 014import jmri.Programmer; 015import jmri.jmrit.XmlFile; 016import jmri.jmrit.symbolicprog.ResetTableModel; 017import jmri.jmrit.symbolicprog.ExtraMenuTableModel; 018import jmri.jmrit.symbolicprog.VariableTableModel; 019import org.jdom2.DataConversionException; 020import org.jdom2.Element; 021import org.slf4j.Logger; 022import org.slf4j.LoggerFactory; 023 024/** 025 * Represents and manipulates a decoder definition, both as a file and in 026 * memory. The internal storage is a JDOM tree. 027 * <p> 028 * This object is created by DecoderIndexFile to represent the decoder 029 * identification info _before_ the actual decoder file is read. 030 * 031 * @author Bob Jacobsen Copyright (C) 2001 032 * @author Howard G. Penny Copyright (C) 2005 033 * @see jmri.jmrit.decoderdefn.DecoderIndexFile 034 */ 035public class DecoderFile extends XmlFile { 036 037 public DecoderFile() { 038 } 039 040 /** 041 * Create a mechanism to manipulate a decoder definition from up to 10 parameters. 042 * 043 * @param mfg manufacturer name 044 * @param mfgID manufacturer's NMRA manufacturer number, typically a "CV8" value 045 * @param model decoder model designation 046 * @param lowVersionID decoder version low byte, where applicable 047 * @param highVersionID decoder version high byte, where applicable 048 * @param family decoder family name, where applicable 049 * @param filename filename of decoder XML definition 050 * @param numFns decoder's number of available functions 051 * @param numOuts decoder's number of available function outputs 052 * @param decoder Element containing decoder XML definition 053 */ 054 public DecoderFile(String mfg, String mfgID, String model, String lowVersionID, 055 String highVersionID, String family, String filename, 056 int numFns, int numOuts, Element decoder) { 057 _mfg = mfg; 058 _mfgID = mfgID; 059 _model = model; 060 _family = family; 061 _filename = filename; 062 _numFns = numFns; 063 _numOuts = numOuts; 064 _element = decoder; 065 066 log.trace("Create DecoderFile with Family \"{}\" Model \"{}\"", family, model); 067 068 // store the default range of version id's 069 setVersionRange(lowVersionID, highVersionID); 070 } 071 072 /** 073 * Create a mechanism to manipulate a decoder definition from up to 12 parameters. 074 * 075 * @param mfg manufacturer name 076 * @param mfgID manufacturer's NMRA manufacturer number, typically a "CV8" value 077 * @param model decoder model designation 078 * @param lowVersionID decoder version low byte, where applicable 079 * @param highVersionID decoder version high byte, where applicable 080 * @param family decoder family name, where applicable 081 * @param filename filename of decoder XML definition 082 * @param numFns decoder's number of available functions 083 * @param numOuts decoder's number of available function outputs 084 * @param decoder Element containing decoder XML definition 085 * @param replacementModel name of decoder file (which replaces this one?) 086 * @param replacementFamily name of decoder family (which replaces this one?) 087 */ 088 public DecoderFile(String mfg, String mfgID, String model, String lowVersionID, 089 String highVersionID, String family, String filename, 090 int numFns, int numOuts, Element decoder, String replacementModel, String replacementFamily) { 091 this(mfg, mfgID, model, lowVersionID, 092 highVersionID, family, filename, 093 numFns, numOuts, decoder); 094 _replacementModel = replacementModel; 095 _replacementFamily = replacementFamily; 096 _developerID = "-1"; 097 if (mfgID.compareTo("") != 0) { 098 // do not have manufacturerID, so take mfgID (which might not be set!) 099 _manufacturerID = mfgID; 100 } else { 101 _manufacturerID = "-1"; 102 } 103 _productID = "-1"; 104 } 105 106 /** 107 * Create a mechanism to manipulate a decoder definition from up to 15 parameters. 108 * 109 * @param mfg manufacturer name 110 * @param mfgID manufacturer's NMRA manufacturer number, typically a "CV8" value 111 * @param model decoder model designation 112 * @param lowVersionID decoder version low byte, where applicable 113 * @param highVersionID decoder version high byte, where applicable 114 * @param family decoder family name, where applicable 115 * @param filename filename of decoder XML definition 116 * @param developerID (typically LocoNet SV2) developerID number (8 bits) 117 * @param manufacturerID manufacturerID number (8 bits) 118 * @param productID product ID number (16 bits) 119 * @param numFns decoder's number of available functions 120 * @param numOuts decoder's number of available function outputs 121 * @param decoder Element containing decoder XML definition 122 * @param replacementModel name of decoder file (which replaces this one?) 123 * @param replacementFamily name of decoder family (which replaces this one?) 124 */ 125 public DecoderFile(String mfg, String mfgID, String model, String lowVersionID, 126 String highVersionID, String family, String filename, 127 String developerID, String manufacturerID, String productID, 128 int numFns, int numOuts, Element decoder, String replacementModel, 129 String replacementFamily) { 130 this(mfg, mfgID, model, lowVersionID, 131 highVersionID, family, filename, 132 numFns, numOuts, decoder); 133 _replacementModel = replacementModel; 134 _replacementFamily = replacementFamily; 135 _developerID = developerID; 136 if (mfgID == null) { 137 log.error("mfgID missing for decoder file {}", filename); 138 } 139 if ((!manufacturerID.isEmpty()) && (manufacturerID.compareTo("-1") != 0)) { 140 // prefer manufacturerID over mfgID 141 _manufacturerID = manufacturerID; 142 } else if ((mfgID != null) && (mfgID.compareTo("") != 0)) { 143 // do not have manufacturerID, so take mfgID (which might not be set!) 144 _manufacturerID = mfgID; 145 } else { 146 _manufacturerID = "-1"; 147 } 148 _productID = productID; 149 } 150 151 /** 152 * Create a mechanism to manipulate a decoder definition from up to 16 parameters. 153 * 154 * @param mfg manufacturer name 155 * @param mfgID manufacturer's NMRA manufacturer number, typically a "CV8" value 156 * @param model decoder model designation 157 * @param lowVersionID decoder version low byte, where applicable 158 * @param highVersionID decoder version high byte, where applicable 159 * @param family decoder family name, where applicable 160 * @param filename filename of decoder XML definition 161 * @param developerID (typically LocoNet SV2) developerID number (8 bits) 162 * @param manufacturerID manufacturerID number (8 bits) 163 * @param productID product ID number (16 bits) 164 * @param numFns decoder's number of available functions 165 * @param numOuts decoder's number of available function outputs 166 * @param decoder Element containing decoder XML definition 167 * @param replacementModel name of decoder file (which replaces this one?) 168 * @param replacementFamily name of decoder family (which replaces this one?) 169 * @param programmingModes a comma-separated list of supported programming modes 170 */ 171 public DecoderFile(String mfg, String mfgID, String model, String lowVersionID, 172 String highVersionID, String family, String filename, 173 String developerID, String manufacturerID, String productID, 174 int numFns, int numOuts, Element decoder, String replacementModel, 175 String replacementFamily, String programmingModes) { 176 this(mfg, mfgID, model, lowVersionID, 177 highVersionID, family, filename, 178 developerID, manufacturerID, productID, 179 numFns, numOuts, decoder, replacementModel, 180 replacementFamily); 181 182 log.debug("DecoderFile {} created with ProgModes: {}", model, programmingModes); 183 _programmingModes = Objects.requireNonNullElse(programmingModes, ""); 184 } 185 186 // store acceptable version numbers 187 boolean[] versions = new boolean[256]; 188 189 public void setOneVersion(int i) { 190 versions[i] = true; 191 } 192 193 public void setVersionRange(int low, int high) { 194 for (int i = low; i <= high; i++) { 195 versions[i] = true; 196 } 197 } 198 199 public void setVersionRange(String lowVersionID, String highVersionID) { 200 if (lowVersionID != null) { 201 // lowVersionID is not null; check high version ID 202 if (highVersionID != null) { 203 // low version and high version are not null 204 setVersionRange(Integer.parseInt(lowVersionID), 205 Integer.parseInt(highVersionID)); 206 } else { 207 // low version not null, but high is null. This is 208 // a single value to match 209 setOneVersion(Integer.parseInt(lowVersionID)); 210 } 211 } else { 212 // lowVersionID is null; check high version ID 213 if (highVersionID != null) { 214 // low version null, but high is not null 215 setOneVersion(Integer.parseInt(highVersionID)); 216 //} else { 217 // both low and high version are null; do nothing 218 } 219 } 220 } 221 222 /** 223 * Test for correct decoder version number 224 * 225 * @param i the version to match 226 * @return true if decoder version matches i 227 */ 228 public boolean isVersion(int i) { 229 return versions[i]; 230 } 231 232 /** 233 * return array of versions 234 * 235 * @return array of boolean where each element is true if version matches 236 */ 237 public boolean[] getVersions() { 238 return Arrays.copyOf(versions, versions.length); 239 } 240 241 @Nonnull 242 public String getVersionsAsString() { 243 String ret = ""; 244 int partStart = -1; 245 String part; 246 for (int i = 0; i < 256; i++) { 247 if (partStart >= 0) { 248 /* working on part, found end of range */ 249 if (!versions[i]) { 250 if (i - partStart > 1) { 251 part = partStart + "-" + (i - 1); 252 } else { 253 part = "" + (i - 1); 254 } 255 if (ret.isEmpty()) { 256 ret = part; 257 } else { 258 ret = "," + part; 259 } 260 partStart = -1; 261 } 262 } else { 263 /* testing for new part */ 264 if (versions[i]) { 265 partStart = i; 266 } 267 } 268 } 269 if (partStart >= 0) { 270 if (partStart != 255) { 271 part = partStart + "-" + 255; 272 } else { 273 part = "" + partStart; 274 } 275 if (ret.isEmpty()) { 276 ret = ret + "," + part; 277 } else { 278 ret = part; 279 } 280 } 281 return (ret); 282 } 283 284 // store indexing information 285 String _mfg = null; 286 String _mfgID = null; 287 String _model = null; 288 String _family = null; 289 String _filename = null; 290 String _productID = null; 291 String _replacementModel = null; 292 String _replacementFamily = null; 293 String _developerID = null; 294 String _manufacturerID = null; 295 String _programmingModes = null; 296 int _numFns = -1; 297 int _numOuts = -1; 298 Element _element = null; 299 300 public String getMfg() { 301 return _mfg; 302 } 303 304 public String getMfgID() { 305 return _mfgID; 306 } 307 308 /** 309 * Get the (LocoNet SV2) "Developer ID" number. 310 * <p> 311 * This value is assigned by the device 312 * manufacturer and is an 8-bit number. 313 * @return the developerID number 314 */ 315 public String getDeveloperID() { 316 return _developerID; 317 } 318 319 /** 320 * Get the (LocoNet SV2/Uhlenbrock LNCV) "Manufacturer ID" number. 321 * <p> 322 * This value typically matches the NMRA 323 * manufacturer ID number and is an 8-bit number. 324 * 325 * @return the manufacturer number 326 */ 327 public String getManufacturerID() { 328 return _manufacturerID; 329 } 330 331 public String getModel() { 332 return _model; 333 } 334 335 public String getFamily() { 336 return _family; 337 } 338 339 public String getReplacementModel() { 340 return _replacementModel; 341 } 342 343 public String getReplacementFamily() { 344 return _replacementFamily; 345 } 346 347 public String getFileName() { 348 return _filename; 349 } 350 351 public int getNumFunctions() { 352 return _numFns; 353 } 354 355 public int getNumOutputs() { 356 return _numOuts; 357 } 358 359 public Showable getShowable() { 360 if (_element.getAttribute("show") == null) { 361 return Showable.YES; // default 362 } else if (_element.getAttributeValue("show").equals("yes")) { 363 return Showable.YES; 364 } else if (_element.getAttributeValue("show").equals("no")) { 365 return Showable.NO; 366 } else if (_element.getAttributeValue("show").equals("maybe")) { 367 return Showable.MAYBE; 368 } else { 369 log.error("unexpected value for show attribute: {}", _element.getAttributeValue("show")); 370 return Showable.YES; // default again 371 } 372 } 373 374 public enum Showable { 375 YES, NO, MAYBE 376 } 377 378 public String getModelComment() { 379 return _element.getAttributeValue("comment"); 380 } 381 382 public String getFamilyComment() { 383 return ((Element) _element.getParent()).getAttributeValue("comment"); 384 } 385 386 /** 387 * Get the "Product ID" value. 388 * <p> 389 * When applied to LocoNet devices programmed using the LocoNet SV2 or the Uhlenbrock LNCV protocol, 390 * this is a 16-bit value, and is used in identifying the decoder definition 391 * file that matches an SV2 or LNCV device. 392 * <p> 393 * Decoders which do not support SV2 or LNCV programming may use the Product ID 394 * value for other purposes. 395 * 396 * @return the productID number 397 */ 398 public String getProductID() { 399 _productID = _element.getAttributeValue("productID"); 400 return _productID; 401 } 402 403 public Element getModelElement() { 404 return _element; 405 } 406 407 ArrayList<LocoAddress.Protocol> protocols = null; 408 409 public LocoAddress.Protocol[] getSupportedProtocols() { 410 if (protocols == null) { 411 setSupportedProtocols(); 412 } 413 return protocols.toArray(new LocoAddress.Protocol[0]); 414 } 415 416 private void setSupportedProtocols() { 417 protocols = new ArrayList<>(); 418 if (_element.getChild("protocols") != null) { 419 List<Element> protocolList = _element.getChild("protocols").getChildren("protocol"); 420 protocolList.forEach((e) -> protocols.add(LocoAddress.Protocol.getByShortName(e.getText()))); 421 } 422 } 423 424 /** 425 * Get all specified programming modes a decoder xml supports. 426 * This does not include the programming attributes (like ops=false). 427 * 428 * @return a comma separated string of modes as specified in the decoder xml 429 * or empty string when none are specified 430 */ 431 public @Nonnull String getProgrammingModes() { 432 if (_programmingModes == null) { 433 _programmingModes = ""; 434 } 435 return _programmingModes; 436 } 437 438 public boolean isProgrammingMode(String mode) { 439 return getProgrammingModes().contains(mode); 440 } 441 442 // static service methods - extract info from a given Element 443 public static String getMfgName(Element decoderElement) { 444 return decoderElement.getChild("family").getAttribute("mfg").getValue(); 445 } 446 447 public static String getProgrammingModes(Element decoderElement) { 448 return decoderElement.getChild("programming").getChild("mode").getText(); 449 } 450 451 boolean isProductIDok(Element e, String extraInclude, String extraExclude) { 452 return isIncluded(e, _productID, _model, _family, extraInclude, extraExclude); 453 } 454 455 /** 456 * @param e XML element with possible "include" and "exclude" 457 * attributes to be checked 458 * @param productID the specific ID of the decoder being loaded, to check 459 * against include/exclude conditions 460 * @param modelID the model ID of the decoder being loaded, to check 461 * against include/exclude conditions 462 * @param familyID the family ID of the decoder being loaded, to check 463 * against include/exclude conditions 464 * @param extraInclude additional "include" terms 465 * @param extraExclude additional "exclude" terms 466 * @return true if element is included; false otherwise 467 */ 468 public static boolean isIncluded(Element e, String productID, String modelID, String familyID, String extraInclude, String extraExclude) { 469 String include = e.getAttributeValue("include"); 470 if (include != null) { 471 include = include + "," + extraInclude; 472 } else { 473 include = extraInclude; 474 } 475 // if there are any include clauses, then it has to match 476 if (!include.isEmpty() && !(isInList(productID, include) || isInList(modelID, include) || isInList(familyID, include))) { 477 if (log.isTraceEnabled()) { 478 log.trace("include not in list of OK values: /{}/ /{}/ /{}/", include, productID, modelID); 479 } 480 return false; 481 } 482 483 String exclude = e.getAttributeValue("exclude"); 484 if (exclude != null) { 485 exclude = exclude + "," + extraExclude; 486 } else { 487 exclude = extraExclude; 488 } 489 // if there are any exclude clauses, then it cannot match 490 if (!exclude.isEmpty() && (isInList(productID, exclude) || isInList(modelID, exclude) || isInList(familyID, exclude))) { 491 if (log.isTraceEnabled()) { 492 log.trace("exclude match: /{}/ /{}/ /{}/", exclude, productID, modelID); 493 } 494 return false; 495 } 496 497 return true; 498 } 499 500 /** 501 * @param checkFor see if this value is present within (this value could 502 * also be a comma-separated list) 503 * @param okList this comma-separated list of items 504 * (familyID/modelID/productID) 505 */ 506 private static boolean isInList(String checkFor, String okList) { 507 String test = "," + okList + ","; 508 if (test.contains("," + checkFor + ",")) { 509 return true; 510 } else if (checkFor != null) { 511 String[] testList = checkFor.split(","); 512 if (testList.length > 1) { 513 for (String item : testList) { 514 if (test.contains("," + item + ",")) { 515 return true; 516 } 517 } 518 } 519 } 520 return false; 521 } 522 523 /** 524 * Load a VariableTableModel for a given decoder Element, for the purposes of 525 * programming. 526 * 527 * @param decoderElement element which corresponds to the decoder 528 * @param variableModel resulting VariableTableModel 529 */ 530 // use the decoder Element from the file to load a VariableTableModel for programming. 531 public void loadVariableModel(Element decoderElement, 532 VariableTableModel variableModel) { 533 534 nextCvStoreIndex = 0; 535 536 processVariablesElement(decoderElement.getChild("variables"), variableModel, "", ""); 537 538 variableModel.configDone(); 539 } 540 541 int nextCvStoreIndex = 0; 542 543 public void processVariablesElement(Element variablesElement, 544 VariableTableModel variableModel, String extraInclude, String extraExclude) { 545 546 // handle include, exclude on this element 547 extraInclude = extraInclude 548 + (variablesElement.getAttributeValue("include") != null ? "," + variablesElement.getAttributeValue("include") : ""); 549 extraExclude = extraExclude 550 + (variablesElement.getAttributeValue("exclude") != null ? "," + variablesElement.getAttributeValue("exclude") : ""); 551 log.debug("extraInclude /{}/, extraExclude /{}/", extraInclude, extraExclude); 552 553 // load variables to table 554 for (Element e : variablesElement.getChildren("variable")) { 555 try { 556 // if it's associated with an inconsistent number of functions, 557 // skip creating it 558 if (getNumFunctions() >= 0 && e.getAttribute("minFn") != null 559 && getNumFunctions() < e.getAttribute("minFn").getIntValue()) { 560 continue; 561 } 562 // if it's associated with an inconsistent number of outputs, 563 // skip creating it 564 if (getNumOutputs() >= 0 && e.getAttribute("minOut") != null 565 && getNumOutputs() < Integer.parseInt(e.getAttribute("minOut").getValue())) { 566 continue; 567 } 568 // if not correct productID, skip 569 if (!isProductIDok(e, extraInclude, extraExclude)) { 570 continue; 571 } 572 } catch (NumberFormatException | DataConversionException ex) { 573 log.warn("Problem parsing minFn or minOut in decoder file, variable {} exception", e.getAttribute("item"), ex); 574 } 575 // load each row 576 variableModel.setRow(nextCvStoreIndex++, e, _element == null ? null : this); 577 } 578 579 // load constants to table 580 for (Element e : variablesElement.getChildren("constant")) { 581 try { 582 // if it's associated with an inconsistent number of functions, 583 // skip creating it 584 if (getNumFunctions() >= 0 && e.getAttribute("minFn") != null 585 && getNumFunctions() < e.getAttribute("minFn").getIntValue()) { 586 continue; 587 } 588 // if it's associated with an inconsistent number of outputs, 589 // skip creating it 590 if (getNumOutputs() >= 0 && e.getAttribute("minOut") != null 591 && getNumOutputs() < e.getAttribute("minOut").getIntValue()) { 592 continue; 593 } 594 // if not correct productID, skip 595 if (!isProductIDok(e, extraInclude, extraExclude)) { 596 continue; 597 } 598 } catch (DataConversionException ex) { 599 log.warn("Problem parsing minFn or minOut in decoder file, variable {} exception", e.getAttribute("item"), ex); 600 } 601 // load each row 602 variableModel.setConstant(e); 603 } 604 605 for (Element e : variablesElement.getChildren("variables")) { 606 processVariablesElement(e, variableModel, extraInclude, extraExclude); 607 } 608 609 } 610 611 // use the decoder Element from the file to load a VariableTableModel for programming. 612 public void loadResetModel(Element decoderElement, 613 ResetTableModel resetModel) { 614 if (decoderElement.getChild("resets") != null) { 615 List<Element> resetList = decoderElement.getChild("resets").getChildren("factReset"); 616 for (int i = 0; i < resetList.size(); i++) { 617 Element e = resetList.get(i); 618 resetModel.setRow(i, e, decoderElement.getChild("resets"), _model); 619 } 620 } 621 } 622 623 // process "extraMenu" elements into data model(s) 624 public void loadExtraMenuModel(Element decoderElement, ArrayList<ExtraMenuTableModel> extraMenuModelList, JLabel progStatus, Programmer mProgrammer) { 625 var menus = decoderElement.getChildren("extraMenu"); 626 log.trace("loadExtraMenuModel {} {}", menus.size(), extraMenuModelList); 627 int i = 0; 628 for (var menuElement : menus) { 629 if (i >= extraMenuModelList.size() || extraMenuModelList.get(i) == null) { 630 log.trace("Add element {} in array of size {}",i,extraMenuModelList.size()); 631 var model = new ExtraMenuTableModel(progStatus, mProgrammer); 632 model.setName(menuElement.getAttributeValue("name","Extra")); 633 extraMenuModelList.add(i, model); 634 } 635 636 List<Element> itemList = menuElement.getChildren("extraMenuItem"); 637 var extraMenuModel = extraMenuModelList.get(i); 638 for (int j = 0; j < itemList.size(); j++) { 639 Element e = itemList.get(j); 640 extraMenuModel.setRow(j, e, menuElement, _model); 641 } 642 i++; 643 } 644 } 645 646 /** 647 * Convert to a canonical text form for ComboBoxes, etc. 648 * <p> 649 * Must be able to distinguish identical models in different families. 650 * 651 * @return the title string for the decoder 652 */ 653 public String titleString() { 654 return titleString(getModel(), getFamily()); 655 } 656 657 public static String titleString(String model, String family) { 658 return model + " (" + family + ")"; 659 } 660 661 @SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL") // script access 662 public static String fileLocation = "decoders" + File.separator; 663 664 // initialize logging 665 private static final Logger log = LoggerFactory.getLogger(DecoderFile.class); 666 667}