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}