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}