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