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("no")) { 363 return Showable.NO; 364 } else if (_element.getAttributeValue("show").equals("maybe")) { 365 return Showable.MAYBE; 366 } else { 367 log.error("unexpected value for show attribute: {}", _element.getAttributeValue("show")); 368 return Showable.YES; // default again 369 } 370 } 371 372 public enum Showable { 373 YES, NO, MAYBE 374 } 375 376 public String getModelComment() { 377 return _element.getAttributeValue("comment"); 378 } 379 380 public String getFamilyComment() { 381 return ((Element) _element.getParent()).getAttributeValue("comment"); 382 } 383 384 /** 385 * Get the "Product ID" value. 386 * <p> 387 * When applied to LocoNet devices programmed using the LocoNet SV2 or the Uhlenbrock LNCV protocol, 388 * this is a 16-bit value, and is used in identifying the decoder definition 389 * file that matches an SV2 or LNCV device. 390 * <p> 391 * Decoders which do not support SV2 or LNCV programming may use the Product ID 392 * value for other purposes. 393 * 394 * @return the productID number 395 */ 396 public String getProductID() { 397 _productID = _element.getAttributeValue("productID"); 398 return _productID; 399 } 400 401 public Element getModelElement() { 402 return _element; 403 } 404 405 ArrayList<LocoAddress.Protocol> protocols = null; 406 407 public LocoAddress.Protocol[] getSupportedProtocols() { 408 if (protocols == null) { 409 setSupportedProtocols(); 410 } 411 return protocols.toArray(new LocoAddress.Protocol[0]); 412 } 413 414 private void setSupportedProtocols() { 415 protocols = new ArrayList<>(); 416 if (_element.getChild("protocols") != null) { 417 List<Element> protocolList = _element.getChild("protocols").getChildren("protocol"); 418 protocolList.forEach((e) -> protocols.add(LocoAddress.Protocol.getByShortName(e.getText()))); 419 } 420 } 421 422 /** 423 * Get all specified programming modes a decoder xml supports. 424 * This does not include the programming attributes (like ops=false). 425 * 426 * @return a comma separated string of modes as specified in the decoder xml 427 * or empty string when none are specified 428 */ 429 public @Nonnull String getProgrammingModes() { 430 if (_programmingModes == null) { 431 _programmingModes = ""; 432 } 433 return _programmingModes; 434 } 435 436 public boolean isProgrammingMode(String mode) { 437 return getProgrammingModes().contains(mode); 438 } 439 440 // static service methods - extract info from a given Element 441 public static String getMfgName(Element decoderElement) { 442 return decoderElement.getChild("family").getAttribute("mfg").getValue(); 443 } 444 445 public static String getProgrammingModes(Element decoderElement) { 446 return decoderElement.getChild("programming").getChild("mode").getText(); 447 } 448 449 boolean isProductIDok(Element e, String extraInclude, String extraExclude) { 450 return isIncluded(e, _productID, _model, _family, extraInclude, extraExclude); 451 } 452 453 /** 454 * @param e XML element with possible "include" and "exclude" 455 * attributes to be checked 456 * @param productID the specific ID of the decoder being loaded, to check 457 * against include/exclude conditions 458 * @param modelID the model ID of the decoder being loaded, to check 459 * against include/exclude conditions 460 * @param familyID the family ID of the decoder being loaded, to check 461 * against include/exclude conditions 462 * @param extraInclude additional "include" terms 463 * @param extraExclude additional "exclude" terms 464 * @return true if element is included; false otherwise 465 */ 466 public static boolean isIncluded(Element e, String productID, String modelID, String familyID, String extraInclude, String extraExclude) { 467 String include = e.getAttributeValue("include"); 468 if (include != null) { 469 include = include + "," + extraInclude; 470 } else { 471 include = extraInclude; 472 } 473 // if there are any include clauses, then it has to match 474 if (!include.isEmpty() && !(isInList(productID, include) || isInList(modelID, include) || isInList(familyID, include))) { 475 if (log.isTraceEnabled()) { 476 log.trace("include not in list of OK values: /{}/ /{}/ /{}/", include, productID, modelID); 477 } 478 return false; 479 } 480 481 String exclude = e.getAttributeValue("exclude"); 482 if (exclude != null) { 483 exclude = exclude + "," + extraExclude; 484 } else { 485 exclude = extraExclude; 486 } 487 // if there are any exclude clauses, then it cannot match 488 if (!exclude.isEmpty() && (isInList(productID, exclude) || isInList(modelID, exclude) || isInList(familyID, exclude))) { 489 if (log.isTraceEnabled()) { 490 log.trace("exclude match: /{}/ /{}/ /{}/", exclude, productID, modelID); 491 } 492 return false; 493 } 494 495 return true; 496 } 497 498 /** 499 * @param checkFor see if this value is present within (this value could 500 * also be a comma-separated list) 501 * @param okList this comma-separated list of items 502 * (familyID/modelID/productID) 503 */ 504 private static boolean isInList(String checkFor, String okList) { 505 String test = "," + okList + ","; 506 if (test.contains("," + checkFor + ",")) { 507 return true; 508 } else if (checkFor != null) { 509 String[] testList = checkFor.split(","); 510 if (testList.length > 1) { 511 for (String item : testList) { 512 if (test.contains("," + item + ",")) { 513 return true; 514 } 515 } 516 } 517 } 518 return false; 519 } 520 521 /** 522 * Load a VariableTableModel for a given decoder Element, for the purposes of 523 * programming. 524 * 525 * @param decoderElement element which corresponds to the decoder 526 * @param variableModel resulting VariableTableModel 527 */ 528 // use the decoder Element from the file to load a VariableTableModel for programming. 529 public void loadVariableModel(Element decoderElement, 530 VariableTableModel variableModel) { 531 532 nextCvStoreIndex = 0; 533 534 processVariablesElement(decoderElement.getChild("variables"), variableModel, "", ""); 535 536 variableModel.configDone(); 537 } 538 539 int nextCvStoreIndex = 0; 540 541 public void processVariablesElement(Element variablesElement, 542 VariableTableModel variableModel, String extraInclude, String extraExclude) { 543 544 // handle include, exclude on this element 545 extraInclude = extraInclude 546 + (variablesElement.getAttributeValue("include") != null ? "," + variablesElement.getAttributeValue("include") : ""); 547 extraExclude = extraExclude 548 + (variablesElement.getAttributeValue("exclude") != null ? "," + variablesElement.getAttributeValue("exclude") : ""); 549 log.debug("extraInclude /{}/, extraExclude /{}/", extraInclude, extraExclude); 550 551 // load variables to table 552 for (Element e : variablesElement.getChildren("variable")) { 553 try { 554 // if it's associated with an inconsistent number of functions, 555 // skip creating it 556 if (getNumFunctions() >= 0 && e.getAttribute("minFn") != null 557 && getNumFunctions() < e.getAttribute("minFn").getIntValue()) { 558 continue; 559 } 560 // if it's associated with an inconsistent number of outputs, 561 // skip creating it 562 if (getNumOutputs() >= 0 && e.getAttribute("minOut") != null 563 && getNumOutputs() < Integer.parseInt(e.getAttribute("minOut").getValue())) { 564 continue; 565 } 566 // if not correct productID, skip 567 if (!isProductIDok(e, extraInclude, extraExclude)) { 568 continue; 569 } 570 } catch (NumberFormatException | DataConversionException ex) { 571 log.warn("Problem parsing minFn or minOut in decoder file, variable {} exception", e.getAttribute("item"), ex); 572 } 573 // load each row 574 variableModel.setRow(nextCvStoreIndex++, e, _element == null ? null : this); 575 } 576 577 // load constants to table 578 for (Element e : variablesElement.getChildren("constant")) { 579 try { 580 // if it's associated with an inconsistent number of functions, 581 // skip creating it 582 if (getNumFunctions() >= 0 && e.getAttribute("minFn") != null 583 && getNumFunctions() < e.getAttribute("minFn").getIntValue()) { 584 continue; 585 } 586 // if it's associated with an inconsistent number of outputs, 587 // skip creating it 588 if (getNumOutputs() >= 0 && e.getAttribute("minOut") != null 589 && getNumOutputs() < e.getAttribute("minOut").getIntValue()) { 590 continue; 591 } 592 // if not correct productID, skip 593 if (!isProductIDok(e, extraInclude, extraExclude)) { 594 continue; 595 } 596 } catch (DataConversionException ex) { 597 log.warn("Problem parsing minFn or minOut in decoder file, variable {} exception", e.getAttribute("item"), ex); 598 } 599 // load each row 600 variableModel.setConstant(e); 601 } 602 603 for (Element e : variablesElement.getChildren("variables")) { 604 processVariablesElement(e, variableModel, extraInclude, extraExclude); 605 } 606 607 } 608 609 // use the decoder Element from the file to load a VariableTableModel for programming. 610 public void loadResetModel(Element decoderElement, 611 ResetTableModel resetModel) { 612 if (decoderElement.getChild("resets") != null) { 613 List<Element> resetList = decoderElement.getChild("resets").getChildren("factReset"); 614 for (int i = 0; i < resetList.size(); i++) { 615 Element e = resetList.get(i); 616 resetModel.setRow(i, e, decoderElement.getChild("resets"), _model); 617 } 618 } 619 } 620 621 // process "extraMenu" elements into data model(s) 622 public void loadExtraMenuModel(Element decoderElement, ArrayList<ExtraMenuTableModel> extraMenuModelList, JLabel progStatus, Programmer mProgrammer) { 623 var menus = decoderElement.getChildren("extraMenu"); 624 log.trace("loadExtraMenuModel {} {}", menus.size(), extraMenuModelList); 625 int i = 0; 626 for (var menuElement : menus) { 627 if (i >= extraMenuModelList.size() || extraMenuModelList.get(i) == null) { 628 log.trace("Add element {} in array of size {}",i,extraMenuModelList.size()); 629 var model = new ExtraMenuTableModel(progStatus, mProgrammer); 630 model.setName(menuElement.getAttributeValue("name","Extra")); 631 extraMenuModelList.add(i, model); 632 } 633 634 List<Element> itemList = menuElement.getChildren("extraMenuItem"); 635 var extraMenuModel = extraMenuModelList.get(i); 636 for (int j = 0; j < itemList.size(); j++) { 637 Element e = itemList.get(j); 638 extraMenuModel.setRow(j, e, menuElement, _model); 639 } 640 i++; 641 } 642 } 643 644 /** 645 * Convert to a canonical text form for ComboBoxes, etc. 646 * <p> 647 * Must be able to distinguish identical models in different families. 648 * 649 * @return the title string for the decoder 650 */ 651 public String titleString() { 652 return titleString(getModel(), getFamily()); 653 } 654 655 static public String titleString(String model, String family) { 656 return model + " (" + family + ")"; 657 } 658 659 @SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL") // script access 660 static public String fileLocation = "decoders" + File.separator; 661 662 // initialize logging 663 private final static Logger log = LoggerFactory.getLogger(DecoderFile.class); 664 665}