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