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