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