001package jmri.jmrit.vsdecoder; 002 003import java.io.BufferedOutputStream; 004import java.io.File; 005import java.io.FileOutputStream; 006import java.io.IOException; 007import java.io.InputStream; 008import java.io.OutputStream; 009import java.util.Enumeration; 010import java.util.Iterator; 011import java.util.List; 012import java.util.zip.ZipEntry; 013import java.util.zip.ZipException; 014import java.util.zip.ZipFile; 015import jmri.jmrit.XmlFile; 016import org.jdom2.Element; 017import org.slf4j.Logger; 018import org.slf4j.LoggerFactory; 019 020/** 021 * Open a VSD file and validate the configuration part. 022 * 023 * <hr> 024 * This file is part of JMRI. 025 * <p> 026 * JMRI is free software; you can redistribute it and/or modify it under 027 * the terms of version 2 of the GNU General Public License as published 028 * by the Free Software Foundation. See the "COPYING" file for a copy 029 * of this license. 030 * <p> 031 * JMRI is distributed in the hope that it will be useful, but WITHOUT 032 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 033 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 034 * for more details. 035 * 036 * @author Mark Underwood Copyright (C) 2011 037 */ 038public class VSDFile extends ZipFile { 039 040 private static final String VSDXmlFileName = "config.xml"; // NOI18N 041 042 // Dummy class just used to instantiate 043 private static class VSDXmlFile extends XmlFile { 044 } 045 046 protected Element root; 047 protected boolean initialized = false; 048 private String _statusMsg = Bundle.getMessage("ButtonOK"); // File Status = OK 049 private String missedFileName; 050 private int num_cylinders; 051 052 public VSDFile(File file) throws ZipException, IOException { 053 super(file); 054 initialized = init(); 055 } 056 057 public VSDFile(File file, int mode) throws ZipException, IOException { 058 super(file, mode); 059 initialized = init(); 060 } 061 062 public VSDFile(String name) throws ZipException, IOException { 063 super(name); 064 initialized = init(); 065 } 066 067 public boolean isInitialized() { 068 return initialized; 069 } 070 071 public String getStatusMessage() { 072 return _statusMsg; 073 } 074 075 @edu.umd.cs.findbugs.annotations.SuppressFBWarnings( value="SLF4J_FORMAT_SHOULD_BE_CONST", 076 justification="error text in _statusMsg kept for later use") 077 protected boolean init() { 078 VSDXmlFile xmlfile = new VSDXmlFile(); 079 initialized = false; 080 081 try { 082 // Debug: List all the top-level contents in the file 083 Enumeration<?> entries = this.entries(); 084 while (entries.hasMoreElements()) { 085 ZipEntry z = (ZipEntry) entries.nextElement(); 086 log.debug("Entry: {}", z.getName()); 087 } 088 089 ZipEntry config = this.getEntry(VSDXmlFileName); 090 if (config == null) { 091 _statusMsg = "File does not contain " + VSDXmlFileName; 092 log.error(_statusMsg); 093 return false; 094 } 095 File f2 = new File(this.getURL(VSDXmlFileName)); 096 root = xmlfile.rootFromFile(f2); 097 ValidateStatus rv = this.validate(root); 098 if (!rv.getValid()) { 099 _statusMsg = rv.getMessage(); 100 } 101 initialized = rv.getValid(); 102 return initialized; 103 104 } catch (java.io.IOException ioe) { 105 _statusMsg = "IO Error auto-loading VSD File: " + VSDXmlFileName + " " + ioe.toString(); 106 log.error(_statusMsg); 107 return false; 108 } catch (org.jdom2.JDOMException ex) { 109 _statusMsg = "JDOM Exception loading VSDecoder from path " + VSDXmlFileName + " " + ex.toString(); 110 log.error(_statusMsg); 111 return false; 112 } 113 } 114 115 public Element getRoot() { 116 return root; 117 } 118 119 public java.io.InputStream getInputStream(String name) { 120 java.io.InputStream rv; 121 try { 122 ZipEntry e = this.getEntry(name); 123 if (e == null) { 124 e = this.getEntry(name.toLowerCase()); 125 if (e == null) { 126 e = this.getEntry(name.toUpperCase()); 127 if (e == null) { 128 // I give up. Return null 129 return null; 130 } 131 } 132 } 133 rv = getInputStream(this.getEntry(name)); 134 } catch (IOException e) { 135 log.error("IOException caught", e); 136 rv = null; 137 } 138 return rv; 139 } 140 141 public java.io.File getFile(String name) { 142 ZipEntry e = this.getEntry(name); 143 if (e == null) { 144 return null; 145 } else { 146 File f = new File(e.getName()); 147 return f; 148 } 149 } 150 151 public String getURL(String name) { 152 try { 153 // Grab the entry from the Zip file, and create a tempfile to dump it into 154 ZipEntry e = this.getEntry(name); 155 File t = File.createTempFile(name, ".wav.tmp"); 156 t.deleteOnExit(); 157 158 // Dump the file from the Zip into the tempfile 159 copyInputStream(this.getInputStream(e), new BufferedOutputStream(new FileOutputStream(t))); 160 161 // return the name of the tempfile 162 return t.getPath(); 163 164 } catch (IOException e) { 165 log.error("IO exception", e); 166 return null; 167 } 168 } 169 170 private static final void copyInputStream(InputStream in, OutputStream out) 171 throws IOException { 172 byte[] buffer = new byte[1024]; 173 int len; 174 175 while ((len = in.read(buffer)) >= 0) { 176 out.write(buffer, 0, len); 177 } 178 179 in.close(); 180 out.close(); 181 } 182 183 static class ValidateStatus { 184 String msg = ""; 185 Boolean valid = false; 186 187 public ValidateStatus() { 188 this(false, ""); 189 } 190 191 public ValidateStatus(Boolean v, String m) { 192 valid = v; 193 msg = m; 194 } 195 196 public void setValid(Boolean v) { 197 valid = v; 198 } 199 200 public void setMessage(String m) { 201 msg = m; 202 } 203 204 public Boolean getValid() { 205 return valid; 206 } 207 208 public String getMessage() { 209 return msg; 210 } 211 } 212 213 public ValidateStatus validate(Element xmlroot) { 214 Element e, el; 215 // Iterate through all the profiles in the file 216 // Would like to get rid of this suppression, but I think it's fairly safe to assume a list of children 217 // returned from an Element is going to be a list of Elements 218 Iterator<Element> i = xmlroot.getChildren("profile").iterator(); 219 // If no Profiles, file is invalid 220 if (!i.hasNext()) { 221 log.error("No Profile(s)"); 222 return new ValidateStatus(false, Bundle.getMessage("VSDFileStatusNoProfiles")); 223 } 224 225 // Iterate through Profiles 226 while (i.hasNext()) { 227 e = i.next(); // e points to a profile 228 log.debug("Validate: Profile {}", e.getAttributeValue("name")); 229 if (e.getAttributeValue("name") == null || e.getAttributeValue("name").isEmpty()) { 230 log.error("Missing Profile name"); 231 return new ValidateStatus(false, "Missing Profile name"); 232 } 233 234 // Get the "Sound" children ... these are the ones that should have files 235 // Would like to get rid of this suppression, but I think it's fairly safe to assume a list of children 236 // returned from an Element is going to be a list of Elements 237 Iterator<Element> i2 = (e.getChildren("sound")).iterator(); 238 if (!i2.hasNext()) { 239 log.error("Profile {} has no Sounds", e.getAttributeValue("name")); 240 return new ValidateStatus(false, Bundle.getMessage("VSDFileStatusNoSounds") + ": " + e.getAttributeValue("name")); 241 } 242 243 // Iterate through Sounds 244 while (i2.hasNext()) { 245 el = i2.next(); 246 log.debug("Element: {}", el); 247 if (el.getAttribute("name") == null) { 248 log.error("Sound element without a name in profile {}", e.getAttributeValue("name")); 249 return new ValidateStatus(false, "Sound-Element without a name"); //Bundle.getMessage("VSDFileStatusNoName") 250 } 251 String type = el.getAttributeValue("type"); 252 log.debug(" Name: {}", el.getAttributeValue("name")); 253 log.debug(" type: {}", type); 254 if (type.equals("configurable")) { 255 // Validate a Configurable Sound 256 // All these elements are optional, so if the element is missing, 257 // that's OK. But if there is an element, and the FILE is missing, 258 // that's bad 259 if (!validateOptionalFile(el, "start-file")) { 260 return new ValidateStatus(false, Bundle.getMessage("VSDFileStatusMissingSoundFile") + " <start-file>: " + missedFileName); 261 } 262 if (!validateOptionalFile(el, "mid-file")) { 263 return new ValidateStatus(false, Bundle.getMessage("VSDFileStatusMissingSoundFile") + " <mid-file>: " + missedFileName); 264 } 265 if (!validateOptionalFile(el, "end-file")) { 266 return new ValidateStatus(false, Bundle.getMessage("VSDFileStatusMissingSoundFile") + " <end-file>: " + missedFileName); 267 } 268 if (!validateOptionalFile(el, "short-file")) { 269 return new ValidateStatus(false, Bundle.getMessage("VSDFileStatusMissingSoundFile") + " <short-file>: " + missedFileName); 270 } 271 } else if (type.equals("diesel")) { 272 // Validate a diesel sound 273 String[] file_elements = {"file"}; 274 if (!validateOptionalFile(el, "start-file")) { 275 return new ValidateStatus(false, Bundle.getMessage("VSDFileStatusMissingSoundFile") + " <start-file>: " + missedFileName); 276 } 277 if (!validateOptionalFile(el, "shutdown-file")) { 278 return new ValidateStatus(false, Bundle.getMessage("VSDFileStatusMissingSoundFile") + " <shutdown-file>: " + missedFileName); 279 } 280 if (!validateFiles(el, "notch-sound", file_elements)) { 281 return new ValidateStatus(false, Bundle.getMessage("VSDFileStatusMissingSoundFile") + " <notch-sound>: " + missedFileName); 282 } 283 if (!validateFiles(el, "notch-transition", file_elements, false)) { 284 return new ValidateStatus(false, Bundle.getMessage("VSDFileStatusMissingSoundFile") + " <notch-transition>: " + missedFileName); 285 } 286 } else if (type.equals("diesel3")) { 287 // Validate a diesel3 sound 288 String[] file_elements = {"file", "accel-file", "decel-file"}; 289 if (!validateOptionalFile(el, "start-file")) { 290 return new ValidateStatus(false, Bundle.getMessage("VSDFileStatusMissingSoundFile") + " <start-file>: " + missedFileName); 291 } 292 if (!validateOptionalFile(el, "shutdown-file")) { 293 return new ValidateStatus(false, Bundle.getMessage("VSDFileStatusMissingSoundFile") + " <shutdown-file>: " + missedFileName); 294 } 295 if (!validateFiles(el, "notch-sound", file_elements)) { 296 return new ValidateStatus(false, Bundle.getMessage("VSDFileStatusMissingSoundFile") + " <notch-sound>: " + missedFileName); 297 } 298 } else if (type.equals("steam")) { 299 // Validate a steam sound 300 String[] file_elements = {"file"}; 301 if (!validateRequiredElement(el, "top-speed")) { 302 return new ValidateStatus(false, Bundle.getMessage("VSDFileStatusMissingElement") + ": <top-speed>"); 303 } 304 if (!validateRequiredElement(el, "driver-diameter")) { 305 return new ValidateStatus(false, Bundle.getMessage("VSDFileStatusMissingElement") + ": <driver-diameter>"); 306 } 307 if (!validateRequiredElement(el, "cylinders")) { 308 return new ValidateStatus(false, Bundle.getMessage("VSDFileStatusMissingElement") + ": <cylinders>"); 309 } else { 310 // Found element <cylinders> - is number valid? 311 if (!validateRequiredElementRange(el, "cylinders", 1, 4)) { 312 return new ValidateStatus(false, "Number of cylinders must be 1, 2, 3 or 4"); 313 } 314 } 315 if (!validateFiles(el, "rpm-step", file_elements)) { 316 return new ValidateStatus(false, Bundle.getMessage("VSDFileStatusMissingSoundFile") + " <rpm-step>: " + missedFileName); 317 } 318 } else if (type.equals("steam1")) { 319 // Validate a steam1 sound 320 if (!validateRequiredElement(el, "top-speed")) { 321 return new ValidateStatus(false, Bundle.getMessage("VSDFileStatusMissingElement") + ": <top-speed>"); 322 } 323 if (!validateRequiredElement(el, "driver-diameter-float")) { 324 return new ValidateStatus(false, Bundle.getMessage("VSDFileStatusMissingElement") + ": <driver-diameter-float>"); 325 } 326 if (!validateRequiredElement(el, "cylinders")) { 327 return new ValidateStatus(false, Bundle.getMessage("VSDFileStatusMissingElement") + ": <cylinders>"); 328 } else { 329 // Found element <cylinders> - is number valid? 330 if (!validateRequiredElementRange(el, "cylinders", 1, 4)) { 331 return new ValidateStatus(false, "Number of cylinders must be 1, 2, 3 or 4"); 332 } 333 // Found element <cylinders> - #cylinders * 2 must correspond to #files 334 String[] file_elements = {"notch-file", "coast-file"}; 335 if (!validateFilesNumbers(el, "s1notch-sound", file_elements, true)) { 336 return new ValidateStatus(false, getStatusMessage()); 337 } 338 } 339 if (!validateRequiredElement(el, "s1notch-sound")) { 340 return new ValidateStatus(false, Bundle.getMessage("VSDFileStatusMissingElement") + ": <s1notch-sound>"); 341 } 342 if (!validateRequiredNotchElement(el, "s1notch-sound", "min-rpm")) { 343 return new ValidateStatus(false, "Element min-rpm for Element s1notch-sound missing"); 344 } 345 if (!validateRequiredNotchElement(el, "s1notch-sound", "max-rpm")) { 346 return new ValidateStatus(false, "Element max-rpm for Element s1notch-sound missing"); 347 } 348 String[] file_elements = {"notch-file", "notchfiller-file", "coast-file", "coastfiller-file"}; 349 if (!validateFiles(el, "s1notch-sound", file_elements)) { 350 return new ValidateStatus(false, Bundle.getMessage("VSDFileStatusMissingSoundFile") + " <s1notch-sound>: " + missedFileName); 351 } 352 } else { 353 return new ValidateStatus(false, "Unsupported sound type: " + type); 354 } 355 } 356 } 357 log.debug("File Validation Successful."); 358 return new ValidateStatus(true, Bundle.getMessage("ButtonOK")); // File Status = OK 359 } 360 361 protected boolean validateRequiredElement(Element el, String name) { 362 if (el.getChild(name) == null || el.getChildText(name).isEmpty()) { 363 log.error("Element {} for Element {} missing", name, el.getAttributeValue("name")); 364 return false; 365 } 366 return true; 367 } 368 369 protected boolean validateRequiredElementRange(Element el, String name, int val_from, int val_to) { 370 int val = Integer.parseInt(el.getChildText(name)); 371 log.debug(" <{}> found: {} ({} to {})", name, val, val_from, val_to); 372 if (val >= val_from && val <= val_to) { 373 if (name.equals("cylinders")) { 374 num_cylinders = val; // save #cylinder for the #files check 375 } 376 return true; 377 } else { 378 log.error("Value of {} is invalid", name); 379 return false; 380 } 381 } 382 383 protected boolean validateRequiredNotchElement(Element el, String name1, String name2) { 384 // Get all notches 385 List<Element> elist = el.getChildren(name1); 386 Iterator<Element> ns_i = elist.iterator(); 387 while (ns_i.hasNext()) { 388 Element ns_e = ns_i.next(); 389 if (ns_e.getChild(name2) == null || ns_e.getChildText(name2).isEmpty()) { 390 log.error("Element {} for Element {} missing", name2, name1); 391 return false; 392 } 393 } 394 return true; 395 } 396 397 protected boolean validateOptionalFile(Element el, String name) { 398 return validateOptionalFile(el, name, true); 399 } 400 401 protected boolean validateOptionalFile(Element el, String name, Boolean required) { 402 String s = el.getChildText(name); 403 if ((s != null) && (getFile(s) == null)) { 404 missedFileName = s; 405 log.error("File {} for Element {} not found {}", s, name, el.getAttributeValue("name")); 406 return false; 407 } 408 return true; 409 } 410 411 protected boolean validateFiles(Element el, String name, String[] fnames) { 412 return validateFiles(el, name, fnames, true); 413 } 414 415 protected boolean validateFiles(Element el, String name, String[] fnames, Boolean required) { 416 List<Element> elist = el.getChildren(name); 417 String s; 418 419 // First, check to see if any elements of this <name> exist 420 if (elist.isEmpty() && required) { 421 // Only fail if this type of element is required 422 log.error("No elements of name {}", name); 423 return false; 424 } 425 426 // Now, if the elements exist, make sure the files they point to exist 427 // Would like to get rid of this suppression, but I think it's fairly safe to assume a list of children 428 // returned from an Element is going to be a list of Elements 429 log.debug("{}(s): {}", name, elist.size()); 430 Iterator<Element> ns_i = elist.iterator(); 431 while (ns_i.hasNext()) { 432 Element ns_e = ns_i.next(); 433 for (String fn : fnames) { 434 List<Element> elistf = ns_e.getChildren(fn); // Handle more than one child 435 log.debug(" {}(s): {}", fn, elistf.size()); 436 Iterator<Element> ns_if = elistf.iterator(); 437 while (ns_if.hasNext()) { 438 Element ns_ef = ns_if.next(); 439 s = ns_ef.getText(); 440 log.debug(" getText: {}", s); 441 if ((s == null) || (getFile(s) == null)) { 442 log.error("File {} for Element {} in Element {} not found", s, fn, name); 443 missedFileName = s; // Pass missing file name to global variable 444 return false; 445 } 446 } 447 } 448 } 449 // Made it this far, all is well 450 return true; 451 } 452 453 @edu.umd.cs.findbugs.annotations.SuppressFBWarnings( value="SLF4J_FORMAT_SHOULD_BE_CONST", 454 justification="error text in _statusMsg kept for later use") 455 protected boolean validateFilesNumbers(Element el, String name, String[] fnames, Boolean required) { 456 List<Element> elist = el.getChildren(name); 457 458 // First, check to see if any elements of this <name> exist 459 if (elist.isEmpty() && required) { 460 // Only fail if this type of element is required 461 log.error("No elements of name {}", name); 462 return false; 463 } 464 465 // Would like to get rid of this suppression, but I think it's fairly safe to assume a list of children 466 // returned from an Element is going to be a list of Elements 467 log.debug("{}(s): {}", name, elist.size()); 468 int nn = 1; // notch number 469 Iterator<Element> ns_i = elist.iterator(); 470 while (ns_i.hasNext()) { 471 Element ns_e = ns_i.next(); 472 log.debug(" nse: {}", ns_e); 473 for (String fn : fnames) { 474 List<Element> elistf = ns_e.getChildren(fn); // get all files of type <fn> 475 // #notch-files must be equal num_cylinders * 2 476 if (fn.equals("notch-file") && (elistf.size() != num_cylinders * 2)) { 477 _statusMsg = "Invalid number of notch files: " + elistf.size() + ", but should be " 478 + (num_cylinders * 2) + " (for " + num_cylinders + " cylinders) in notch " + nn; 479 log.error(_statusMsg); 480 return false; 481 } 482 // #coast files are allowed on notch1 only, but are optional. If exist, must be equal num_cylinders * 2 483 if (fn.equals("coast-file") && nn == 1 && !((elistf.size() == num_cylinders * 2) || elistf.size() == 0)) { 484 _statusMsg = "Invalid number of coast files: " + elistf.size() + ", but should be " 485 + (num_cylinders * 2) + " (for " + num_cylinders + " cylinders) in notch 1"; 486 log.error(_statusMsg); 487 return false; 488 } 489 // Coast files are not allowed on notches > 1 490 if (fn.equals("coast-file") && nn > 1 && (elistf.size() != 0)) { 491 _statusMsg = "Invalid number of coast files: " + elistf.size() + ", but should be 0 in notch " + nn; 492 log.error(_statusMsg); 493 return false; 494 } 495 // Note: no check for a notchfiller-file or a coastfiller-file 496 } 497 nn++; 498 } 499 // Made it this far, all is well 500 return true; 501 } 502 503 private final static Logger log = LoggerFactory.getLogger(VSDFile.class); 504 505}