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