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}