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}