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