/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 2008 jOpenDocument, by ILM Informatique. All rights reserved. * * The contents of this file are subject to the terms of the GNU * General Public License Version 3 only ("GPL"). * You may not use this file except in compliance with the License. * You can obtain a copy of the License at http://www.gnu.org/licenses/gpl-3.0.html * See the License for the specific language governing permissions and limitations under the License. * * When distributing the software, include this License Header Notice in each file. * */ package org.jopendocument.dom; import org.jopendocument.util.CopyUtils; import org.jopendocument.util.FileUtils; import org.jopendocument.util.StreamUtils; import org.jopendocument.util.StringInputStream; import org.jopendocument.util.Zip; import org.jopendocument.util.ZippedFilesProcessor; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.zip.ZipEntry; import org.jdom.Document; import org.jdom.Element; import org.jdom.JDOMException; import org.jdom.output.Format; import org.jdom.output.XMLOutputter; /** * An OpenDocument package, ie a zip containing XML documents and their associated files. * * @author ILM Informatique 2 août 2004 */ public class ODPackage { // use raw format, otherwise spaces are added to every spreadsheet cell private static final XMLOutputter OUTPUTTER = new XMLOutputter(Format.getRawFormat()); private static final Set<String> subdocNames; static { subdocNames = new HashSet<String>(); // section 2.1 of OpenDocument-v1.1-os.odt subdocNames.add("content.xml"); subdocNames.add("styles.xml"); subdocNames.add("meta.xml"); subdocNames.add("settings.xml"); } /** * Whether the passed entry is specific to a package. * * @param name a entry name, eg "mimetype" * @return <code>true</code> if <code>name</code> is a standard file, eg <code>true</code>. */ public static final boolean isStandardFile(final String name) { return name.equals("mimetype") || subdocNames.contains(name) || name.startsWith("Thumbnails") || name.startsWith("META-INF") || name.startsWith("Configurations"); } private final Map<String, ODPackageEntry> files; private ContentTypeVersioned type; private File file; public ODPackage() { this.files = new HashMap<String, ODPackageEntry>(); this.type = null; this.file = null; } public ODPackage(InputStream ins) throws IOException { this(); final ByteArrayOutputStream out = new ByteArrayOutputStream(4096); new ZippedFilesProcessor() { @Override protected void processEntry(ZipEntry entry, InputStream in) throws IOException { final String name = entry.getName(); final Object res; if (subdocNames.contains(name)) { try { res = new ODXMLDocument(OOUtils.getBuilder().build(in)); } catch (JDOMException e) { // always correct throw new IllegalStateException("parse error", e); } } else { out.reset(); StreamUtils.copy(in, out); res = out.toByteArray(); } // we don't know yet the types putFile(name, res, null, entry.getMethod() == ZipEntry.DEFLATED); } }.process(ins); // fill in the missing types from the manifest, if any final ODPackageEntry me = this.files.remove(Manifest.ENTRY_NAME); if (me != null) { final byte[] m = (byte[]) me.getData(); try { final Map<String, String> manifestEntries = Manifest.parse(new ByteArrayInputStream(m)); for (final Map.Entry<String, String> e : manifestEntries.entrySet()) { final String path = e.getKey(); final ODPackageEntry entry = this.files.get(path); // eg directory if (entry == null) this.files.put(path, new ODPackageEntry(path, e.getValue(), null)); else entry.setType(e.getValue()); } } catch (JDOMException e) { throw new IllegalArgumentException("bad manifest " + new String(m), e); } } } public ODPackage(File f) throws IOException { this(new BufferedInputStream(new FileInputStream(f), 512 * 1024)); this.file = f; } public ODPackage(ODPackage o) { this(); // ATTN this works because, all files are read upfront for (final String name : o.getEntries()) { final ODPackageEntry entry = o.getEntry(name); final Object data = entry.getData(); final Object myData; if (data instanceof byte[]) // assume byte[] are immutable myData = data; else if (data instanceof ODSingleXMLDocument) { myData = new ODSingleXMLDocument((ODSingleXMLDocument) data, this); } else { myData = CopyUtils.copy(data); } this.putFile(name, myData, entry.getType(), entry.isCompressed()); } this.file = o.file; } public final File getFile() { return this.file; } public final void setFile(File f) { this.file = this.addExt(f); } private final File addExt(File f) { final String ext = '.' + this.getContentType().getExtension(); if (!f.getName().endsWith(ext)) f = new File(f.getParentFile(), f.getName() + ext); return f; } /** * The version of this package, <code>null</code> if it cannot be found (eg this package is * empty, or contains no xml). * * @return the version of this package, can be <code>null</code>. */ public final XMLVersion getVersion() { final ODXMLDocument content = this.getContent(); if (content == null) return null; else return content.getVersion(); } /** * The type of this package, <code>null</code> if it cannot be found (eg this package is empty). * * @return the type of this package, can be <code>null</code>. */ public final ContentTypeVersioned getContentType() { if (this.type == null) { if (this.files.containsKey("mimetype")) this.type = ContentTypeVersioned.fromMime(new String(this.getBinaryFile("mimetype"))); else if (this.getVersion().equals(XMLVersion.OOo)) { final Element contentRoot = this.getContent().getDocument().getRootElement(); final String docClass = contentRoot.getAttributeValue("class", contentRoot.getNamespace("office")); this.type = ContentTypeVersioned.fromClass(docClass); } else if (this.getVersion().equals(XMLVersion.OD)) { final Element bodyChild = (Element) this.getContent().getChild("body").getChildren().get(0); this.type = ContentTypeVersioned.fromBody(bodyChild.getName()); } } return this.type; } public final String getMimeType() { return this.getContentType().getMimeType(); } /** * Call {@link OOXML#isValid(Document)} on each XML subdocuments. * * @return all problems indexed by subdocuments names, ie empty if all ok. */ public final Map<String, String> validateSubDocuments() { final OOXML ooxml = OOXML.get(getVersion()); final Map<String, String> res = new HashMap<String, String>(); for (final String s : subdocNames) { if (this.getEntries().contains(s)) { final String valid = ooxml.isValid(this.getDocument(s)); if (valid != null) res.put(s, valid); } } return res; } // *** getter on files public final Set<String> getEntries() { return this.files.keySet(); } public final ODPackageEntry getEntry(String entry) { return this.files.get(entry); } protected final Object getData(String entry) { final ODPackageEntry e = this.getEntry(entry); return e == null ? null : e.getData(); } public final byte[] getBinaryFile(String entry) { return (byte[]) this.getData(entry); } public final ODXMLDocument getXMLFile(String xmlEntry) { return (ODXMLDocument) this.getData(xmlEntry); } public final ODXMLDocument getXMLFile(final Document doc) { for (final String s : subdocNames) { final ODXMLDocument xmlFile = getXMLFile(s); if (xmlFile != null && xmlFile.getDocument() == doc) { return xmlFile; } } return null; } public final ODXMLDocument getContent() { return this.getXMLFile("content.xml"); } public final ODMeta getMeta() { final ODMeta meta; if (this.getEntries().contains("meta.xml")) meta = ODMeta.create(this.getXMLFile("meta.xml")); else meta = ODMeta.create(this.getContent()); return meta; } /** * Return an XML document. * * @param xmlEntry the filename, eg "styles.xml". * @return the matching document, or <code>null</code> if there's none. * @throws JDOMException if error about the XML. * @throws IOException if an error occurs while reading the file. */ public Document getDocument(String xmlEntry) { final ODXMLDocument xml = this.getXMLFile(xmlEntry); return xml == null ? null : xml.getDocument(); } /** * Find the passed automatic or common style referenced from the content. * * @param family the family, eg "paragraph". * @param name the name, eg "P1". * @return the corresponding XML element. */ public final Element getStyle(final String family, final String name) { return this.getStyle(this.getContent().getDocument(), family, name); } /** * Find the passed automatic or common style. NOTE : <code>referent</code> is needed because * there can exist automatic styles with the same name in both "content.xml" and "styles.xml". * * @param referent the document referencing the style. * @param family the family, eg "paragraph". * @param name the name, eg "P1". * @return the corresponding XML element. * @see ODXMLDocument#getStyle(String, String) */ public final Element getStyle(final Document referent, final String family, final String name) { // avoid searching in content then styles if it cannot be found if (name == null) return null; String refSubDoc = null; final String[] stylesContainer = new String[] { "content.xml", "styles.xml" }; for (final String subDoc : stylesContainer) if (this.getDocument(subDoc) == referent) refSubDoc = subDoc; if (refSubDoc == null) throw new IllegalArgumentException("neither in content nor styles : " + referent); Element res = this.getXMLFile(refSubDoc).getStyle(family, name); // if it isn't in content.xml it might be in styles.xml if (res == null && refSubDoc.equals(stylesContainer[0]) && this.getXMLFile(stylesContainer[1]) != null) res = this.getXMLFile(stylesContainer[1]).getStyle(family, name); return res; } // *** setter public void putFile(String entry, Object data) { this.putFile(entry, data, null); } public void putFile(final String entry, final Object data, final String mediaType) { this.putFile(entry, data, mediaType, true); } public void putFile(final String entry, final Object data, final String mediaType, final boolean compress) { if (entry == null) throw new NullPointerException("null name"); final Object myData; if (subdocNames.contains(entry)) { final ODXMLDocument oodoc; if (data instanceof Document) oodoc = new ODXMLDocument((Document) data); else oodoc = (ODXMLDocument) data; // si le package est vide n'importe quelle version convient if (this.getVersion() != null && !oodoc.getVersion().equals(this.getVersion())) throw new IllegalArgumentException("version mismatch " + this.getVersion() + " != " + oodoc); myData = oodoc; } else if (data != null && !(data instanceof byte[])) throw new IllegalArgumentException("should be byte[] for " + entry + ": " + data); else myData = data; final String inferredType = mediaType != null ? mediaType : FileUtils.findMimeType(entry); this.files.put(entry, new ODPackageEntry(entry, inferredType, myData, compress)); } public void rmFile(String entry) { this.files.remove(entry); } /** * Transform this to use a {@link ODSingleXMLDocument}. Ie after this method, only "content.xml" * remains and it's an instance of ODSingleXMLDocument. * * @return the created ODSingleXMLDocument. */ public ODSingleXMLDocument toSingle() { if (!this.isSingle()) { // this removes xml files used by OOSingleXMLDocument final Document content = removeAndGetDoc("content.xml"); final Document styles = removeAndGetDoc("styles.xml"); final Document settings = removeAndGetDoc("settings.xml"); final Document meta = removeAndGetDoc("meta.xml"); return ODSingleXMLDocument.createFromDocument(content, styles, settings, meta, this); } else return (ODSingleXMLDocument) this.getContent(); } public final boolean isSingle() { return this.getContent() instanceof ODSingleXMLDocument; } private Document removeAndGetDoc(String name) { if (!this.files.containsKey(name)) return null; final ODXMLDocument xmlDoc = (ODXMLDocument) this.files.remove(name).getData(); return xmlDoc == null ? null : xmlDoc.getDocument(); } // *** save public final void save(OutputStream out) throws IOException { final Zip z = new Zip(out); // magic number, see section 17.4 z.zipNonCompressed("mimetype", this.getMimeType().getBytes("UTF8")); final Manifest manifest = new Manifest(this.getVersion(), this.getMimeType()); for (final String name : this.files.keySet()) { // added at the end if (name.equals("mimetype") || name.equals(Manifest.ENTRY_NAME)) continue; final ODPackageEntry entry = this.files.get(name); final Object val = entry.getData(); if (val != null) { if (val instanceof ODXMLDocument) { final OutputStream o = z.createEntry(name); OUTPUTTER.output(((ODXMLDocument) val).getDocument(), o); o.close(); } else { z.zip(name, (byte[]) val, entry.isCompressed()); } } final String mediaType = entry.getType(); manifest.addEntry(name, mediaType == null ? "" : mediaType); } z.zip(Manifest.ENTRY_NAME, new StringInputStream(manifest.asString())); z.close(); } /** * Save the content of this package to our file, overwriting it if it exists. * * @return the saved file. * @throws IOException if an error occurs while saving. */ public File save() throws IOException { return this.saveAs(this.getFile()); } public File saveAs(final File fNoExt) throws IOException { final File f = this.addExt(fNoExt); if (f.getParentFile() != null) f.getParentFile().mkdirs(); // ATTN at this point, we must have read all the content of this file // otherwise we could save to File.createTempFile("oofd", null).deleteOnExit(); final FileOutputStream out = new FileOutputStream(f); final BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(out, 512 * 1024); try { this.save(bufferedOutputStream); } finally { bufferedOutputStream.close(); } return f; } }