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}