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}