001package jmri.jmrit.consisttool; 002 003import java.beans.PropertyChangeEvent; 004import java.beans.PropertyChangeListener; 005import java.io.File; 006import java.io.IOException; 007import java.util.*; 008 009import jmri.Consist; 010import jmri.ConsistManager; 011import jmri.LocoAddress; 012import jmri.DccLocoAddress; 013import jmri.InstanceManager; 014import jmri.jmrit.XmlFile; 015import jmri.jmrit.roster.Roster; 016import jmri.jmrit.roster.RosterConfigManager; 017import jmri.util.FileUtil; 018import org.jdom2.Attribute; 019import org.jdom2.Document; 020import org.jdom2.Element; 021import org.jdom2.JDOMException; 022import org.jdom2.ProcessingInstruction; 023import org.jdom2.filter.ElementFilter; 024import org.slf4j.Logger; 025import org.slf4j.LoggerFactory; 026 027/** 028 * Handle saving/restoring consist information to XML files. This class 029 * manipulates files conforming to the consist-roster-config DTD. 030 * 031 * @author Paul Bender Copyright (C) 2008 032 */ 033public class ConsistFile extends XmlFile implements PropertyChangeListener { 034 035 private static final String CONSIST = "consist"; // NOI18N 036 private static final String CONSISTID = "id"; // NOI18N 037 private static final String CONSISTNUMBER = "consistNumber"; // NOI18N 038 private static final String DCCLOCOADDRESS = "dccLocoAddress"; // NOI18N 039 private static final String PROTOCOL = "protocol"; // NOI18N 040 private static final String LONGADDRESS = "longAddress"; // NOI18N 041 private static final String LOCODIR = "locoDir"; // NOI18N 042 private static final String LOCONAME = "locoName"; // NOI18N 043 private static final String LOCOROSTERID = "locoRosterId"; // NOI18N 044 private static final String NORMAL = "normal"; // NOI18N 045 private static final String REVERSE = "reverse"; // NOI18N 046 047 protected ConsistManager consistMan = null; 048 049 public ConsistFile() { 050 super(); 051 consistMan = InstanceManager.getDefault(jmri.ConsistManager.class); 052 Roster.getDefault().addPropertyChangeListener(this); 053 } 054 055 /** 056 * Load a Consist from the consist elements in the file. 057 * 058 * @param consist a JDOM element containing a consist 059 */ 060 private void consistFromXml(Element consist) { 061 Attribute cnumber; 062 Attribute isCLong; 063 Attribute hasProtocol; 064 Consist newConsist; 065 066 // Read the consist address from the file and create the 067 // consisit in memory if it doesn't exist already. 068 cnumber = consist.getAttribute(CONSISTNUMBER); 069 isCLong = consist.getAttribute(LONGADDRESS); 070 hasProtocol = consist.getAttribute(PROTOCOL); 071 DccLocoAddress consistAddress; 072 073 if (hasProtocol != null) { 074 log.debug("adding consist {} with protocol set to {}.", cnumber, hasProtocol.getValue()); 075 try { 076 int number = Integer.parseInt(cnumber.getValue()); 077 consistAddress = new DccLocoAddress(number, 078 jmri.DccLocoAddress.Protocol.getByShortName(hasProtocol.getValue())); 079 } catch (NumberFormatException e) { 080 log.debug("Consist number not an integer"); 081 return; 082 } 083 } else if (isCLong != null) { 084 log.debug("adding consist {} with longAddress set to {}.", cnumber, isCLong.getValue()); 085 try { 086 int number = Integer.parseInt(cnumber.getValue()); 087 consistAddress = new DccLocoAddress(number, isCLong.getValue().equals("yes")); 088 } catch (NumberFormatException e) { 089 log.debug("Consist number not an integer"); 090 return; 091 } 092 093 } else { 094 log.debug("adding consist {} with default long address setting.", cnumber); 095 consistAddress = new DccLocoAddress(Integer.parseInt(cnumber.getValue()), false); 096 } 097 newConsist = consistMan.getConsist(consistAddress); 098 if (!(newConsist.getConsistList().isEmpty())) { 099 log.debug("Consist {} is not empty. Using version in memory.", consistAddress); 100 return; 101 } 102 103 readConsistType(consist, newConsist); 104 readConsistId(consist, newConsist); 105 readConsistLocoList(consist,newConsist); 106 consistMan.notifyConsistListChanged(); 107 } 108 109 public void readConsistLocoList(Element consist, Consist newConsist) { 110 // read each child of locomotive in the consist from the file 111 // and restore it's information to memory. 112 Iterator<Element> childIterator = consist.getDescendants(new ElementFilter("loco")); 113 try { 114 Element e; 115 do { 116 e = childIterator.next(); 117 Attribute number = e.getAttribute(DCCLOCOADDRESS); 118 log.debug("adding Loco {}", number); 119 DccLocoAddress address = readLocoAddress(e); 120 121 Attribute direction = e.getAttribute(LOCODIR); 122 boolean directionNormal = false; 123 if (direction != null) { 124 // use the values from the file 125 log.debug("using direction from file {}", direction.getValue()); 126 directionNormal = direction.getValue().equals(NORMAL); 127 } else { 128 // use default, normal direction 129 directionNormal = true; 130 } 131 // Use restore so we DO NOT cause send any commands 132 // to the command station as we recreate the consist. 133 newConsist.restore(address,directionNormal); 134 readLocoPosition(e,address,newConsist); 135 Attribute rosterId = e.getAttribute(LOCOROSTERID); 136 if (rosterId != null) { 137 newConsist.setRosterId(address, rosterId.getValue()); 138 } 139 } while (true); 140 } catch (NoSuchElementException nse) { 141 log.debug("end of loco list"); 142 } 143 } 144 145 private void readConsistType(Element consist, Consist newConsist){ 146 // read and set the consist type 147 Attribute type = consist.getAttribute("type"); 148 if (type != null) { 149 // use the value read from the file 150 newConsist.setConsistType((type.getValue().equals("CSAC")) ? Consist.CS_CONSIST : Consist.ADVANCED_CONSIST); 151 } else { 152 // use the default (DAC) 153 newConsist.setConsistType(Consist.ADVANCED_CONSIST); 154 } 155 } 156 157 private void readConsistId(Element consist,Consist newConsist){ 158 // Read the consist ID from the file 159 Attribute cID = consist.getAttribute(CONSISTID); 160 if (cID != null) { 161 // use the value read from the file 162 newConsist.setConsistID(cID.getValue()); 163 } 164 } 165 166 private void readLocoPosition(Element loco,DccLocoAddress address, Consist newConsist){ 167 Attribute position = loco.getAttribute(LOCONAME); 168 if (position != null && !position.getValue().equals("mid")) { 169 if (position.getValue().equals("lead")) { 170 newConsist.setPosition(address, Consist.POSITION_LEAD); 171 } else if (position.getValue().equals("rear")) { 172 newConsist.setPosition(address, Consist.POSITION_TRAIL); 173 } 174 } else { 175 Attribute midNumber = loco.getAttribute("locoMidNumber"); 176 if (midNumber != null) { 177 int pos = Integer.parseInt(midNumber.getValue()); 178 newConsist.setPosition(address, pos); 179 } 180 } 181 } 182 183 private DccLocoAddress readLocoAddress(Element loco){ 184 DccLocoAddress address; 185 Attribute number = loco.getAttribute(DCCLOCOADDRESS); 186 Attribute isLong = loco.getAttribute(LONGADDRESS); 187 Attribute hasProtocol = loco.getAttribute(PROTOCOL); 188 189 if (hasProtocol != null) { 190 log.debug("adding loco with protocol set to {}.", hasProtocol.getValue()); 191 var protocol = jmri.LocoAddress.Protocol.getByShortName(hasProtocol.getValue()); 192 address = new DccLocoAddress( 193 Integer.parseInt(number.getValue()), 194 protocol); 195 196 } else if (isLong != null ) { 197 // use the values from the file 198 address = new DccLocoAddress( 199 Integer.parseInt(number.getValue()), 200 isLong.getValue().equals("yes")); 201 } else { 202 // set as long address 203 address = new DccLocoAddress( 204 Integer.parseInt(number.getValue()), 205 true); 206 } 207 208 return address; 209 } 210 211 /** 212 * convert a Consist to XML. 213 * 214 * @param consist a Consist object to write to the file 215 * @return an Element representing the consist. 216 */ 217 private Element consistToXml(Consist consist) { 218 Element e = new Element(CONSIST); 219 e.setAttribute(CONSISTID, consist.getConsistID()); 220 e.setAttribute(CONSISTNUMBER, "" + consist.getConsistAddress() 221 .getNumber()); 222 223 log.debug("writing long address {} from protocol {}", consist.getConsistAddress() 224 .isLongAddress(), consist.getConsistAddress() 225 .getProtocol()); 226 e.setAttribute(LONGADDRESS, consist.getConsistAddress() 227 .isLongAddress() ? "yes" : "no"); 228 e.setAttribute(PROTOCOL, consist.getConsistAddress().getProtocol().getShortName()); 229 e.setAttribute("type", consist.getConsistType() == Consist.ADVANCED_CONSIST ? "DAC" : "CSAC"); 230 ArrayList<DccLocoAddress> addressList = consist.getConsistList(); 231 232 for (int i = 0; i < addressList.size(); i++) { 233 DccLocoAddress locoaddress = addressList.get(i); 234 Element eng = new Element("loco"); 235 eng.setAttribute(DCCLOCOADDRESS, "" + locoaddress.getNumber()); 236 eng.setAttribute(PROTOCOL, locoaddress.getProtocol().getShortName()); 237 eng.setAttribute(LONGADDRESS, locoaddress.isLongAddress() ? "yes" : "no"); 238 eng.setAttribute(LOCODIR, consist.getLocoDirection(locoaddress) ? NORMAL : REVERSE); 239 int position = consist.getPosition(locoaddress); 240 switch (position) { 241 case Consist.POSITION_LEAD: 242 eng.setAttribute(LOCONAME, "lead"); 243 break; 244 case Consist.POSITION_TRAIL: 245 eng.setAttribute(LOCONAME, "rear"); 246 break; 247 default: 248 eng.setAttribute(LOCONAME, "mid"); 249 eng.setAttribute("locoMidNumber", "" + position); 250 break; 251 } 252 String rosterId = consist.getRosterId(locoaddress); 253 if (rosterId != null) { 254 eng.setAttribute(LOCOROSTERID, rosterId); 255 } 256 e.addContent(eng); 257 } 258 return (e); 259 } 260 261 /** 262 * Read all consists from the default file name. 263 * 264 * @throws org.jdom2.JDOMException if unable to parse consists 265 * @throws java.io.IOException if unable to read file 266 */ 267 public void readFile() throws JDOMException, IOException { 268 readFile(defaultConsistFilename()); 269 } 270 271 /** 272 * Read all consists from a file. 273 * 274 * @param fileName path to file 275 * @throws org.jdom2.JDOMException if unable to parse consists 276 * @throws java.io.IOException if unable to read file 277 */ 278 public void readFile(String fileName) throws JDOMException, IOException { 279 if (checkFile(fileName)) { 280 Element root = rootFromName(fileName); 281 Element roster; 282 if (root == null) { 283 log.warn("consist file could not be read"); 284 return; 285 } 286 roster = root.getChild("roster"); 287 if (roster == null) { 288 log.debug("consist file does not contain a roster entry"); 289 return; 290 } 291 Iterator<Element> consistIterator = root.getDescendants(new ElementFilter(CONSIST)); 292 try { 293 Element consist; 294 do { 295 consist = consistIterator.next(); 296 consistFromXml(consist); 297 } while (consistIterator.hasNext()); 298 } catch (NoSuchElementException nde) { 299 log.debug("end of consist list"); 300 } 301 } else { 302 log.info("Consist file does not exist. One will be created if necessary."); 303 } 304 305 } 306 307 /** 308 * Write all consists to the default file name. 309 * 310 * @param consistList list of consist addresses 311 * @throws java.io.IOException if unable to write file 312 */ 313 public void writeFile(List<LocoAddress> consistList) throws IOException { 314 writeFile(consistList, defaultConsistFilename()); 315 } 316 317 /** 318 * Write all consists to a file. 319 * 320 * @param consistList list of consist addresses 321 * @param fileName path to file 322 * @throws java.io.IOException if unable to write file 323 */ 324 public void writeFile(List<LocoAddress> consistList, String fileName) throws IOException { 325 // create root element 326 Element root = new Element("consist-roster-config"); 327 Document doc = newDocument(root, dtdLocation + "consist-roster-config.dtd"); 328 329 // add XSLT processing instruction 330 Map<String, String> m = new HashMap<>(); 331 m.put("type", "text/xsl"); 332 m.put("href", xsltLocation + "consistRoster.xsl"); 333 ProcessingInstruction p = new ProcessingInstruction("xml-stylesheet", m); 334 doc.addContent(0, p); 335 336 Element roster = new Element("roster"); 337 338 for (int i = 0; i < consistList.size(); i++) { 339 Consist newConsist = consistMan.getConsist(consistList.get(i)); 340 roster.addContent(consistToXml(newConsist)); 341 } 342 root.addContent(roster); 343 if (!checkFile(fileName)) { 344 //The file does not exist, create it before writing 345 File file = new File(fileName); 346 // verify the directory exists. 347 File parentDir = file.getParentFile(); 348 FileUtil.createDirectory(parentDir); 349 if (!file.createNewFile()) { 350 throw (new IOException()); 351 } 352 } 353 writeXML(findFile(fileName), doc); 354 } 355 356 /** 357 * GetFile Location. 358 * 359 * @return the preferences subdirectory in which Consist Files are kept 360 * this is relative to the roster files location. 361 */ 362 public static String getFileLocation() { 363 return Roster.getDefault().getRosterFilesLocation() + CONSIST + File.separator; 364 } 365 366 /** 367 * Get the filename for the default Consist file, including location. 368 * 369 * @return the filename 370 */ 371 public static String defaultConsistFilename() { 372 return getFileLocation() + "consist.xml"; 373 } 374 375 /** 376 * {@inheritDoc} 377 */ 378 @Override 379 public void propertyChange(PropertyChangeEvent evt) { 380 if (evt.getSource() instanceof Roster && 381 evt.getPropertyName().equals(RosterConfigManager.DIRECTORY)) { 382 try { 383 this.writeFile(consistMan.getConsistList()); 384 } catch (IOException ioe) { 385 log.error("Unable to write consist information to new consist folder"); 386 } 387 } 388 } 389 390 // initialize logging 391 private static final Logger log = LoggerFactory.getLogger(ConsistFile.class); 392}