/*
* 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;
}
}