001package jmri.jmrit.decoderdefn;
002
003import java.awt.GraphicsEnvironment;
004import java.io.File;
005import java.io.IOException;
006import java.io.FileNotFoundException;
007import java.net.URL;
008import java.util.ArrayList;
009import java.util.Arrays;
010import java.util.Collections;
011import java.util.HashMap;
012import java.util.List;
013import java.util.Set;
014import javax.annotation.Nonnull;
015import javax.swing.JComboBox;
016import javax.swing.JDialog;
017import javax.swing.JProgressBar;
018import javax.swing.JOptionPane;
019import jmri.InstanceInitializer;
020import jmri.InstanceManager;
021import jmri.implementation.AbstractInstanceInitializer;
022import jmri.jmrit.XmlFile;
023import jmri.util.FileUtil;
024import jmri.util.ThreadingUtil;
025import org.jdom2.Attribute;
026import org.jdom2.Comment;
027import org.jdom2.Document;
028import org.jdom2.Element;
029import org.jdom2.JDOMException;
030import org.jdom2.ProcessingInstruction;
031import org.openide.util.lookup.ServiceProvider;
032import org.slf4j.Logger;
033import org.slf4j.LoggerFactory;
034
035/**
036 * DecoderIndex represents the decoderIndex.xml (decoder types) and
037 * nmra_mfg_list.xml (Manufacturer ID list) files in memory.
038 * <p>
039 * This allows a program to navigate to various decoder descriptions without
040 * having to manipulate files.
041 * <p>
042 * This class doesn't provide tools for defining the index; that's done
043 * by {@link jmri.jmrit.decoderdefn.DecoderIndexCreateAction}, which
044 * rebuilds it from the decoder files.
045 * <p>
046 * Multiple DecoderIndexFile objects don't make sense, so we use an "instance"
047 * member to navigate to a single one.
048 * <p>
049 * Previous to JMRI 4.19.1, the manufacturer information was kept in the
050 * decoderIndex.xml file. Starting with that version it's in the separate
051 * nmra_mfg_list.xml file, but still written to decoderIndex.xml when
052 * one is created.
053 *
054 * @author Bob Jacobsen Copyright (C) 2001, 2019, 2025
055 * @see jmri.jmrit.decoderdefn.DecoderIndexCreateAction
056 */
057public class DecoderIndexFile extends XmlFile {
058
059    public static final String MANUFACTURER = "manufacturer";
060    public static final String MFG_ID = "mfgID";
061    public static final String DECODER_INDEX = "decoderIndex";
062    public static final String VERSION = "version";
063    public static final String LOW_VERSION_ID = "lowVersionID";
064    public static final String HIGH_VERSION_ID = "highVersionID";
065    // fill in abstract members
066    protected List<DecoderFile> decoderList = new ArrayList<>();
067
068    public int numDecoders() {
069        return decoderList.size();
070    }
071
072    int fileVersion = -1;
073
074    // map mfg ID numbers from & to mfg names
075    protected HashMap<String, String> _mfgIdFromNameHash = new HashMap<>();
076    protected HashMap<String, String> _mfgNameFromIdHash = new HashMap<>();
077
078    protected ArrayList<String> mMfgNameList = new ArrayList<>();
079
080    public List<String> getMfgNameList() {
081        return mMfgNameList;
082    }
083
084    public String mfgIdFromName(String name) {
085        return _mfgIdFromNameHash.get(name);
086    }
087
088    /**
089     *
090     * @param idNum String containing the manufacturer's NMRA
091     *      manufacturer ID number
092     * @return String containing the "friendly" name of the manufacturer
093     */
094
095    public String mfgNameFromID(String idNum) {
096        return _mfgNameFromIdHash.get(idNum);
097    }
098
099    /**
100     * Get a List of decoders matching (only) the programming mode.
101     *
102     * @param progMode  decoder programming mode as defined in a decoder's programming element
103     * @return a list, possibly empty, of matching decoders
104     */
105    @Nonnull
106    public List<DecoderFile> matchingDecoderList(String progMode) {
107        return (matchingDecoderList(null, null, null, null, null,
108                null, null, null, null, progMode));
109    }
110
111    /**
112     * Get a List of decoders matching basic characteristics.
113     *
114     * @param mfg              decoder manufacturer
115     * @param family           decoder family
116     * @param decoderMfgID     NMRA decoder manufacturer ID
117     * @param decoderVersionID decoder version ID
118     * @param decoderProductID decoder product ID
119     * @param model            decoder model
120     * @return a list, possibly empty, of matching decoders
121     */
122    @Nonnull
123    public List<DecoderFile> matchingDecoderList(String mfg, String family,
124            String decoderMfgID, String decoderVersionID, String decoderProductID,
125            String model) {
126        return (matchingDecoderList(mfg, family, decoderMfgID, decoderVersionID, decoderProductID, model,
127                null, null, null, null));
128    }
129
130    /**
131     * Get a List of decoders matching basic characteristics + product ID etc.
132     *
133     * @param mfg              decoder manufacturer
134     * @param family           decoder family
135     * @param decoderMfgID     NMRA decoder manufacturer ID
136     * @param decoderVersionID decoder version ID
137     * @param decoderProductID decoder product ID
138     * @param model            decoder model
139     * @param developerID      developer ID number
140     * @param manufacturerID   manufacturerID number
141     * @param productID        productID number
142     * @return a list, possibly empty, of matching decoders
143     */
144    @Nonnull
145    public List<DecoderFile> matchingDecoderList(String mfg, String family,
146            String decoderMfgID, String decoderVersionID,
147            String decoderProductID, String model, String developerID, String manufacturerID, String productID) {
148        return (matchingDecoderList(mfg, family, decoderMfgID, decoderVersionID, decoderProductID, model,
149                null, null, null, null));
150    }
151
152    /**
153     * Get a List of decoders matching basic characteristics + product ID etc. + programming mode.
154     *
155     * @param mfg              decoder manufacturer
156     * @param family           decoder family
157     * @param decoderMfgID     NMRA decoder manufacturer ID
158     * @param decoderVersionID decoder version ID
159     * @param decoderProductID decoder product ID
160     * @param model            decoder model
161     * @param developerID      developer ID number
162     * @param manufacturerID   manufacturerID number
163     * @param productID        productID number
164     * @param progMode         programming mode as defined in a decoder's programming element
165     * @return a list, possibly empty, of matching decoders
166     */
167    @Nonnull
168    public List<DecoderFile> matchingDecoderList(String mfg, String family,
169                                                 String decoderMfgID, String decoderVersionID,
170                                                 String decoderProductID, String model, String developerID,
171                                                 String manufacturerID, String productID, String progMode) {
172        List<DecoderFile> l = new ArrayList<>();
173        for (int i = 0; i < numDecoders(); i++) {
174            if (checkEntry(i, mfg, family, decoderMfgID, decoderVersionID, decoderProductID, model, developerID,
175                    manufacturerID, productID, progMode)) {
176                l.add(decoderList.get(i));
177            }
178        }
179        return l;
180    }
181
182    /**
183     * Get a JComboBox representing the choices that match basic characteristics.
184     *
185     * @param mfg              decoder manufacturer
186     * @param family           decoder family
187     * @param decoderMfgID     NMRA decoder manufacturer ID
188     * @param decoderVersionID decoder version ID
189     * @param decoderProductID decoder product ID
190     * @param model            decoder model
191     * @return a combo box populated with matching decoders
192     */
193    public JComboBox<String> matchingComboBox(String mfg, String family, String decoderMfgID, String decoderVersionID,
194                                              String decoderProductID, String model) {
195        List<DecoderFile> l = matchingDecoderList(mfg, family, decoderMfgID, decoderVersionID, decoderProductID, model);
196        return jComboBoxFromList(l);
197    }
198
199    /**
200     * Get a new JComboBox made with the titles from a list of DecoderFile.
201     *
202     * @param l list of decoders
203     * @return a combo box populated with the list
204     */
205    public static JComboBox<String> jComboBoxFromList(List<DecoderFile> l) {
206        return new JComboBox<>(jComboBoxModelFromList(l));
207    }
208
209    /**
210     * Get a new ComboBoxModel made with the titles from a list of DecoderFile.
211     * entries.
212     *
213     * @param l list of decoders
214     * @return a combo box model populated with the list
215     */
216    public static javax.swing.ComboBoxModel<String> jComboBoxModelFromList(List<DecoderFile> l) {
217        javax.swing.DefaultComboBoxModel<String> b = new javax.swing.DefaultComboBoxModel<>();
218        for (DecoderFile r : l) {
219            b.addElement(r.titleString());
220        }
221        return b;
222    }
223
224    /**
225     * Get a DecoderFile from a "title" string, typically a selection in a
226     * matching ComboBox.
227     *
228     * @param title the decoder title
229     * @return the decoder file
230     */
231    public DecoderFile fileFromTitle(String title) {
232        for (int i = numDecoders() - 1; i >= 0; i--) {
233            DecoderFile r = decoderList.get(i);
234            if (r.titleString().equals(title)) {
235                return r;
236            }
237        }
238        return null;
239    }
240
241    /**
242     * Check if an entry consistent with specific properties. A null String
243     * entry always matches. Strings are used for convenience in GUI building.
244     * Don't bother asking about the model number...
245     *
246     * @param i                index of entry
247     * @param mfgName          decoder manufacturer
248     * @param family           decoder family
249     * @param mfgID            NMRA decoder manufacturer ID
250     * @param decoderVersionID decoder version ID
251     * @param decoderProductID decoder product ID
252     * @param model            decoder model
253     * @param developerID      developer ID number
254     * @param manufacturerID   manufacturer ID number
255     * @param productID        product ID number
256     * @param progMode         programming mode as defined in a decoder's programming element
257     * @return true if entry at i matches the other parameters; false otherwise
258     */
259    public boolean checkEntry(int i, String mfgName, String family, String mfgID,
260            String decoderVersionID, String decoderProductID, String model,
261            String developerID, String manufacturerID, String productID, String progMode) {
262        DecoderFile r = decoderList.get(i);
263        if (mfgName != null && !mfgName.equals(r.getMfg())) {
264            return false;
265        }
266        if (family != null && !family.equals(r.getFamily())) {
267            return false;
268        }
269        if (mfgID != null && !mfgID.equals(r.getMfgID())) {
270            return false;
271        }
272        if (model != null && !model.equals(r.getModel())) {
273            return false;
274        }
275        // check version ID - no match if a range specified and out of range
276        if (decoderVersionID != null) {
277            int versionID = Integer.parseInt(decoderVersionID);
278            if (!r.isVersion(versionID)) {
279                return false;
280            }
281        }
282
283        if (decoderProductID != null && !checkInCommaDelimString(decoderProductID, r.getProductID())) {
284            return false;
285        }
286
287        if (developerID != null) {
288            // must have a (LocoNet SV2) developerID value that matches to consider this entry a match
289            if (!developerID.equals(r.getDeveloperID())) {
290                // didn't match the getDeveloperID() value, so check the model developerID value
291                if (r.getModelElement().getAttribute("developerID") == null) {
292                    // no model developerID value, so not a match!
293                    return false;
294                }
295                if (!("," + r.getModelElement().getAttribute("developerID").getValue() + ",").contains("," + developerID + ",")) {
296                        return false;
297                }
298            }
299            log.debug("developerID match");
300        }
301
302        if (manufacturerID != null) {
303            log.debug("checking manufactureriD {}, mfgID {}, modelElement[manufacturerID] {}",
304                    manufacturerID, r._mfgID, r.getModelElement().getAttribute("manufacturerID"));
305            // must have a manufacturerID value that matches to consider this entry a match
306
307            if ((r._mfgID == null) || (manufacturerID.compareTo(r._mfgID) != 0)) {
308                // ID number from manufacturer name isn't identical; try another way
309                if (!manufacturerID.equals(r.getManufacturerID())) {
310                    // no match to the manufacturerID attribute at the (family?) level, so try model level
311                    Attribute a = r.getModelElement().getAttribute("manufacturerID");
312                    if ((a == null) || (a.getValue() == null) ||
313                            (manufacturerID.compareTo(a.getValue())!=0)) {
314                            // no model manufacturerID value, or model manufacturerID
315                            // value does not match so this decoder is not a match!
316                            return false;
317                    }
318                }
319            }
320            log.debug("manufacturerID match");
321        }
322
323        if (productID != null) {
324            // must have a (LocoNet SV2 or the Uhlenbrock LNCV protocol) productID value that matches to consider this entry a match
325            if (!productID.equals(r.getProductID())) {
326                // didn't match the getProductID() value, so check the model productID value
327                if (r.getModelElement().getAttribute("productID") == null) {
328                    // no model productID value, so not a match!
329                    return false;
330                }
331                if (!("," + r.getModelElement().getAttribute("productID").getValue() + ",").contains("," + productID + ",")) {
332                        return false;
333                }
334            }
335            log.debug("productID match");
336        }
337
338        if (progMode != null) {
339            // must have a progMode value that matches to consider this entry a match
340            return r.isProgrammingMode(progMode); // simplified logic while this is the last if in method
341        }
342
343        return true;
344    }
345
346    /**
347     * Replace the managed instance with a new instance.
348     */
349    public static synchronized void resetInstance() {
350        InstanceManager.getDefault().clear(DecoderIndexFile.class);
351    }
352
353    /**
354     * Check whether the user's version of the decoder index file needs to be
355     * updated; if it does, then forces the update.
356     *
357     * @return true is the index should be reloaded because it was updated
358     * @throws JDOMException if unable to parse decoder index
359     * @throws IOException     if unable to read decoder index
360     */
361    public static boolean updateIndexIfNeeded() throws JDOMException, IOException {
362        switch (FileUtil.findFiles(defaultDecoderIndexFilename(), ".").size()) {
363            case 0:
364                log.debug("creating decoder index");
365                forceCreationOfNewIndex();
366                return true; // no index exists, so create one
367            case 1:
368                return false; // only one index, so nothing to compare
369            default:
370                // multiple indexes, so continue with more specific checks
371                break;
372        }
373
374        // get version from master index; if not found, give up
375        String masterVersion = null;
376        DecoderIndexFile masterXmlFile = new DecoderIndexFile();
377        URL masterFile = FileUtil.findURL("xml/" + defaultDecoderIndexFilename(), FileUtil.Location.INSTALLED);
378        if (masterFile == null) {
379            return false;
380        }
381        log.debug("checking for master file at {}", masterFile);
382        Element masterRoot = masterXmlFile.rootFromURL(masterFile);
383        if (masterRoot.getChild(DECODER_INDEX) != null) {
384            if (masterRoot.getChild(DECODER_INDEX).getAttribute(VERSION) != null) {
385                masterVersion = masterRoot.getChild(DECODER_INDEX).getAttribute(VERSION).getValue();
386            }
387            log.debug("master version found, is {}", masterVersion);
388        } else {
389            return false;
390        }
391
392        // get from user index.  Unless they are equal, force an update.
393        // note we find this file via the search path; if not exists, so that
394        // the master is found, we still do the right thing (nothing).
395        String userVersion = null;
396        DecoderIndexFile userXmlFile = new DecoderIndexFile();
397        log.debug("checking for user file at {}", defaultDecoderIndexFilename());
398        Element userRoot = userXmlFile.rootFromName(defaultDecoderIndexFilename());
399        if (userRoot.getChild(DECODER_INDEX) != null) {
400            if (userRoot.getChild(DECODER_INDEX).getAttribute(VERSION) != null) {
401                userVersion = userRoot.getChild(DECODER_INDEX).getAttribute(VERSION).getValue();
402            }
403            log.debug("user version found, is {}", userVersion);
404        }
405        if (masterVersion != null && masterVersion.equals(userVersion)) {
406            return false;
407        }
408
409        // force the update, with the version number located earlier is available
410        log.debug("forcing update of decoder index due to {} and {}", masterVersion, userVersion);
411        forceCreationOfNewIndex();
412        // and force it to be used
413        return true;
414    }
415
416    /**
417     * Force creation of a new user index without incrementing version
418     */
419    public static void forceCreationOfNewIndex() {
420        forceCreationOfNewIndex(false);
421    }
422
423    /**
424     * Force creation of a new user index.
425     *
426     * @param increment true to increment the version of the decoder index
427     */
428    public static void forceCreationOfNewIndex(boolean increment) {
429        log.info("update decoder index");
430        // make sure we're using only the default manufacturer info
431        // to keep from propagating wrong, old stuff
432        File oldfile = new File(FileUtil.getUserFilesPath() + DECODER_INDEX_FILE_NAME);
433        if (oldfile.exists()) {
434            log.debug("remove existing user decoderIndex.xml file");
435            if (!oldfile.delete()) // delete file, check for success
436            {
437                log.error("Failed to delete old index file");
438            }
439            // force read from distributed file on next access
440            resetInstance();
441        }
442
443        // create an array of file names from decoders dir in preferences, count entries
444        ArrayList<String> al = new ArrayList<>();
445        FileUtil.createDirectory(FileUtil.getUserFilesPath() + DecoderFile.fileLocation);
446        File fp = new File(FileUtil.getUserFilesPath() + DecoderFile.fileLocation);
447
448        if (fp.exists()) {
449            String[] list = fp.list();
450            if (list !=null) {
451                for (String sp : list) {
452                    if (sp.endsWith(".xml") || sp.endsWith(".XML")) {
453                        al.add(sp);
454                    }
455                }
456            }
457        } else {
458            log.debug("{}decoders was missing, though tried to create it", FileUtil.getUserFilesPath());
459        }
460        // create an array of file names from xml/decoders, count entries
461        String[] fileList = (new File(XmlFile.xmlDir() + DecoderFile.fileLocation)).list();
462        if (fileList != null) {
463            for (String sx : fileList ) {
464                if (sx.endsWith(".xml") || sx.endsWith(".XML")) {
465                    // Valid name.  Does it exist in preferences xml/decoders?
466                    if (!al.contains(sx)) {
467                        // no, include it!
468                        al.add(sx);
469                    }
470                }
471            }
472        } else {
473            log.error("Could not access decoder definition directory {}{}", XmlFile.xmlDir(), DecoderFile.fileLocation);
474        }
475        // copy the decoder entries to the final array
476        String[] sbox = al.toArray(new String[0]);
477
478        //the resulting array is now sorted on file-name to make it easier
479        // for humans to read
480        Arrays.sort(sbox);
481
482        // create a new decoderIndex
483        DecoderIndexFile index = new DecoderIndexFile();
484
485        // For user operations the existing version is used, so that a new master file
486        // with a larger one will force an update
487        if (increment) {
488            index.fileVersion = InstanceManager.getDefault(DecoderIndexFile.class).fileVersion + 2;
489        } else {
490            index.fileVersion = InstanceManager.getDefault(DecoderIndexFile.class).fileVersion;
491        }
492
493        // If not many entries, or headless, just recreate index without updating the UI
494        // Also block if not on the GUI (event dispatch) thread
495        if (sbox.length < 30 || GraphicsEnvironment.isHeadless() || !ThreadingUtil.isGUIThread()) {
496            try {
497                index.writeFile(DECODER_INDEX_FILE_NAME,
498                            InstanceManager.getDefault(DecoderIndexFile.class), sbox, null, null);
499            } catch (IOException ex) {
500                log.error("Error writing new decoder index file: {}", ex.getMessage());
501            }
502            return;
503        }
504
505        // Create a dialog with a progress bar and a cancel button
506        String message = Bundle.getMessage("DecoderProgressMessage", "..."); // NOI18N
507        String title = Bundle.getMessage("DecoderProgressMessage", "");
508        String cancel = Bundle.getMessage("ButtonCancel"); // NOI18N
509        // HACK: add long blank space to message to make dialog wider.
510        JOptionPane pane = new JOptionPane(message + "                            \t",
511                JOptionPane.PLAIN_MESSAGE,
512                JOptionPane.OK_CANCEL_OPTION,
513                null,
514                new String[]{cancel});
515        JProgressBar pb = new JProgressBar(0, sbox.length);
516        pb.setValue(0);
517        pane.add(pb, 1);
518        JDialog dialog = pane.createDialog(null, title);
519
520        ThreadingUtil.newThread(() -> {
521            try {
522                index.writeFile(DECODER_INDEX_FILE_NAME,
523                            InstanceManager.getDefault(DecoderIndexFile.class), sbox, pane, pb);
524            // catch all exceptions, so progress dialog will close
525            } catch (IOException e) {
526                // TODO: show message in progress dialog?
527                log.error("Error writing new decoder index file: {}", e.getMessage());
528            }
529            dialog.setVisible(false);
530            dialog.dispose();
531        }, "decoderIndexer").start();
532
533        // improve visibility if any always on top frames present
534        dialog.setAlwaysOnTop(true);
535        dialog.toFront();
536        // this will block until the thread completes, either by
537        // finishing or by being cancelled
538        dialog.setVisible(true);
539    }
540
541    /**
542     * Read the contents of a decoderIndex XML file into this object. Note that
543     * this does not clear any existing entries; reset the instance to do that.
544     *
545     * @param name the name of the decoder index file
546     * @throws JDOMException if unable to parse to decoder index file
547     * @throws IOException     if unable to read decoder index file
548     */
549    void readFile(String name) throws JDOMException, IOException {
550        log.debug("readFile {}", name);
551
552        // read file, find root
553        Element root = rootFromName(name);
554
555        // decode type, invoke proper processing routine if a decoder file
556        if (root.getChild(DECODER_INDEX) != null) {
557            if (root.getChild(DECODER_INDEX).getAttribute(VERSION) != null) {
558                fileVersion = Integer.parseInt(root.getChild(DECODER_INDEX)
559                        .getAttribute(VERSION)
560                        .getValue()
561                );
562            }
563            log.debug("found fileVersion of {}", fileVersion);
564            readMfgSection();
565            readFamilySection(root.getChild(DECODER_INDEX));
566        } else {
567            log.error("Unrecognized decoderIndex file contents in file: {}", name);
568        }
569    }
570
571    void readMfgSection() throws JDOMException, IOException {
572        // always reads the NMRA manufacturer file distributed with JMRI
573        Element mfgList = rootFromName("nmra_mfg_list.xml");
574
575        if (mfgList != null) {
576
577            Attribute a;
578            a = mfgList.getAttribute("nmraListDate");
579            if (a != null) {
580                nmraListDate = a.getValue();
581            }
582            a = mfgList.getAttribute("updated");
583            if (a != null) {
584                updated = a.getValue();
585            }
586            a = mfgList.getAttribute("lastadd");
587            if (a != null) {
588                lastAdd = a.getValue();
589            }
590
591            List<Element> l = mfgList.getChildren(MANUFACTURER);
592            log.debug("readMfgSection sees {} children",l.size());
593            for (Element el : l) {
594                // handle each entry
595                String mfg = el.getAttribute("mfg").getValue();
596                mMfgNameList.add(mfg);
597                Attribute attr = el.getAttribute(MFG_ID);
598                if (attr != null) {
599                    _mfgIdFromNameHash.put(mfg, attr.getValue());
600                    _mfgNameFromIdHash.put(attr.getValue(), mfg);
601                }
602            }
603        } else {
604            log.debug("no mfgList found");
605        }
606    }
607
608    void readFamilySection(Element decoderIndex) {
609        Element familyList = decoderIndex.getChild("familyList");
610        if (familyList != null) {
611
612            List<Element> l = familyList.getChildren("family");
613            log.trace("readFamilySection sees {} children", l.size());
614            for (Element el : l) {
615                // handle each entry
616                readFamily(el);
617            }
618        } else {
619            log.debug("no familyList found in decoderIndexFile");
620        }
621    }
622
623    void readFamily(Element family) {
624        Attribute attr;
625        String filename = family.getAttribute("file").getValue();
626        String parentLowVersID = ((attr = family.getAttribute(LOW_VERSION_ID)) != null ? attr.getValue() : null);
627        String parentHighVersID = ((attr = family.getAttribute(HIGH_VERSION_ID)) != null ? attr.getValue() : null);
628        String ParentReplacementFamilyName = ((attr = family.getAttribute("replacementFamily")) != null ? attr.getValue() : null);
629        String familyName = ((attr = family.getAttribute("name")) != null ? attr.getValue() : null);
630        String mfg = ((attr = family.getAttribute("mfg")) != null ? attr.getValue() : null);
631        String developerID = ((attr = family.getAttribute("developerID")) != null ? attr.getValue() : null);
632        String manufacturerID = ((attr = family.getAttribute("manufacturerID")) != null ? attr.getValue() : null);
633        String productID = ((attr = family.getAttribute("productID")) != null ? attr.getValue() : null);
634        String mfgID = null;
635        if (mfg != null) {
636            mfgID = mfgIdFromName(mfg);
637        } else {
638            log.error("Did not find required mfg attribute, may not find proper manufacturer");
639        }
640
641        // extract <programming> modes of a family's parent <decoder> element
642        String modes = ((attr = family.getAttribute("modes")) != null ? attr.getValue() : null);
643
644        List<Element> l = family.getChildren("model");
645        log.trace("readFamily sees {} children", l.size());
646        Element modelElement;
647        if (l.isEmpty()) {
648            log.error("Did not find at least one model in the {} family", familyName);
649            modelElement = null;
650        } else {
651            modelElement = l.get(0);
652        }
653
654        // Record the family as a specific model, which allows you to select the
655        // family as a possible thing to program
656        DecoderFile vFamilyDecoderFile
657                = new DecoderFile(mfg, mfgID, familyName,
658                        parentLowVersID, parentHighVersID,
659                        familyName,
660                        filename,
661                        (developerID != null) ? developerID : "-1",
662                        (manufacturerID != null) ? manufacturerID : "-1",
663                        (productID != null) ? productID : "-1",
664                        -1, -1, modelElement,
665                        ParentReplacementFamilyName, ParentReplacementFamilyName,
666                        modes); // numFns, numOuts, XML element equal
667        // add family model as the first decoder
668        decoderList.add(vFamilyDecoderFile);
669
670        // record each of the decoders
671        for (Element decoder : l) {
672            // handle each entry by creating a DecoderFile object containing all it knows
673            String loVersID = ((attr = decoder.getAttribute(LOW_VERSION_ID)) != null ? attr.getValue() : parentLowVersID);
674            String hiVersID = ((attr = decoder.getAttribute(HIGH_VERSION_ID)) != null ? attr.getValue() : parentHighVersID);
675            String replacementModelName = ((attr = decoder.getAttribute("replacementModel")) != null ? attr.getValue() : null);
676            String replacementFamilyName = ((attr = decoder.getAttribute("replacementFamily")) != null ? attr.getValue() : ParentReplacementFamilyName);
677            int numFns = ((attr = decoder.getAttribute("numFns")) != null ? Integer.parseInt(attr.getValue()) : -1);
678            int numOuts = ((attr = decoder.getAttribute("numOuts")) != null ? Integer.parseInt(attr.getValue()) : -1);
679            String devId = ((attr = decoder.getAttribute("developerID")) != null ? attr.getValue() : "-1");
680            String manufId = ((attr = decoder.getAttribute("manufacturerID")) != null ? attr.getValue() : "-1");
681            String prodId = ((attr = decoder.getAttribute("productID")) != null ? attr.getValue() : "-1");
682
683            DecoderFile df = new DecoderFile(mfg, mfgID,
684                    ((attr = decoder.getAttribute("model")) != null ? attr.getValue() : null),
685                    loVersID, hiVersID, familyName, filename, devId, manufId, prodId, numFns, numOuts, decoder,
686                    replacementModelName, replacementFamilyName, modes);
687            // and store it
688            decoderList.add(df);
689            // if there are additional version numbers defined, handle them too
690            List<Element> vcodes = decoder.getChildren("versionCV");
691            for (Element vcv : vcodes) {
692                // for each versionCV element
693                String vLoVersID = ((attr = vcv.getAttribute(LOW_VERSION_ID)) != null ? attr.getValue() : loVersID);
694                String vHiVersID = ((attr = vcv.getAttribute(HIGH_VERSION_ID)) != null ? attr.getValue() : hiVersID);
695                df.setVersionRange(vLoVersID, vHiVersID);
696            }
697        }
698    }
699
700    /**
701     * Check if target string is in a comma-delimited string
702     * <p>
703     * Example:
704     *      findString = "47"
705     *      inString = "1,4,53,97"
706     *      return value is 'false'
707     * <p>
708     * Example:
709     *      findString = "47"
710     *      inString = "1,31,47,51"
711     *      return value is 'true'
712     * <p>
713     * Example:
714     *      findString = "47"
715     *      inString = "47"
716     *      return value is true
717     *
718     * @param findString string to find
719     * @param inString comma-delimited string of sub-strings
720     * @return true if target string is found as sub-string within comma-
721     *      delimited string
722     */
723    public boolean checkInCommaDelimString(String findString, String inString) {
724        String bracketedFindString = ","+findString+",";
725        String bracketedInString = ","+inString+",";
726        return bracketedInString.contains(bracketedFindString);
727    }
728
729    /**
730     * Build and write the decoder index file, based on a set of decoder files.
731     * <p>
732     * This creates the full DOM object for the decoder index based on reading the
733     * supplied decoder xml files. It then saves the decoder index out to a new file.
734     *
735     * @param name name of the new index file
736     * @param oldIndex old decoder index file
737     * @param files array of files to read for new index
738     * @param pane optional JOptionPane to check for cancellation
739     * @param pb optional JProgressBar to update during operations
740     * @throws IOException for errors writing the decoder index file
741     */
742    public void writeFile(String name, DecoderIndexFile oldIndex,
743                          String[] files, JOptionPane pane, JProgressBar pb) throws IOException {
744        log.debug("writeFile {}",name);
745
746        // This is taken in large part from "Java and XML" page 368
747        File file = new File(FileUtil.getUserFilesPath() + name);
748
749        // create root element and document
750        Element root = new Element("decoderIndex-config");
751        root.setAttribute("noNamespaceSchemaLocation",
752                "http://jmri.org/xml/schema/decoder-4-15-2.xsd",
753                org.jdom2.Namespace.getNamespace("xsi",
754                        "http://www.w3.org/2001/XMLSchema-instance"));
755
756        Document doc = newDocument(root);
757
758        // add XSLT processing instruction
759        // <?xml-stylesheet type="text/xsl" href="XSLT/DecoderID.xsl"?>
760        java.util.Map<String, String> m = new java.util.HashMap<>();
761        m.put("type", "text/xsl");
762        m.put("href", xsltLocation + "DecoderID.xsl");
763        ProcessingInstruction p = new ProcessingInstruction("xml-stylesheet", m);
764        doc.addContent(0, p);
765
766        // add top-level elements
767        Element index;
768        root.addContent(index = new Element(DECODER_INDEX));
769        index.setAttribute(VERSION, Integer.toString(fileVersion));
770        log.debug("version written to file as {}", fileVersion);
771
772        // add mfg list from existing DecoderIndexFile item
773        Element mfgList = new Element("mfgList");
774        // copy dates from original mfgList element
775        if (oldIndex.nmraListDate != null) {
776            mfgList.setAttribute("nmraListDate", oldIndex.nmraListDate);
777        }
778        if (oldIndex.updated != null) {
779            mfgList.setAttribute("updated", oldIndex.updated);
780        }
781        if (oldIndex.lastAdd != null) {
782            mfgList.setAttribute("lastadd", oldIndex.lastAdd);
783        }
784
785        // We treat "NMRA" special...
786        Element mfg = new Element(MANUFACTURER);
787        mfg.setAttribute("mfg", "NMRA");
788        mfg.setAttribute(MFG_ID, "999");
789        mfgList.addContent(mfg);
790        // start working on the rest of the entries
791        List<String> keys = new ArrayList<>(oldIndex._mfgIdFromNameHash.keySet());
792        Collections.sort(keys);
793        for (Object item : keys) {
794            String mfgName = (String) item;
795            if (!mfgName.equals("NMRA")) {
796                mfg = new Element(MANUFACTURER);
797                mfg.setAttribute("mfg", mfgName);
798                mfg.setAttribute(MFG_ID, oldIndex._mfgIdFromNameHash.get(mfgName));
799                mfgList.addContent(mfg);
800            }
801        }
802
803        // add family list by scanning files
804        Element familyList = new Element("familyList");
805        int fileNum = 0;
806        for (String fileName : files) {
807            // update progress monitor, if passed in
808            if (pb != null) {
809                pb.setValue(fileNum++);
810            }
811            if (pane != null && pane.getValue() != JOptionPane.UNINITIALIZED_VALUE) {
812                log.info("Decoder index recreation cancelled");
813                return;
814            }
815            DecoderFile d = new DecoderFile();
816            try {
817                // get <family> element and add the file name
818                Element droot = d.rootFromName(DecoderFile.fileLocation + fileName);
819                Element family = droot.getChild("decoder").getChild("family").clone();
820                // get decoder element's child programming and copy the mode children
821                Element prog = droot.getChild("decoder").getChild("programming");
822                if (prog != null) {
823                    List<Element> modes = prog.getChildren("mode");
824                    if (modes != null) {
825                        StringBuilder supportedModes = new StringBuilder();
826                        for (Element md : modes) { // typically only 1 mode element in a definition
827                            String modeName = md.getText(); // example: LOCONETLNCVMODE
828                            if (supportedModes.length() > 0) supportedModes.append(",");
829                            supportedModes.append(modeName);
830                        }
831                        if (supportedModes.length() > 0) {
832                            family.setAttribute("modes", supportedModes.toString());
833                        }
834                    }
835                }
836                family.setAttribute("file", fileName);
837
838                // drop the decoder implementation content
839                // comment is kept, so it displays
840                // don't remove "outputs" due to use by ESU function map pane
841                // family.removeChildren("output");
842                // family.removeChildren("functionlabels");
843
844                // and drop content of model elements
845                for (Element element : family.getChildren()) { // model elements
846                    element.removeAttribute("maxInputVolts");
847                    element.removeAttribute("maxMotorCurrent");
848                    element.removeAttribute("maxTotalCurrent");
849                    element.removeAttribute("formFactor");
850                    element.removeAttribute("connector");
851                    // comment is kept so it displays
852                    element.removeAttribute("nmraWarrant");
853                    element.removeAttribute("nmraWarrantStart");
854
855                    // element.removeContent();
856                    element.removeChildren("size");
857
858                    //element.removeChildren("functionlabels");
859
860                    // don't remove "output" due to use by ESU function map pane
861                    for (Element output : element.getChildren()) {
862                        output.removeAttribute("connection");
863                        output.removeAttribute("maxcurrent");
864                        output.removeChildren("label");
865                    }
866                }
867
868                // and store to output
869                familyList.addContent(family);
870            } catch (JDOMException exj) {
871                log.error("could not parse {}: {}", fileName, exj.getMessage());
872            } catch (FileNotFoundException exj) {
873                log.error("could not read {}: {}", fileName, exj.getMessage());
874            } catch (IOException exj) {
875                log.error("other exception while dealing with {}: {}", fileName, exj.getMessage());
876            }
877        }
878
879        index.addContent(new Comment("The manufacturer list is from the nmra_mfg_list.xml file"));
880        index.addContent(mfgList);
881        index.addContent(familyList);
882
883        log.debug("Writing decoderIndex");
884        try {
885            writeXML(file, doc);
886        } catch (Exception e) {
887            log.error("Error writing file: {}", file, e);
888        }
889
890        // force a read of the new file next time
891        resetInstance();
892    }
893
894    String nmraListDate = null;
895    String updated = null;
896    String lastAdd = null;
897
898    /**
899     * Get the filename for the default decoder index file, including location.
900     * This is here to allow easy override in tests.
901     *
902     * @return the complete path to the decoder index
903     */
904    protected static String defaultDecoderIndexFilename() {
905        return DECODER_INDEX_FILE_NAME;
906    }
907
908    protected static final String DECODER_INDEX_FILE_NAME = "decoderIndex.xml";
909    private static final Logger log = LoggerFactory.getLogger(DecoderIndexFile.class);
910
911    @ServiceProvider(service = InstanceInitializer.class)
912    public static class Initializer extends AbstractInstanceInitializer {
913
914        @Override
915        @Nonnull
916        public <T> Object getDefault(Class<T> type) {
917            if (type.equals(DecoderIndexFile.class)) {
918                // create and load
919                DecoderIndexFile instance = new DecoderIndexFile();
920                log.debug("DecoderIndexFile creating instance");
921                try {
922                    instance.readFile(defaultDecoderIndexFilename());
923                } catch (IOException | JDOMException e) {
924                    log.error("Exception during decoder index reading: ", e);
925                }
926                // see if needs to be updated
927                try {
928                    if (updateIndexIfNeeded()) {
929                        try {
930                            instance = new DecoderIndexFile();
931                            instance.readFile(defaultDecoderIndexFilename());
932                        } catch (IOException | JDOMException e) {
933                            log.error("Exception during decoder index reload: ", e);
934                        }
935                    }
936                } catch (IOException | JDOMException e) {
937                    log.error("Exception during decoder index update: ", e);
938                }
939                log.debug("DecoderIndexFile returns instance {}", instance);
940                return instance;
941            }
942            return super.getDefault(type);
943        }
944
945        @Override
946        @Nonnull
947        public Set<Class<?>> getInitalizes() {
948            Set<Class<?>> set = super.getInitalizes();
949            set.add(DecoderIndexFile.class);
950            return set;
951        }
952    }
953
954}