001package jmri.jmrit.consisttool;
002
003import java.beans.PropertyChangeEvent;
004import java.beans.PropertyChangeListener;
005import java.io.File;
006import java.io.IOException;
007import java.util.*;
008
009import jmri.Consist;
010import jmri.ConsistManager;
011import jmri.LocoAddress;
012import jmri.DccLocoAddress;
013import jmri.InstanceManager;
014import jmri.jmrit.XmlFile;
015import jmri.jmrit.roster.Roster;
016import jmri.jmrit.roster.RosterConfigManager;
017import jmri.util.FileUtil;
018import org.jdom2.Attribute;
019import org.jdom2.Document;
020import org.jdom2.Element;
021import org.jdom2.JDOMException;
022import org.jdom2.ProcessingInstruction;
023import org.jdom2.filter.ElementFilter;
024import org.slf4j.Logger;
025import org.slf4j.LoggerFactory;
026
027/**
028 * Handle saving/restoring consist information to XML files. This class
029 * manipulates files conforming to the consist-roster-config DTD.
030 *
031 * @author Paul Bender Copyright (C) 2008
032 */
033public class ConsistFile extends XmlFile implements PropertyChangeListener {
034
035    private static final String CONSIST = "consist"; // NOI18N
036    private static final String CONSISTID = "id"; // NOI18N
037    private static final String CONSISTNUMBER = "consistNumber"; // NOI18N
038    private static final String DCCLOCOADDRESS = "dccLocoAddress"; // NOI18N
039    private static final String LONGADDRESS = "longAddress"; // NOI18N
040    private static final String LOCODIR = "locoDir"; // NOI18N
041    private static final String LOCONAME = "locoName"; // NOI18N
042    private static final String LOCOROSTERID = "locoRosterId"; // NOI18N
043    private static final String NORMAL = "normal"; // NOI18N
044    private static final String REVERSE = "reverse"; // NOI18N
045
046    protected ConsistManager consistMan = null;
047
048    public ConsistFile() {
049        super();
050        consistMan = InstanceManager.getDefault(jmri.ConsistManager.class);
051        Roster.getDefault().addPropertyChangeListener(this);
052    }
053
054    /**
055     * Load a Consist from the consist elements in the file.
056     *
057     * @param consist a JDOM element containing a consist
058     */
059    private void consistFromXml(Element consist) {
060        Attribute cnumber;
061        Attribute isCLong;
062        Consist newConsist;
063
064        // Read the consist address from the file and create the
065        // consisit in memory if it doesn't exist already.
066        cnumber = consist.getAttribute(CONSISTNUMBER);
067        isCLong = consist.getAttribute(LONGADDRESS);
068        DccLocoAddress consistAddress;
069        if (isCLong != null) {
070            log.debug("adding consist {} with longAddress set to {}.", cnumber, isCLong.getValue());
071            try {
072                int number = Integer.parseInt(cnumber.getValue());
073                consistAddress = new DccLocoAddress(number, isCLong.getValue().equals("yes"));
074            } catch (NumberFormatException e) {
075                log.debug("Consist number not an integer");
076                return;
077            }
078
079        } else {
080            log.debug("adding consist {} with default long address setting.", cnumber);
081            consistAddress = new DccLocoAddress(Integer.parseInt(cnumber.getValue()), false);
082        }
083        newConsist = consistMan.getConsist(consistAddress);
084        if (!(newConsist.getConsistList().isEmpty())) {
085            log.debug("Consist {} is not empty.  Using version in memory.", consistAddress);
086            return;
087        }
088
089        readConsistType(consist, newConsist);
090        readConsistId(consist, newConsist);
091        readConsistLocoList(consist,newConsist);
092        consistMan.notifyConsistListChanged();
093    }
094
095    public void readConsistLocoList(Element consist, Consist newConsist) {
096        // read each child of locomotive in the consist from the file
097        // and restore it's information to memory.
098        Iterator<Element> childIterator = consist.getDescendants(new ElementFilter("loco"));
099        try {
100            Element e;
101            do {
102                e = childIterator.next();
103                Attribute number = e.getAttribute(DCCLOCOADDRESS);
104                log.debug("adding Loco {}", number);
105                DccLocoAddress address = readLocoAddress(e);
106
107                Attribute direction = e.getAttribute(LOCODIR);
108                boolean directionNormal = false;
109                if (direction != null) {
110                    // use the values from the file
111                    log.debug("using direction from file {}", direction.getValue());
112                    directionNormal = direction.getValue().equals(NORMAL);
113                } else {
114                    // use default, normal direction
115                    directionNormal = true;
116                }
117                // Use restore so we DO NOT cause send any commands
118                // to the command station as we recreate the consist.
119                newConsist.restore(address,directionNormal);
120                readLocoPosition(e,address,newConsist);
121                Attribute rosterId = e.getAttribute(LOCOROSTERID);
122                if (rosterId != null) {
123                    newConsist.setRosterId(address, rosterId.getValue());
124                }
125            } while (true);
126        } catch (NoSuchElementException nse) {
127            log.debug("end of loco list");
128        }
129    }
130
131    private void readConsistType(Element consist, Consist newConsist){
132        // read and set the consist type
133        Attribute type = consist.getAttribute("type");
134        if (type != null) {
135            // use the value read from the file
136            newConsist.setConsistType((type.getValue().equals("CSAC")) ? Consist.CS_CONSIST : Consist.ADVANCED_CONSIST);
137        } else {
138            // use the default (DAC)
139            newConsist.setConsistType(Consist.ADVANCED_CONSIST);
140        }
141    }
142
143    private void readConsistId(Element consist,Consist newConsist){
144        // Read the consist ID from the file
145        Attribute cID = consist.getAttribute(CONSISTID);
146        if (cID != null) {
147            // use the value read from the file
148            newConsist.setConsistID(cID.getValue());
149        }
150    }
151
152    private void readLocoPosition(Element loco,DccLocoAddress address, Consist newConsist){
153        Attribute position = loco.getAttribute(LOCONAME);
154        if (position != null && !position.getValue().equals("mid")) {
155            if (position.getValue().equals("lead")) {
156                newConsist.setPosition(address, Consist.POSITION_LEAD);
157            } else if (position.getValue().equals("rear")) {
158                newConsist.setPosition(address, Consist.POSITION_TRAIL);
159            }
160        } else {
161            Attribute midNumber = loco.getAttribute("locoMidNumber");
162            if (midNumber != null) {
163                int pos = Integer.parseInt(midNumber.getValue());
164                newConsist.setPosition(address, pos);
165            }
166        }
167    }
168
169    private DccLocoAddress readLocoAddress(Element loco){
170        DccLocoAddress address;
171        Attribute number = loco.getAttribute(DCCLOCOADDRESS);
172        Attribute isLong = loco.getAttribute(LONGADDRESS);
173        if (isLong != null ) {
174            // use the values from the file
175            address = new DccLocoAddress(
176                    Integer.parseInt(number.getValue()),
177                    isLong.getValue().equals("yes"));
178        } else {
179            // set as long address
180            address = new DccLocoAddress(
181                    Integer.parseInt(number.getValue()),
182                    true);
183        }
184
185        return address;
186    }
187
188    /**
189     * convert a Consist to XML.
190     *
191     * @param consist a Consist object to write to the file
192     * @return an Element representing the consist.
193     */
194    private Element consistToXml(Consist consist) {
195        Element e = new Element(CONSIST);
196        e.setAttribute(CONSISTID, consist.getConsistID());
197        e.setAttribute(CONSISTNUMBER, "" + consist.getConsistAddress()
198                .getNumber());
199        e.setAttribute(LONGADDRESS, consist.getConsistAddress()
200                .isLongAddress() ? "yes" : "no");
201        e.setAttribute("type", consist.getConsistType() == Consist.ADVANCED_CONSIST ? "DAC" : "CSAC");
202        ArrayList<DccLocoAddress> addressList = consist.getConsistList();
203
204        for (int i = 0; i < addressList.size(); i++) {
205            DccLocoAddress locoaddress = addressList.get(i);
206            Element eng = new Element("loco");
207            eng.setAttribute(DCCLOCOADDRESS, "" + locoaddress.getNumber());
208            eng.setAttribute(LONGADDRESS, locoaddress.isLongAddress() ? "yes" : "no");
209            eng.setAttribute(LOCODIR, consist.getLocoDirection(locoaddress) ? NORMAL : REVERSE);
210            int position = consist.getPosition(locoaddress);
211            switch (position) {
212                case Consist.POSITION_LEAD:
213                    eng.setAttribute(LOCONAME, "lead");
214                    break;
215                case Consist.POSITION_TRAIL:
216                    eng.setAttribute(LOCONAME, "rear");
217                    break;
218                default:
219                    eng.setAttribute(LOCONAME, "mid");
220                    eng.setAttribute("locoMidNumber", "" + position);
221                    break;
222            }
223            String rosterId = consist.getRosterId(locoaddress);
224            if (rosterId != null) {
225                eng.setAttribute(LOCOROSTERID, rosterId);
226            }
227            e.addContent(eng);
228        }
229        return (e);
230    }
231
232    /**
233     * Read all consists from the default file name.
234     *
235     * @throws org.jdom2.JDOMException if unable to parse consists
236     * @throws java.io.IOException     if unable to read file
237     */
238    public void readFile() throws JDOMException, IOException {
239        readFile(defaultConsistFilename());
240    }
241
242    /**
243     * Read all consists from a file.
244     *
245     * @param fileName path to file
246     * @throws org.jdom2.JDOMException if unable to parse consists
247     * @throws java.io.IOException     if unable to read file
248     */
249    public void readFile(String fileName) throws JDOMException, IOException {
250        if (checkFile(fileName)) {
251            Element root = rootFromName(fileName);
252            Element roster;
253            if (root == null) {
254                log.warn("consist file could not be read");
255                return;
256            }
257            roster = root.getChild("roster");
258            if (roster == null) {
259                log.debug("consist file does not contain a roster entry");
260                return;
261            }
262            Iterator<Element> consistIterator = root.getDescendants(new ElementFilter(CONSIST));
263            try {
264                Element consist;
265                do {
266                    consist = consistIterator.next();
267                    consistFromXml(consist);
268                } while (consistIterator.hasNext());
269            } catch (NoSuchElementException nde) {
270                log.debug("end of consist list");
271            }
272        } else {
273            log.info("Consist file does not exist.  One will be created if necessary.");
274        }
275
276    }
277
278    /**
279     * Write all consists to the default file name.
280     *
281     * @param consistList list of consist addresses
282     * @throws java.io.IOException if unable to write file
283     */
284    public void writeFile(List<LocoAddress> consistList) throws IOException {
285        writeFile(consistList, defaultConsistFilename());
286    }
287
288    /**
289     * Write all consists to a file.
290     *
291     * @param consistList list of consist addresses
292     * @param fileName    path to file
293     * @throws java.io.IOException if unable to write file
294     */
295    public void writeFile(List<LocoAddress> consistList, String fileName) throws IOException {
296        // create root element
297        Element root = new Element("consist-roster-config");
298        Document doc = newDocument(root, dtdLocation + "consist-roster-config.dtd");
299
300        // add XSLT processing instruction
301        Map<String, String> m = new HashMap<>();
302        m.put("type", "text/xsl");
303        m.put("href", xsltLocation + "consistRoster.xsl");
304        ProcessingInstruction p = new ProcessingInstruction("xml-stylesheet", m);
305        doc.addContent(0, p);
306
307        Element roster = new Element("roster");
308
309        for (int i = 0; i < consistList.size(); i++) {
310            Consist newConsist = consistMan.getConsist(consistList.get(i));
311            roster.addContent(consistToXml(newConsist));
312        }
313        root.addContent(roster);
314        if (!checkFile(fileName)) {
315            //The file does not exist, create it before writing
316            File file = new File(fileName);
317            // verify the directory exists.
318            File parentDir = file.getParentFile();
319            FileUtil.createDirectory(parentDir);
320            if (!file.createNewFile()) {
321                throw (new IOException());
322            }
323        }
324        writeXML(findFile(fileName), doc);
325    }
326
327    /**
328     * GetFile Location.
329     *
330     * @return the preferences subdirectory in which Consist Files are kept
331     * this is relative to the roster files location.
332     */
333    public static String getFileLocation() {
334        return Roster.getDefault().getRosterFilesLocation() + CONSIST + File.separator;
335    }
336
337    /**
338     * Get the filename for the default Consist file, including location.
339     *
340     * @return the filename
341     */
342    public static String defaultConsistFilename() {
343        return getFileLocation() + "consist.xml";
344    }
345
346    /**
347     * {@inheritDoc}
348     */
349    @Override
350    public void propertyChange(PropertyChangeEvent evt) {
351        if (evt.getSource() instanceof Roster &&
352            evt.getPropertyName().equals(RosterConfigManager.DIRECTORY)) {
353            try {
354                this.writeFile(consistMan.getConsistList());
355            } catch (IOException ioe) {
356                log.error("Unable to write consist information to new consist folder");
357            }
358        }
359    }
360
361    // initialize logging
362    private static final Logger log = LoggerFactory.getLogger(ConsistFile.class);
363}