001package jmri.jmrix.nce.consist;
002
003import java.io.File;
004import java.io.IOException;
005import java.util.ArrayList;
006import java.util.List;
007import javax.swing.JComboBox;
008import jmri.InstanceManagerAutoDefault;
009import jmri.InstanceManagerAutoInitialize;
010import jmri.jmrit.XmlFile;
011import jmri.jmrit.roster.Roster;
012import org.jdom2.Document;
013import org.jdom2.Element;
014import org.jdom2.JDOMException;
015import org.jdom2.ProcessingInstruction;
016import org.slf4j.Logger;
017import org.slf4j.LoggerFactory;
018
019/**
020 * NCE Consist Roster manages and manipulates a roster of consists.
021 * <p>
022 * It works with the "consist-roster-config" XML DTD to load and store its
023 * information.
024 * <p>
025 * This is an in-memory representation of the roster xml file (see below for
026 * constants defining name and location). As such, this class is also
027 * responsible for the "dirty bit" handling to ensure it gets written. As a
028 * temporary reliability enhancement, all changes to this structure are now
029 * being written to a backup file, and a copy is made when the file is opened.
030 * <p>
031 * Multiple Roster objects don't make sense, so we use an "instance" member to
032 * navigate to a single one.
033 * <p>
034 * This predates the "XmlFile" base class, so doesn't use it. Not sure whether
035 * it should...
036 * <p>
037 * The only bound property is the list of s; a PropertyChangedEvent is fired
038 * every time that changes.
039 * <p>
040 * The entries are stored in an ArrayList, sorted alphabetically. That sort is
041 * done manually each time an entry is added.
042 *
043 * @author Bob Jacobsen Copyright (C) 2001; Dennis Miller Copyright 2004
044 * @author Daniel Boudreau (C) 2008
045 * @see NceConsistRosterEntry
046 */
047public class NceConsistRoster extends XmlFile implements InstanceManagerAutoDefault, InstanceManagerAutoInitialize {
048
049    public NceConsistRoster() {
050    }
051
052    /**
053     * Add a RosterEntry object to the in-memory Roster.
054     *
055     * @param e Entry to add
056     */
057    public void addEntry(NceConsistRosterEntry e) {
058        log.debug("Add entry {}", e);
059        int i = _list.size() - 1;// Last valid index
060        while (i >= 0) {
061            if (e.getId().compareTo(_list.get(i).getId())> 0) {
062                break; // I can never remember whether I want break or continue here
063            }
064            i--;
065        }
066        _list.add(i + 1, e);
067        setDirty(true);
068        firePropertyChange("add", null, e);
069    }
070
071    /**
072     * Remove a RosterEntry object from the in-memory Roster. This does not
073     * delete the file for the RosterEntry!
074     *
075     * @param e Entry to remove
076     */
077    public void removeEntry(NceConsistRosterEntry e) {
078        log.debug("Remove entry {}", e);
079        _list.remove(e);
080        setDirty(true);
081        firePropertyChange("remove", null, e);
082    }
083
084    /**
085     * @return Number of entries in the Roster
086     */
087    public int numEntries() {
088        return _list.size();
089    }
090
091    /**
092     * Return a combo box containing the entire ConsistRoster.
093     * <p>
094     * This is based on a single model, so it can be updated when the
095     * ConsistRoster changes.
096     * @return combo box of whole roster
097     *
098     */
099    public JComboBox<String> fullRosterComboBox() {
100        return matchingComboBox(null, null, null, null,
101                null, null, null, null, null,
102                null);
103    }
104
105    /**
106     * Get a JComboBox representing the choices that match. There's 10 elements.
107     * @param roadName value to match against roster roadname field
108     * @param roadNumber value to match against roster roadnumber field
109     * @param consistNumber value to match against roster consist number field
110     * @param eng1Address value to match against roster 1st engine address field
111     * @param eng2Address value to match against roster 2nd engine address field
112     * @param eng3Address value to match against roster 3rd engine address field
113     * @param eng4Address value to match against roster 4th engine address field
114     * @param eng5Address value to match against roster 5th engine address field
115     * @param eng6Address value to match against roster 6th engine address field
116     * @param id value to match against roster id field
117     * @return combo box of matching roster entries
118     */
119    public JComboBox<String> matchingComboBox(String roadName, String roadNumber,
120            String consistNumber, String eng1Address, String eng2Address,
121            String eng3Address, String eng4Address, String eng5Address,
122            String eng6Address, String id) {
123        List<NceConsistRosterEntry> l = matchingList(roadName, roadNumber, consistNumber, eng1Address,
124                eng2Address, eng3Address, eng4Address, eng5Address,
125                eng6Address, id);
126        JComboBox<String> b = new JComboBox<>();
127        for (int i = 0; i < l.size(); i++) {
128            NceConsistRosterEntry r = _list.get(i);
129            b.addItem(r.titleString());
130        }
131        return b;
132    }
133
134    public void updateComboBox(JComboBox<String> box) {
135        List<NceConsistRosterEntry> l = matchingList(null, null, null,
136                null, null, null, null, null,
137                null, null);
138        box.removeAllItems();
139        for (int i = 0; i < l.size(); i++) {
140            NceConsistRosterEntry r = _list.get(i);
141            box.addItem(r.titleString());
142        }
143    }
144
145    /**
146     * Return RosterEntry from a "title" string, ala selection in
147     * matchingComboBox
148     * @param title title to search for in consist roster
149     * @return matching consist roster entry
150     */
151    public NceConsistRosterEntry entryFromTitle(String title) {
152        for (int i = 0; i < numEntries(); i++) {
153            NceConsistRosterEntry r = _list.get(i);
154            if (r.titleString().equals(title)) {
155                return r;
156            }
157        }
158        return null;
159    }
160
161    /**
162     * List of contained RosterEntry elements.
163     */
164    protected List<NceConsistRosterEntry> _list = new ArrayList<>();
165
166    /**
167     * Get a List of entries matching some information. The list may have null
168     * contents.
169     * @param roadName value to match against roster roadname field
170     * @param roadNumber value to match against roster roadnumber field
171     * @param consistNumber value to match against roster consist number field
172     * @param eng1Address value to match against roster 1st engine address field
173     * @param eng2Address value to match against roster 2nd engine address field
174     * @param eng3Address value to match against roster 3rd engine address field
175     * @param eng4Address value to match against roster 4th engine address field
176     * @param eng5Address value to match against roster 5th engine address field
177     * @param eng6Address value to match against roster 6th engine address field
178     * @param id value to match against roster id field
179     * @return list of consist roster entries matching request
180     */
181    public List<NceConsistRosterEntry> matchingList(String roadName, String roadNumber,
182            String consistNumber, String eng1Address, String eng2Address,
183            String eng3Address, String eng4Address, String eng5Address,
184            String eng6Address, String id) {
185        List<NceConsistRosterEntry> l = new ArrayList<>();
186        for (int i = 0; i < numEntries(); i++) {
187            if (checkEntry(i, roadName, roadNumber, consistNumber, eng1Address,
188                    eng2Address, eng3Address, eng4Address, eng5Address,
189                    eng6Address, id)) {
190                l.add(_list.get(i));
191            }
192        }
193        return l;
194    }
195
196    /**
197     * Check if an entry consistent with specific properties. A null String
198     * entry always matches. Strings are used for convenience in GUI building.
199     * @param i index to consist roster entry
200     * @param roadName value to match against roster roadname field
201     * @param roadNumber value to match against roster roadnumber field
202     * @param consistNumber value to match against roster consist number field
203     * @param loco1Address value to match against roster 1st engine address field
204     * @param loco2Address value to match against roster 2nd engine address field
205     * @param loco3Address value to match against roster 3rd engine address field
206     * @param loco4Address value to match against roster 4th engine address field
207     * @param loco5Address value to match against roster 5th engine address field
208     * @param loco6Address value to match against roster 6th engine address field
209     * @param id value to match against roster id field
210     * @return true if values provided matches indexed entry
211     */
212    public boolean checkEntry(int i, String roadName, String roadNumber,
213            String consistNumber, String loco1Address, String loco2Address,
214            String loco3Address, String loco4Address, String loco5Address,
215            String loco6Address, String id) {
216        NceConsistRosterEntry r = _list.get(i);
217        if (id != null && !id.equals(r.getId())) {
218            return false;
219        }
220        if (roadName != null && !roadName.equals(r.getRoadName())) {
221            return false;
222        }
223        if (roadNumber != null && !roadNumber.equals(r.getRoadNumber())) {
224            return false;
225        }
226        if (consistNumber != null && !consistNumber.equals(r.getConsistNumber())) {
227            return false;
228        }
229        if (loco1Address != null && !loco1Address.equals(r.getLoco1DccAddress())) {
230            return false;
231        }
232        if (loco2Address != null && !loco2Address.equals(r.getLoco2DccAddress())) {
233            return false;
234        }
235        if (loco3Address != null && !loco3Address.equals(r.getLoco3DccAddress())) {
236            return false;
237        }
238        if (loco4Address != null && !loco4Address.equals(r.getLoco4DccAddress())) {
239            return false;
240        }
241        if (loco5Address != null && !loco5Address.equals(r.getLoco5DccAddress())) {
242            return false;
243        }
244        if (loco6Address != null && !loco6Address.equals(r.getLoco6DccAddress())) {
245            return false;
246        }
247        return true;
248    }
249
250    /**
251     * Write the entire roster to a file. This does not do backup; that has to
252     * be done separately. See writeRosterFile() for a function that finds the
253     * default location, does a backup and then calls this.
254     *
255     * @param name Filename for new file, including path info as needed.
256     * @throws java.io.FileNotFoundException when file not found
257     * @throws java.io.IOException when fault accessing file
258     */
259    void writeFile(String name) throws java.io.FileNotFoundException, java.io.IOException {
260        log.debug("writeFile {}", name);
261        // This is taken in large part from "Java and XML" page 368
262        File file = findFile(name);
263        if (file == null) {
264            file = new File(name);
265        }
266        // create root element
267        Element root = new Element("consist-roster-config");
268        Document doc = newDocument(root, dtdLocation + "consist-roster-config.dtd");
269
270        // add XSLT processing instruction
271        java.util.Map<String, String> m = new java.util.HashMap<>();
272        m.put("type", "text/xsl");
273        m.put("href", xsltLocation + "consistRoster.xsl");
274        ProcessingInstruction p = new ProcessingInstruction("xml-stylesheet", m);
275        doc.addContent(0, p);
276
277        //Check the Comment and Decoder Comment fields for line breaks and
278        //convert them to a processor directive for storage in XML
279        //Note: this is also done in the LocoFile.java class to do
280        //the same thing in the indidvidual locomotive roster files
281        //Note: these changes have to be undone after writing the file
282        //since the memory version of the roster is being changed to the
283        //file version for writing
284        for (int i = 0; i < numEntries(); i++) {
285
286            //Extract the RosterEntry at this index and inspect the Comment and
287            //Decoder Comment fields to change any \n characters to <?p?> processor
288            //directives so they can be stored in the xml file and converted
289            //back when the file is read.
290            NceConsistRosterEntry r = _list.get(i);
291            String tempComment = r.getComment();
292            StringBuilder buf = new StringBuilder();
293
294            //transfer tempComment to xmlComment one character at a time, except
295            //when \n is found.  In that case, insert <?p?>
296            for (int k = 0; k < tempComment.length(); k++) {
297                if (tempComment.startsWith("\n", k)) {
298                    buf.append("<?p?>");
299                } else {
300                    buf.append(tempComment.charAt(k));
301                }
302            }
303            r.setComment(buf.toString());
304        }
305        // All Comments and Decoder Comment line feeds have been changed to processor directives
306
307        // add top-level elements
308        Element values;
309        root.addContent(values = new Element("roster"));
310        // add entries
311        for (int i = 0; i < numEntries(); i++) {
312            values.addContent(_list.get(i).store());
313        }
314        writeXML(file, doc);
315
316        //Now that the roster has been rewritten in file form we need to
317        //restore the RosterEntry object to its normal \n state for the
318        //Comment and Decoder comment fields, otherwise it can cause problems in
319        //other parts of the program (e.g. in copying a roster)
320        for (int i = 0; i < numEntries(); i++) {
321            NceConsistRosterEntry r = _list.get(i);
322            String xmlComment = r.getComment();
323            StringBuilder buf = new StringBuilder();
324
325            for (int k = 0; k < xmlComment.length(); k++) {
326                if (xmlComment.startsWith("<?p?>", k)) {
327                    buf.append("\n");
328                    k = k + 4;
329                } else {
330                    buf.append(xmlComment.charAt(k));
331                }
332            }
333            r.setComment(buf.toString());
334        }
335
336        // done - roster now stored, so can't be dirty
337        setDirty(false);
338    }
339
340    /**
341     * Read the contents of a roster XML file into this object. Note that this
342     * does not clear any existing entries.
343     * @param name file name for consist roster
344     * @throws org.jdom2.JDOMException other errors
345     * @throws java.io.IOException error accessing file
346     */
347    void readFile(String name) throws org.jdom2.JDOMException, java.io.IOException {
348        // find root
349        Element root = rootFromName(name);
350        if (root == null) {
351            log.debug("ConsistRoster file could not be read");
352            return;
353        }
354        //if (log.isDebugEnabled()) XmlFile.dumpElement(root);
355
356        // decode type, invoke proper processing routine if a decoder file
357        if (root.getChild("roster") != null) {
358            List<Element> l = root.getChild("roster").getChildren("consist");
359            if (log.isDebugEnabled()) {
360                log.debug("readFile sees {} children", l.size());
361            }
362            for (Element element : l) {
363                addEntry(new NceConsistRosterEntry(element));
364            }
365
366            //Scan the object to check the Comment and Decoder Comment fields for
367            //any <?p?> processor directives and change them to back \n characters
368            for (int i = 0; i < numEntries(); i++) {
369                //Get a RosterEntry object for this index
370                NceConsistRosterEntry r = _list.get(i);
371
372                //Extract the Comment field and create a new string for output
373                String tempComment = r.getComment();
374                StringBuilder buf = new StringBuilder();
375
376                //transfer tempComment to xmlComment one character at a time, except
377                //when <?p?> is found.  In that case, insert a \n and skip over those
378                //characters in tempComment.
379                for (int k = 0; k < tempComment.length(); k++) {
380                    if (tempComment.startsWith("<?p?>", k)) {
381                        buf.append("\n");
382                        k = k + 4;
383                    } else {
384                        buf.append(tempComment.charAt(k));
385                    }
386                }
387                r.setComment(buf.toString());
388            }
389
390        } else {
391            log.error("Unrecognized ConsistRoster file contents in file: {}", name); // NOI18N
392        }
393    }
394
395    private boolean dirty = false;
396
397    void setDirty(boolean b) {
398        dirty = b;
399    }
400
401    boolean isDirty() {
402        return dirty;
403    }
404
405    public void dispose() {
406        log.debug("dispose");
407        if (dirty) {
408            log.error("Dispose invoked on dirty ConsistRoster");
409        }
410    }
411
412    /**
413     * Store the roster in the default place, including making a backup if
414     * needed
415     */
416    public void writeRosterFile() {
417        makeBackupFile(defaultNceConsistRosterFilename());
418        try {
419            writeFile(defaultNceConsistRosterFilename());
420        } catch (IOException e) {
421            log.error("Exception while writing the new ConsistRoster file, may not be complete: {}", e.getMessage());
422        }
423    }
424
425    /**
426     * update the in-memory Roster to be consistent with the current roster
427     * file. This removes the existing roster entries!
428     */
429    public void reloadRosterFile() {
430        // clear existing
431        _list.clear();
432        // and read new
433        try {
434            readFile(defaultNceConsistRosterFilename());
435        } catch (IOException | JDOMException e) {
436            log.error("Exception during ConsistRoster reading: {}", e.getMessage()); // NOI18N
437        }
438    }
439
440    /**
441     * Return the filename String for the default ConsistRoster file, including
442     * location.
443     * @return consist roster file name
444     */
445    public static String defaultNceConsistRosterFilename() {
446        return Roster.getDefault().getRosterLocation() + nceConsistRosterFileName;
447    }
448
449    public static void setNceConsistRosterFileName(String name) {
450        nceConsistRosterFileName = name;
451    }
452    private static String nceConsistRosterFileName = "ConsistRoster.xml";
453
454    // since we can't do a "super(this)" in the ctor to inherit from PropertyChangeSupport, we'll
455    // reflect to it.
456    // Note that dispose() doesn't act on these.  Its not clear whether it should...
457    java.beans.PropertyChangeSupport pcs = new java.beans.PropertyChangeSupport(this);
458
459    public synchronized void addPropertyChangeListener(java.beans.PropertyChangeListener l) {
460        pcs.addPropertyChangeListener(l);
461    }
462
463    protected void firePropertyChange(String p, Object old, Object n) {
464        pcs.firePropertyChange(p, old, n);
465    }
466
467    public synchronized void removePropertyChangeListener(java.beans.PropertyChangeListener l) {
468        pcs.removePropertyChangeListener(l);
469    }
470
471    /**
472     * Notify that the ID of an entry has changed. This doesn't actually change
473     * the ConsistRoster per se, but triggers recreation.
474     * @param r consist roster to recreate due to changes
475     */
476    public void entryIdChanged(NceConsistRosterEntry r) {
477        log.debug("EntryIdChanged");
478
479        // order may be wrong! Sort
480        NceConsistRosterEntry[] rarray = new NceConsistRosterEntry[_list.size()];
481        for (int i = 0; i < rarray.length; i++) {
482            rarray[i] = _list.get(i);
483        }
484        jmri.util.StringUtil.sortUpperCase(rarray);
485        for (int i = 0; i < rarray.length; i++) {
486            _list.set(i, rarray[i]);
487        }
488
489        firePropertyChange("change", null, r);
490    }
491
492    @Override
493    public void initialize() {
494        if (checkFile(defaultNceConsistRosterFilename())) {
495            try {
496                readFile(defaultNceConsistRosterFilename());
497            } catch (IOException | JDOMException e) {
498                log.error("Exception during ConsistRoster reading: {}", e.getMessage());
499            }
500        }
501    }
502
503    // initialize logging
504    private final static Logger log = LoggerFactory.getLogger(NceConsistRoster.class);
505
506}