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