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