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}