001package jmri.jmrit.roster;
002
003import java.io.File;
004import java.io.IOException;
005import java.util.HashMap;
006import java.util.List;
007import jmri.jmrit.XmlFile;
008import jmri.jmrit.symbolicprog.AbstractValue;
009import jmri.jmrit.symbolicprog.CvTableModel;
010import jmri.jmrit.symbolicprog.CvValue;
011import jmri.jmrit.symbolicprog.VariableTableModel;
012import jmri.jmrit.symbolicprog.VariableValue;
013import org.jdom2.Document;
014import org.jdom2.Element;
015import org.jdom2.ProcessingInstruction;
016import org.slf4j.Logger;
017import org.slf4j.LoggerFactory;
018
019/**
020 * Represents and manipulates a locomotive definition, both as a file and in
021 * memory. The interal storage is a JDOM tree. See locomotive-config.xsd
022 * <p>
023 * This class is intended for use by RosterEntry only; you should not use it
024 * directly. That's why this is not a public class.
025 *
026 * @author Bob Jacobsen Copyright (C) 2001, 2002, 2008
027 * @author Dennis Miller Copyright (C) 2004
028 * @author Howard G. Penny Copyright (C) 2005
029 * @see jmri.jmrit.roster.RosterEntry
030 * @see jmri.jmrit.roster.Roster
031 */
032public class LocoFile extends XmlFile {
033
034    /**
035     * Convert to a canonical text form for ComboBoxes, etc.
036     * @return loco title.
037     */
038    public String titleString() {
039        return "no title form yet";
040    }
041
042    /**
043     * Load a CvTableModel from the locomotive element in the File.
044     *
045     * @param loco    A JDOM Element containing the locomotive definition
046     * @param cvModel An existing CvTableModel object which will have the CVs
047     *                from the loco Element appended. It is intended, but not
048     *                required, that this be empty.
049     * @param mfgID   Decoder manufacturer. Used to check if there's need for special
050     *                treatment.
051     * @param family  Decoder family. Used to check if there's need for special
052     *                treatment.
053     */
054    public static void loadCvModel(Element loco, CvTableModel cvModel, String mfgID, String family) {
055        CvValue cvObject;
056        // get the CVs and load
057        String rosterName = loco.getAttributeValue("id");
058        Element values = loco.getChild("values");
059
060        // Ugly hack because of bug 1898971 in JMRI 2.1.2 - contents may be directly inside the
061        // locomotive element, instead of in a nested values element
062        if (values == null) {
063            // check for non-nested content, in which case use loco element
064            List<Element> elementList = loco.getChildren("CVvalue");
065            if (elementList != null) {
066                values = loco;
067            }
068        }
069
070        if (values != null) {
071            // get the CV values and load
072            if (log.isDebugEnabled()) {
073                log.debug("Found {} CVvalues", values.getChildren("CVvalue").size());
074            }
075
076            for (Element element : values.getChildren("CVvalue")) {
077                // locate the row
078                if (element.getAttribute("name") == null) {
079                    if (log.isDebugEnabled()) {
080                        log.debug("unexpected null in name {} {}", element, element.getAttributes());
081                    }
082                    break;
083                }
084                if (element.getAttribute("value") == null) {
085                    if (log.isDebugEnabled()) {
086                        log.debug("unexpected null in value {} {}", element, element.getAttributes());
087                    }
088                    break;
089                }
090
091                String name = element.getAttribute("name").getValue();
092                String value = element.getAttribute("value").getValue();
093                log.debug("CV named {} has value: {}", name, value);
094
095                // Fairly ugly hack to migrate Indexed CVs of existing Tsunami2 & Econami
096                // roster entries to full NMRA S9.2.2 format (include CV 31 value).
097                if (family != null && (family.startsWith("Tsunami2") || family.startsWith("Econami")) && name.matches("\\d+\\.\\d+")) {
098                    String oldName = name;
099                    name = "16." + oldName;
100                    log.info("CV{} renamed to {} has value: {}", oldName, name, value);
101                }
102
103                // check whether the CV already exists, i.e. due to a variable definition
104                cvObject = cvModel.allCvMap().get(name);
105                if (cvObject == null && name.equals("19")) {
106                    log.info("CV19 special case triggered, kept without Variable; {} {}", mfgID, family);
107                    cvModel.addCV(name, false, false, false);
108                    cvObject = cvModel.allCvMap().get(name);
109                }
110                if (cvObject == null) {
111                    log.trace("undefined CV check with mfgID={} family={}", mfgID, family);
112                    if ( (mfgID != null && mfgID.equals("151")) || (family != null && family.startsWith("ESU ")) ) { // Electronic Solutions Ulm GmbH
113                        // ESU files do not generate CV entries until panel load time
114                        cvModel.addCV(name, false, false, false);
115                        cvObject = cvModel.allCvMap().get(name);
116                     } else {
117                        // this is a valid way to migrate a decoder definition, i.e. to remove a variable.
118                        log.info("CV {} was in '{}' roster file, but not defined by the decoder definition '{}'. '{}'; migrated", 
119                                name, rosterName, mfgID, family);
120                    }
121                }
122                if (cvObject != null) {
123                    cvObject.setValue(Integer.parseInt(value));
124                    cvObject.setState(AbstractValue.ValueState.FROMFILE);
125                }
126            }
127        } else {
128            log.error("no values element found in config file; CVs not configured for ID=\"{}\"", rosterName);
129        }
130
131        // ugly hack - set CV17 back to fromFile if present
132        // this is here because setting CV17, then CV18 seems to set
133        // CV17 to Edited.  This needs to be understood & fixed.
134        cvObject = cvModel.allCvMap().get("17");
135        if (cvObject != null) {
136            cvObject.setState(AbstractValue.ValueState.FROMFILE);
137        }
138    }
139
140    /**
141     * Load a VariableTableModel from the locomotive element in the File
142     *
143     * @param loco    A JDOM Element containing the locomotive definition
144     * @param varModel An existing VariableTableModel object
145     */
146    public static void loadVariableModel(Element loco, VariableTableModel varModel) {
147
148        Element values = loco.getChild("values");
149
150        if (values == null) {
151            log.error("no values element found in config file; Variable values not loaded for \"{}\"", loco.getAttributeValue("id"));
152            return;
153        }
154
155        Element decoderDef = values.getChild("decoderDef");
156
157        if (decoderDef == null) {
158            log.error("no decoderDef element found in config file; Variable values not loaded for \"{}\"", loco.getAttributeValue("id"));
159            return;
160        }
161
162
163        // get the Variable values and load
164        if (log.isDebugEnabled()) {
165            log.debug("Found {} varValue elements", decoderDef.getChildren("varValue").size());
166        }
167
168        // preload an index
169        HashMap<String, VariableValue> map = new HashMap<>();
170        for (int i = 0; i < varModel.getRowCount(); i++) {
171            log.debug("  map put {} to {}", varModel.getItem(i), varModel.getVariable(i));
172            map.put(varModel.getItem(i), varModel.getVariable(i));
173            map.put(varModel.getLabel(i), varModel.getVariable(i));
174        }
175
176        for (Element element : decoderDef.getChildren("varValue")) {
177            // locate the row
178            if (element.getAttribute("item") == null) {
179                if (log.isDebugEnabled()) {
180                    log.debug("unexpected null in item {} {}", element, element.getAttributes());
181                }
182                break;
183            }
184            if (element.getAttribute("value") == null) {
185                if (log.isDebugEnabled()) {
186                    log.debug("unexpected null in value {} {}", element, element.getAttributes());
187                }
188                break;
189            }
190
191            String item = element.getAttribute("item").getValue();
192            String value = element.getAttribute("value").getValue();
193            log.debug("Variable \"{}\" has value: {}", item, value);
194
195            VariableValue var = map.get(item);
196            if (var != null) {
197                var.setValue(value);
198            } else {
199                if (selectMissingVarResponse(item) == MessageResponse.REPORT) {
200                    // not an warning, as this is how some definitions are migrated to remove erroneous variables
201                    log.debug("Did not find locofile variable \"{}\" in decoder definition, no variable loaded", item);
202                }
203            }
204        }
205
206    }
207
208    enum MessageResponse { IGNORE, REPORT }
209
210    /**
211     * Determine if a missing variable in decoder definition should be logged
212     * @param var Name of missing variable
213     * @return Decision on how to handle
214     */
215    protected static MessageResponse selectMissingVarResponse(String var) {
216        if (var.startsWith("ESU Function Row")) return MessageResponse.IGNORE; // from jmri.jmrit.symbolicprog.FnMapPanelESU
217        return MessageResponse.REPORT;
218    }
219
220    /**
221     * Write an XML version of this object, including also the RosterEntry
222     * information, and memory-resident decoder contents.
223     *
224     * Does not do an automatic backup of the file, so that should be done
225     * elsewhere.
226     *
227     * @param file          Destination file. This file is overwritten if it
228     *                      exists.
229     * @param cvModel       provides the CV numbers and contents
230     * @param variableModel provides the variable names and contents
231     * @param r             RosterEntry providing name, etc, information
232     */
233    public void writeFile(File file, CvTableModel cvModel, VariableTableModel variableModel, RosterEntry r) {
234        if (log.isDebugEnabled()) {
235            log.debug("writeFile to {} {}", file.getAbsolutePath(), file.getName());
236        }
237        try {
238            // This is taken in large part from "Java and XML" page 368
239
240            // create root element
241            Element root = new Element("locomotive-config");
242            root.setAttribute("noNamespaceSchemaLocation",
243                    "http://jmri.org/xml/schema/locomotive-config" + Roster.schemaVersion + ".xsd",
244                    org.jdom2.Namespace.getNamespace("xsi",
245                            "http://www.w3.org/2001/XMLSchema-instance"));
246
247            Document doc = newDocument(root);
248
249            // add XSLT processing instruction
250            // <?xml-stylesheet type="text/xsl" href="XSLT/locomotive.xsl"?>
251            java.util.Map<String, String> m = new java.util.HashMap<>();
252            m.put("type", "text/xsl");
253            m.put("href", xsltLocation + "locomotive.xsl");
254            ProcessingInstruction p = new ProcessingInstruction("xml-stylesheet", m);
255            doc.addContent(0, p);
256            // add top-level elements
257            Element locomotive = r.store();   // the locomotive element from the RosterEntry
258
259            root.addContent(locomotive);
260            Element values = new Element("values");
261            locomotive.addContent(values);
262
263            // Append a decoderDef element to values
264            Element decoderDef;
265            values.addContent(decoderDef = new Element("decoderDef"));
266            // add the variable values to the decoderDef Element
267            if (variableModel != null) {
268                for (int i = 0; i < variableModel.getRowCount(); i++) {
269                    decoderDef.addContent(new Element("varValue")
270                            .setAttribute("item", variableModel.getItem(i))
271                            .setAttribute("value", variableModel.getValString(i))
272                    );
273                }
274                // mark file as OK
275                variableModel.setFileDirty(false);
276            }
277
278            // add the CV values to the values Element
279            if (cvModel != null) {
280                for (int i = 0; i < cvModel.getRowCount(); i++) {
281                    values.addContent(new Element("CVvalue")
282                            .setAttribute("name", cvModel.getName(i))
283                            .setAttribute("value", cvModel.getValString(i))
284                    );
285                }
286            }
287
288            writeXML(file, doc);
289
290        } catch (java.io.IOException ex) {
291            log.error("IOException", ex);
292        }
293    }
294
295    /**
296     * Write an XML version of this object from an existing XML tree, updating
297     * only the ID string.
298     *
299     * Does not do an automatic backup of the file, so that should be done
300     * elsewhere. This is intended for copy and import operations, where the
301     * tree has been read from an existing file. Hence, only the "ID"
302     * information in the roster entry is updated. Note that any multi-line
303     * comments are not changed here.
304     *
305     * @param pFile        Destination file. This file is overwritten if it
306     *                     exists.
307     * @param pRootElement Root element of the JDOM tree to write. This should
308     *                     be of type "locomotive-config", and should not be in
309     *                     use elsewhere (clone it first!)
310     * @param pEntry       RosterEntry providing name, etc, information
311     */
312    public void writeFile(File pFile, Element pRootElement, RosterEntry pEntry) {
313        if (log.isDebugEnabled()) {
314            log.debug("writeFile to {} {}", pFile.getAbsolutePath(), pFile.getName());
315        }
316        try {
317            // This is taken in large part from "Java and XML" page 368
318
319            // create root element
320            Document doc = newDocument(pRootElement, dtdLocation + "locomotive-config.dtd");
321
322            // Update the locomotive.id element
323            if (log.isDebugEnabled()) {
324                log.debug("pEntry: {}", pEntry);
325            }
326            pRootElement.getChild("locomotive").getAttribute("id").setValue(pEntry.getId());
327
328            writeXML(pFile, doc);
329        } catch (IOException ex) {
330            log.error("Unable to write {}", pFile, ex);
331        }
332    }
333
334    /**
335     * Write an XML version of this object, updating the RosterEntry
336     * information, from an existing XML tree.
337     *
338     * Does not do an automatic backup of the file, so that should be done
339     * elsewhere. This is intended for writing out changes to the RosterEntry
340     * information only.
341     *
342     * @param pFile           Destination file. This file is overwritten if it
343     *                        exists.
344     * @param existingElement Root element of the existing JDOM tree containing
345     *                        the CV and variable contents
346     * @param newLocomotive   Element from RosterEntry providing name, etc,
347     *                        information
348     */
349    public void writeFile(File pFile, Element existingElement, Element newLocomotive) {
350        if (log.isDebugEnabled()) {
351            log.debug("writeFile to {} {}", pFile.getAbsolutePath(), pFile.getName());
352        }
353        try {
354            // This is taken in large part from "Java and XML" page 368
355
356            // create root element
357            Element root = new Element("locomotive-config");
358            Document doc = newDocument(root, dtdLocation + "locomotive-config.dtd");
359            root.addContent(newLocomotive);
360
361            // add XSLT processing instruction
362            // <?xml-stylesheet type="text/xsl" href="XSLT/locomotive.xsl"?>
363            java.util.Map<String, String> m = new java.util.HashMap<>();
364            m.put("type", "text/xsl");
365            m.put("href", xsltLocation + "locomotive.xsl");
366            ProcessingInstruction p = new ProcessingInstruction("xml-stylesheet", m);
367            doc.addContent(0, p);
368
369            // Add the variable info
370            Element values = existingElement.getChild("locomotive").getChild("values");
371            newLocomotive.addContent(values.clone());
372
373            writeXML(pFile, doc);
374        } catch (IOException ex) {
375            log.error("Unable to write {}", pFile, ex);
376        }
377    }
378
379    static public String getFileLocation() {
380        return Roster.getDefault().getRosterFilesLocation();
381    }
382
383    // initialize logging
384    private final static Logger log = LoggerFactory.getLogger(LocoFile.class);
385
386}