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 internal 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
096                // Fairly ugly hack to migrate Indexed CVs of existing Tsunami2 & Econami
097                // roster entries to full NMRA S9.2.2 format (include CV 31 value).
098                if (family != null && (family.startsWith("Tsunami2") || family.startsWith("Econami")) && name.matches("\\d+\\.\\d+")) {
099                    String oldName = name;
100                    name = "16." + oldName;
101                    log.info("CV{} renamed to {} has value: {}", oldName, name, value);
102                }
103
104                // check whether the CV already exists, i.e. due to a variable definition
105                cvObject = cvModel.allCvMap().get(name);
106                if (cvObject == null && name.equals("19")) {
107                    log.info("CV19 special case triggered, kept without Variable; {} {}", mfgID, family);
108                    cvModel.addCV(name, false, false, false);
109                    cvObject = cvModel.allCvMap().get(name);
110                }
111                if (cvObject == null) {
112                    log.trace("undefined CV check with mfgID={} family={}", mfgID, family);
113                    if ( (mfgID != null && mfgID.equals("151")) || (family != null && family.startsWith("ESU ")) ) { // Electronic Solutions Ulm GmbH
114                        // ESU files do not generate CV entries until panel load time
115                        cvModel.addCV(name, false, false, false);
116                        cvObject = cvModel.allCvMap().get(name);
117                     } else {
118                        // this is a valid way to migrate a decoder definition, i.e. to remove a variable.
119                        log.info("CV {} was in '{}' roster file, but not defined by the decoder definition '{}'. '{}'; migrated", 
120                                name, rosterName, mfgID, family);
121                    }
122                }
123                if (cvObject != null) {
124                    cvObject.setValue(Integer.parseInt(value));
125
126                    // retrieve the state, if available
127                    if (element.getAttribute("state") != null) {
128                        var state = AbstractValue.ValueState.valueOf(element.getAttributeValue("state"));
129                        if (!state.equals(AbstractValue.ValueState.FROMDEFAULT)) {
130                            state = AbstractValue.ValueState.FROMFILE;
131                        }
132                        cvObject.setState(state);
133                    } else {
134                        cvObject.setState(AbstractValue.ValueState.FROMFILEUNKNOWN);
135                    }
136
137                }
138            }
139        } else {
140            log.error("no values element found in config file; CVs not configured for ID=\"{}\"", rosterName);
141        }
142
143        // ugly hack - set CV17 back to fromFile if present
144        // this is here because setting CV17, then CV18 seems to set
145        // CV17 to Edited.  This needs to be understood & fixed.
146        cvObject = cvModel.allCvMap().get("17");
147        if (cvObject != null) {
148            cvObject.setState(AbstractValue.ValueState.FROMFILE);
149        }
150    }
151
152    /**
153     * Load a VariableTableModel from the locomotive element in the File
154     *
155     * @param loco    A JDOM Element containing the locomotive definition
156     * @param varModel An existing VariableTableModel object
157     */
158    public static void loadVariableModel(Element loco, VariableTableModel varModel) {
159
160        Element values = loco.getChild("values");
161
162        if (values == null) {
163            log.error("no values element found in config file; Variable values not loaded for \"{}\"", loco.getAttributeValue("id"));
164            return;
165        }
166
167        Element decoderDef = values.getChild("decoderDef");
168
169        if (decoderDef == null) {
170            log.error("no decoderDef element found in config file; Variable values not loaded for \"{}\"", loco.getAttributeValue("id"));
171            return;
172        }
173
174
175        // get the Variable values and load
176        if (log.isDebugEnabled()) {
177            log.debug("Found {} varValue elements", decoderDef.getChildren("varValue").size());
178        }
179
180        // preload an index
181        HashMap<String, VariableValue> map = new HashMap<>();
182        for (int i = 0; i < varModel.getRowCount(); i++) {
183            log.debug("  map put {} to {}", varModel.getItem(i), varModel.getVariable(i));
184            map.put(varModel.getItem(i), varModel.getVariable(i));
185            map.put(varModel.getLabel(i), varModel.getVariable(i));
186        }
187
188        for (Element element : decoderDef.getChildren("varValue")) {
189            // locate the row
190            if (element.getAttribute("item") == null) {
191                if (log.isDebugEnabled()) {
192                    log.debug("unexpected null in item {} {}", element, element.getAttributes());
193                }
194                break;
195            }
196            if (element.getAttribute("value") == null) {
197                if (log.isDebugEnabled()) {
198                    log.debug("unexpected null in value {} {}", element, element.getAttributes());
199                }
200                break;
201            }
202
203            String item = element.getAttribute("item").getValue();
204            String value = element.getAttribute("value").getValue();
205            log.debug("Variable \"{}\" has value: {}", item, value);
206
207            VariableValue varValue = map.get(item);
208            if (varValue != null) {
209                varValue.setValue(value);
210
211                // retrieve the state, if available
212                if (element.getAttribute("state") != null) {
213                    var state = AbstractValue.ValueState.valueOf(element.getAttributeValue("state"));
214                    varValue.setState(state);
215                } else {
216                    varValue.setState(AbstractValue.ValueState.FROMFILEUNKNOWN);
217                }
218
219            } else {
220                if (selectMissingVarResponse(item) == MessageResponse.REPORT) {
221                    // not a warning, as this is how some definitions are migrated to remove erroneous variables
222                    log.debug("Did not find locofile variable \"{}\" in decoder definition, no variable loaded", item);
223                }
224            }
225        }
226
227    }
228
229    enum MessageResponse { IGNORE, REPORT }
230
231    /**
232     * Determine if a missing variable in decoder definition should be logged
233     * @param var Name of missing variable
234     * @return Decision on how to handle
235     */
236    protected static MessageResponse selectMissingVarResponse(String var) {
237        if (var.startsWith("ESU Function Row")) return MessageResponse.IGNORE; // from jmri.jmrit.symbolicprog.FnMapPanelESU
238        return MessageResponse.REPORT;
239    }
240
241    /**
242     * Write an XML version of this object, including also the RosterEntry
243     * information, and memory-resident decoder contents.
244     *
245     * Does not do an automatic backup of the file, so that should be done
246     * elsewhere.
247     *
248     * @param file          Destination file. This file is overwritten if it
249     *                      exists.
250     * @param cvModel       provides the CV numbers and contents
251     * @param variableModel provides the variable names and contents
252     * @param r             RosterEntry providing name, etc, information
253     */
254    public void writeFile(File file, CvTableModel cvModel, VariableTableModel variableModel, RosterEntry r) {
255        if (log.isDebugEnabled()) {
256            log.debug("writeFile to {} {}", file.getAbsolutePath(), file.getName());
257        }
258        try {
259            // This is taken in large part from "Java and XML" page 368
260
261            // create root element
262            Element root = new Element("locomotive-config");
263            root.setAttribute("noNamespaceSchemaLocation",
264                    "http://jmri.org/xml/schema/locomotive-config" + Roster.schemaVersion + ".xsd",
265                    org.jdom2.Namespace.getNamespace("xsi",
266                            "http://www.w3.org/2001/XMLSchema-instance"));
267
268            Document doc = newDocument(root);
269
270            // add XSLT processing instruction
271            // <?xml-stylesheet type="text/xsl" href="XSLT/locomotive.xsl"?>
272            java.util.Map<String, String> m = new java.util.HashMap<>();
273            m.put("type", "text/xsl");
274            m.put("href", xsltLocation + "locomotive.xsl");
275            ProcessingInstruction p = new ProcessingInstruction("xml-stylesheet", m);
276            doc.addContent(0, p);
277            // add top-level elements
278            Element locomotive = r.store();   // the locomotive element from the RosterEntry
279
280            root.addContent(locomotive);
281            Element values = new Element("values");
282            locomotive.addContent(values);
283
284            // Append a decoderDef element to values
285            Element decoderDef;
286            values.addContent(decoderDef = new Element("decoderDef"));
287            // add the variable values to the decoderDef Element
288            if (variableModel != null) {
289                for (int i = 0; i < variableModel.getRowCount(); i++) {
290                    decoderDef.addContent(new Element("varValue")
291                            .setAttribute("item", variableModel.getItem(i))
292                            .setAttribute("value", variableModel.getValString(i))
293                            .setAttribute("state", variableModel.getState(i).toString())
294                    );
295                }
296                // mark file as OK
297                variableModel.setFileDirty(false);
298            }
299
300            // add the CV values to the values Element
301            if (cvModel != null) {
302                for (int i = 0; i < cvModel.getRowCount(); i++) {
303                    values.addContent(new Element("CVvalue")
304                            .setAttribute("name", cvModel.getName(i))
305                            .setAttribute("value", cvModel.getValString(i))
306                            .setAttribute("state", cvModel.getCvByRow(i).getState().toString())
307                    );
308                }
309            }
310
311            writeXML(file, doc);
312
313        } catch (java.io.IOException ex) {
314            log.error("IOException", ex);
315        }
316    }
317
318    /**
319     * Write an XML version of this object from an existing XML tree, updating
320     * only the ID string.
321     * <p>
322     * Does not do an automatic backup of the file, so that should be done
323     * elsewhere. This is intended for copy and import operations, where the
324     * tree has been read from an existing file. Hence, only the "ID"
325     * information in the roster entry is updated. Note that any multi-line
326     * comments are not changed here.
327     *
328     * @param pFile        Destination file. This file is overwritten if it
329     *                     exists.
330     * @param pRootElement Root element of the JDOM tree to write. This should
331     *                     be of type "locomotive-config", and should not be in
332     *                     use elsewhere (clone it first!)
333     * @param pEntry       RosterEntry providing name, etc, information
334     */
335    public void writeFile(File pFile, Element pRootElement, RosterEntry pEntry) {
336        if (log.isDebugEnabled()) {
337            log.debug("writeFile to {} {}", pFile.getAbsolutePath(), pFile.getName());
338        }
339        try {
340            // This is taken in large part from "Java and XML" page 368
341
342            // create root element
343            Document doc = newDocument(pRootElement, dtdLocation + "locomotive-config.dtd");
344
345            // Update the locomotive.id element
346            if (log.isDebugEnabled()) {
347                log.debug("pEntry: {}", pEntry);
348            }
349            pRootElement.getChild("locomotive").getAttribute("id").setValue(pEntry.getId());
350
351            writeXML(pFile, doc);
352        } catch (IOException ex) {
353            log.error("Unable to write {}", pFile, ex);
354        }
355    }
356
357    /**
358     * Write an XML version of this object, updating the RosterEntry
359     * information, from an existing XML tree.
360     * <p>
361     * Does not do an automatic backup of the file, so that should be done
362     * elsewhere. This is intended for writing out changes to the RosterEntry
363     * information only.
364     *
365     * @param pFile           Destination file. This file is overwritten if it
366     *                        exists.
367     * @param existingElement Root element of the existing JDOM tree containing
368     *                        the CV and variable contents
369     * @param newLocomotive   Element from RosterEntry providing name, etc,
370     *                        information
371     */
372    public void writeFile(File pFile, Element existingElement, Element newLocomotive) {
373        if (log.isDebugEnabled()) {
374            log.debug("writeFile to {} {}", pFile.getAbsolutePath(), pFile.getName());
375        }
376        try {
377            // This is taken in large part from "Java and XML" page 368
378
379            // create root element
380            Element root = new Element("locomotive-config");
381            Document doc = newDocument(root, dtdLocation + "locomotive-config.dtd");
382            root.addContent(newLocomotive);
383
384            // add XSLT processing instruction
385            // <?xml-stylesheet type="text/xsl" href="XSLT/locomotive.xsl"?>
386            java.util.Map<String, String> m = new java.util.HashMap<>();
387            m.put("type", "text/xsl");
388            m.put("href", xsltLocation + "locomotive.xsl");
389            ProcessingInstruction p = new ProcessingInstruction("xml-stylesheet", m);
390            doc.addContent(0, p);
391
392            // Add the variable info
393            if (existingElement!=null) {
394                Element values = existingElement.getChild("locomotive").getChild("values");
395                newLocomotive.addContent(values.clone());
396            }
397
398            writeXML(pFile, doc);
399        } catch (IOException ex) {
400            log.error("Unable to write {}", pFile, ex);
401        }
402    }
403
404    public static String getFileLocation() {
405        return Roster.getDefault().getRosterFilesLocation();
406    }
407
408    // initialize logging
409    private static final Logger log = LoggerFactory.getLogger(LocoFile.class);
410
411}