Xcos: ZCOS should not store the time
[scilab.git] / scilab / modules / xcos / src / java / org / scilab / modules / xcos / io / spec / XcosPackage.java
1 /*
2  * Scilab ( http://www.scilab.org/ ) - This file is part of Scilab
3  * Copyright (C) 2012 - Scilab Enterprises - Clement DAVID
4  *
5  * This file must be used under the terms of the CeCILL.
6  * This source file is licensed as described in the file COPYING, which
7  * you should have received as part of this distribution.  The terms
8  * are also available at
9  * http://www.cecill.info/licences/Licence_CeCILL_V2.1-en.txt
10  *
11  */
12
13 package org.scilab.modules.xcos.io.spec;
14
15 import java.io.File;
16 import java.io.FileInputStream;
17 import java.io.FileOutputStream;
18 import java.io.IOException;
19 import java.io.InputStream;
20 import java.util.Arrays;
21 import java.util.zip.CRC32;
22 import java.util.zip.ZipEntry;
23 import java.util.zip.ZipInputStream;
24 import java.util.zip.ZipOutputStream;
25
26 import javax.xml.parsers.ParserConfigurationException;
27 import javax.xml.transform.OutputKeys;
28 import javax.xml.transform.Transformer;
29 import javax.xml.transform.TransformerConfigurationException;
30 import javax.xml.transform.TransformerException;
31 import javax.xml.transform.TransformerFactory;
32 import javax.xml.transform.dom.DOMResult;
33 import javax.xml.transform.dom.DOMSource;
34 import javax.xml.transform.stream.StreamResult;
35 import javax.xml.transform.stream.StreamSource;
36 import javax.xml.xpath.XPath;
37 import javax.xml.xpath.XPathConstants;
38 import javax.xml.xpath.XPathExpression;
39 import javax.xml.xpath.XPathExpressionException;
40 import javax.xml.xpath.XPathFactory;
41
42 import org.scilab.modules.commons.xml.ScilabDocumentBuilderFactory;
43 import org.scilab.modules.commons.xml.ScilabTransformerFactory;
44 import org.scilab.modules.commons.xml.ScilabXPathFactory;
45 import org.scilab.modules.types.ScilabList;
46 import org.scilab.modules.xcos.graph.XcosDiagram;
47 import org.w3c.dom.Document;
48 import org.w3c.dom.Element;
49 import org.xml.sax.SAXException;
50
51 /**
52  * This class handle xcos format package (eg. ZIP) management
53  *
54  * You can load/save from/to a file a specific package without storing any data.
55  */
56 public class XcosPackage {
57     private static final String MIMETYPE = "mimetype";
58     private static final String META_INF_MANIFEST_XML = "META-INF/manifest.xml";
59
60     private static final String VERSION = "0.2";
61     private static final String MIME = "application/x-scilab-xcos";
62     private static final byte[] MIME_BYTES = MIME.getBytes();
63
64     private static final String INVALID_MIMETYPE = "Invalid mimetype";
65
66     /**
67      * Specific InputStream implementation to use entry closing instead of
68      * stream closing.
69      */
70     private static class EntryInputStream extends InputStream {
71         private final ZipInputStream zin;
72
73         public EntryInputStream(final ZipInputStream zin) {
74             this.zin = zin;
75         }
76
77         @Override
78         public int available() throws IOException {
79             return zin.available();
80         }
81
82         @Override
83         public int read() throws IOException {
84             return zin.read();
85         }
86
87         @Override
88         public int read(byte[] b, int off, int len) throws IOException {
89             return zin.read(b, off, len);
90         }
91
92         @Override
93         public long skip(long n) throws IOException {
94             return zin.skip(n);
95         }
96
97         @Override
98         public void close() throws IOException {
99             zin.closeEntry();
100         }
101     }
102
103     /*
104      * XPath expression cache
105      */
106     private static XPathExpression XPATH_VERSION;
107
108     /*
109      * Package level data
110      */
111     private final File file;
112     private Document manifest;
113
114     /**
115      * Entries encoder/decoder stored in the encoding order
116      * <p>
117      * take care:
118      * <ul>
119      * <li>the order is the encoding order (from start to end)
120      * <li>decoding will be performed from the end to the start
121      */
122     private Entry[] availableEntries = new Entry[] { new ContentEntry(), new DictionaryEntry() };
123
124     /*
125      * Data to store or load into
126      */
127     private XcosDiagram content;
128     private final ScilabList dictionary;
129
130     /*
131      * External methods, to save/load a file
132      */
133
134     /**
135      * Default constructor
136      *
137      * @param file
138      *            the file to use on this package (see {@link #load()} and
139      *            {@link #store()})
140      * @throws ParserConfigurationException
141      *             on invalid DOM engine configuration
142      */
143     public XcosPackage(final File file) throws ParserConfigurationException {
144         this.file = file;
145         this.dictionary = new ScilabList();
146
147         manifest = ScilabDocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
148         final Element root = manifest.createElementNS("urn:scilab:xcos:xmlns:manifest:0.1", "manifest:manifest");
149         manifest.appendChild(root);
150     }
151
152     private boolean hasInvalidManifest() {
153         return manifest == null || manifest.getFirstChild() == null || !manifest.getFirstChild().hasChildNodes();
154     }
155
156     /**
157      * Load the {@link #file} into the internals
158      *
159      * @throws IOException
160      *             on I/O Exception or invalid format
161      * @throws SAXException
162      *             on SAX error
163      * @throws ParserConfigurationException
164      *             on invalid DOM engine configuration
165      * @throws TransformerException
166      */
167     public void load() throws IOException, SAXException, ParserConfigurationException, TransformerException {
168         if (hasInvalidManifest()) {
169             checkHeader();
170         }
171
172         // Decode using the specified order (from end to start)
173         for (int i = availableEntries.length - 1; 0 <= i;) {
174             Entry e = availableEntries[i];
175
176             // open the file on each entry to manage non well ordered (but still
177             // valid) zip files
178             final FileInputStream fis = new FileInputStream(file);
179             final ZipInputStream zin = new ZipInputStream(fis);
180
181             try {
182                 ZipEntry entry;
183                 while ((entry = zin.getNextEntry()) != null) {
184                     final String path = entry.getName();
185                     if (path.equals(e.getFullPath())) {
186                         // decode the current entry
187                         e.setup(this);
188                         e.load(entry, new EntryInputStream(zin));
189
190                         // try to decode the next entry (for well ordered zip,
191                         // the more common case)
192                         i--;
193                         if (0 <= i) {
194                             e = availableEntries[i];
195                         }
196                     }
197                 }
198             } finally {
199                 zin.close();
200             }
201         }
202     }
203
204     /**
205      * Check an xcos file as a ZIP package.
206      *
207      * @param file
208      *            the file to read
209      * @throws IOException
210      *             on I/O Exception or invalid format
211      * @throws TransformerException
212      *             on invalid XSLT engine configuration
213      */
214     public void checkHeader() throws IOException, TransformerException {
215         final FileInputStream fis = new FileInputStream(file);
216         final ZipInputStream zin = new ZipInputStream(fis);
217
218         ZipEntry entry;
219         try {
220             while ((entry = zin.getNextEntry()) != null) {
221                 // extract data
222                 // open output streams
223
224                 final String name = entry.getName();
225                 if (MIMETYPE.equals(name)) {
226                     if (entry.getSize() != MIME_BYTES.length) {
227                         throw new IOException(INVALID_MIMETYPE);
228                     }
229
230                     final byte[] buf = new byte[MIME_BYTES.length];
231                     zin.read(buf);
232
233                     if (!Arrays.equals(MIME_BYTES, buf)) {
234                         throw new IOException(INVALID_MIMETYPE);
235                     }
236                 } else if (META_INF_MANIFEST_XML.equals(name)) {
237                     final TransformerFactory tranFactory = ScilabTransformerFactory.newInstance();
238                     final Transformer aTransformer = tranFactory.newTransformer();
239
240                     // take care: to avoid closing input stream as the
241                     // transformer will close the input stream.
242                     final StreamSource src = new StreamSource(new EntryInputStream(zin));
243                     final DOMResult result = new DOMResult();
244                     aTransformer.transform(src, result);
245                     manifest = (Document) result.getNode();
246                 }
247             }
248         } finally {
249             zin.close();
250         }
251
252         if (hasInvalidManifest()) {
253             throw new IOException("META-INF/manifest.xml entry not found");
254         }
255     }
256
257     public void store() throws IOException {
258         final FileOutputStream fos = new FileOutputStream(file);
259         final ZipOutputStream zout = new ZipOutputStream(fos);
260
261         try {
262             // add the header (standard package)
263             storeHeader(zout);
264
265             // store the entries in encoding order
266             for (final Entry entry : availableEntries) {
267                 entry.setup(this);
268                 entry.store(zout);
269             }
270
271             // store the manifest file
272             storeTrailer(zout);
273         } catch (Exception e) {
274             e.printStackTrace();
275         } finally {
276             zout.close();
277         }
278     }
279
280     private void storeHeader(final ZipOutputStream zout) throws IOException {
281         /*
282          * Store header
283          */
284         final ZipEntry entry = new ZipEntry(MIMETYPE);
285         entry.setSize(MIME_BYTES.length);
286         final CRC32 crc = new CRC32();
287         crc.update(MIME_BYTES);
288         entry.setCrc(crc.getValue());
289         entry.setMethod(ZipEntry.STORED);
290         entry.setTime(0l);
291         zout.putNextEntry(entry);
292         zout.write(MIME_BYTES);
293
294         /*
295          * Add an entry to the manifest
296          */
297         final Element e = manifest.createElement("manifest:file-entry");
298         e.setAttribute("manifest:media-type", MIME);
299         e.setAttribute("manifest:version", VERSION);
300         e.setAttribute("manifest:full-path", "/");
301         manifest.getFirstChild().appendChild(e);
302     }
303
304     private void storeTrailer(final ZipOutputStream zout) throws IOException {
305         /*
306          * Append the entry
307          */
308         final ZipEntry entry = new ZipEntry(META_INF_MANIFEST_XML);
309         entry.setTime(0l);
310         zout.putNextEntry(entry);
311
312         /*
313          * Store the content
314          */
315         try {
316             final TransformerFactory tranFactory = ScilabTransformerFactory.newInstance();
317             final Transformer aTransformer = tranFactory.newTransformer();
318             aTransformer.setOutputProperty(OutputKeys.INDENT, "yes");
319             aTransformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "1");
320
321             final DOMSource src = new DOMSource(manifest);
322             final StreamResult result = new StreamResult(zout);
323             aTransformer.transform(src, result);
324         } catch (TransformerConfigurationException e) {
325             throw new IOException(e);
326         } catch (TransformerException e) {
327             throw new IOException(e);
328         }
329     }
330
331     /*
332      * Getters/setters for data
333      */
334     public Document getManifest() {
335         return manifest;
336     }
337
338     public void setContent(XcosDiagram content) {
339         this.content = content;
340     }
341
342     public XcosDiagram getContent() {
343         return content;
344     }
345
346     public ScilabList getDictionary() {
347         return dictionary;
348     }
349
350     /*
351      * Utilities
352      */
353
354     /**
355      * Get the manifest package version
356      *
357      * @return the package version or the default version in case of version not
358      *         found
359      */
360     public String getPackageVersion() {
361         // cache the xpath expression
362         if (XPATH_VERSION == null) {
363             final XPathFactory factory = ScilabXPathFactory.newInstance();
364             final XPath xpath = factory.newXPath();
365
366             try {
367                 XPATH_VERSION = xpath.compile("//manifest/file-entry[media-type='" + MIME + "']/@version");
368             } catch (XPathExpressionException e) {
369                 e.printStackTrace();
370             }
371         }
372
373         // evaluate the version
374         if (XPATH_VERSION != null) {
375             try {
376                 return XPATH_VERSION.evaluate(manifest, XPathConstants.STRING).toString();
377             } catch (XPathExpressionException e) {
378                 e.printStackTrace();
379             }
380         }
381
382         // fallback to the default
383         return VERSION;
384     }
385 }