Java tutorial
/* * 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 static org.jopendocument.dom.ODPackage.RootElement.CONTENT; import org.jopendocument.dom.ODPackage.RootElement; import org.jopendocument.dom.style.data.DataStyle; import org.jopendocument.util.Base64; import org.jopendocument.util.CollectionUtils; import org.jopendocument.util.FileUtils; import org.jopendocument.util.Tuple2; import org.jopendocument.util.cc.IFactory; import org.jopendocument.util.cc.IPredicate; import org.jopendocument.util.JDOMUtils; import org.jopendocument.util.SimpleXMLPath; import org.jopendocument.util.Step; import org.jopendocument.util.Step.Axis; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.math.BigDecimal; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import org.apache.commons.collections.Transformer; import org.jdom.Attribute; import org.jdom.DocType; import org.jdom.Document; import org.jdom.Element; import org.jdom.JDOMException; import org.jdom.Namespace; import org.jdom.xpath.XPath; /** * An XML document containing all of an office document, see section 2.1 of OpenDocument 1.1. * * @author Sylvain CUAZ 24 nov. 2004 */ public class ODSingleXMLDocument extends ODXMLDocument implements Cloneable { private static final String BASIC_LANG_NAME = "ooo:Basic"; private static final SimpleXMLPath<org.jdom.Attribute> ALL_HREF_ATTRIBUTES = SimpleXMLPath.allAttributes("href", "xlink"); private static final SimpleXMLPath<org.jdom.Element> ALL_BINARY_DATA_ELEMENTS = SimpleXMLPath .allElements("binary-data", "office"); // see 10.4.5 <office:binary-data> of OpenDocument-v1.2-os private static final Set<String> BINARY_DATA_PARENTS = CollectionUtils.createSet("draw:image", "draw:object-ole", "style:background-image", "text:list-level-style-image"); final static Set<String> DONT_PREFIX; static { DONT_PREFIX = new HashSet<String>(); // don't touch to user fields and variables // we want them to be the same across the document DONT_PREFIX.add("user-field-decl"); DONT_PREFIX.add("user-field-get"); DONT_PREFIX.add("variable-get"); DONT_PREFIX.add("variable-decl"); DONT_PREFIX.add("variable-set"); } // Voir le TODO du ctor // public static OOSingleXMLDocument createEmpty() { // } /** * Create a document from a collection of subdocuments. * * @param content the content. * @param style the styles, can be <code>null</code>. * @return the merged document. */ public static ODSingleXMLDocument createFromDocument(org.jdom.Document content, org.jdom.Document style) { return org.jopendocument.dom.ODPackage.createFromDocuments(content, style).toSingle(); } static ODSingleXMLDocument create(org.jopendocument.dom.ODPackage files) { final org.jdom.Document content = files.getContent().getDocument(); final org.jdom.Document style = files .getDocument(org.jopendocument.dom.ODPackage.RootElement.STYLES.getZipEntry()); // signal that the xml is a complete document (was document-content) final org.jdom.Document singleContent = org.jopendocument.dom.ODPackage.RootElement.createSingle(content); copyNS(content, singleContent); files.getContentType().setType(singleContent); final org.jdom.Element root = singleContent.getRootElement(); root.addContent(content.getRootElement().removeContent()); // see section 2.1.1 first meta, then settings, then the rest createScriptsElement(root, files); prependToRoot(files.getDocument(org.jopendocument.dom.ODPackage.RootElement.SETTINGS.getZipEntry()), root); prependToRoot(files.getDocument(org.jopendocument.dom.ODPackage.RootElement.META.getZipEntry()), root); final ODSingleXMLDocument single = new ODSingleXMLDocument(singleContent, files); if (single.getChild("body") == null) throw new IllegalArgumentException("no body in " + single); if (style != null) { // section 2.1 : Styles used in the document content and automatic styles used in the // styles themselves. // more precisely in section 2.1.1 : office:document-styles contains style, master // style, auto style, font decls ; the last two being also in content.xml but are *not* // related : eg P1 of styles.xml is *not* the P1 of content.xml try { single.mergeAllStyles(new ODXMLDocument(style), true); } catch (org.jdom.JDOMException e) { throw new IllegalArgumentException("style is not valid", e); } } return single; } private static void createScriptsElement(final org.jdom.Element root, final org.jopendocument.dom.ODPackage pkg) { final Map<String, Library> basicLibraries = pkg.readBasicLibraries(); if (basicLibraries.size() > 0) { final XMLFormatVersion formatVersion = pkg.getFormatVersion(); final org.jopendocument.dom.XMLVersion version = formatVersion.getXMLVersion(); final org.jdom.Namespace officeNS = version.getOFFICE(); // scripts must be before the body and automatic styles final org.jdom.Element scriptsElem = JDOMUtils.getOrCreateChild(root, formatVersion.getXML().getOfficeScripts(), officeNS, 0); final org.jdom.Element scriptElem = new org.jdom.Element(formatVersion.getXML().getOfficeScript(), officeNS); scriptElem.setAttribute("language", BASIC_LANG_NAME, version.getNS("script")); // script must be before events scriptsElem.addContent(0, scriptElem); final org.jdom.Element libsElem = new org.jdom.Element("libraries", version.getLibrariesNS()); for (final Library lib : basicLibraries.values()) { libsElem.addContent(lib.toFlatXML(formatVersion)); } scriptElem.addContent(libsElem); } } private static void prependToRoot(org.jdom.Document settings, final org.jdom.Element root) { if (settings != null) { copyNS(settings, root.getDocument()); final org.jdom.Element officeSettings = (org.jdom.Element) settings.getRootElement().getChildren() .get(0); root.addContent(0, (org.jdom.Element) officeSettings.clone()); } } // some namespaces are needed even if not directly used, see 18.3.19 namespacedToken // of v1.2-part1-cd04 (e.g. 19.31 config:name or 19.260 form:control-implementation) @SuppressWarnings("unchecked") private static void copyNS(final org.jdom.Document src, final org.jdom.Document dest) { JDOMUtils.addNamespaces(dest.getRootElement(), src.getRootElement().getAdditionalNamespaces()); } /** * Create a document from a package. * * @param f an OpenDocument package file. * @return the merged file. * @throws org.jdom.JDOMException if the file is not a valid OpenDocument file. * @throws java.io.IOException if the file can't be read. */ public static ODSingleXMLDocument createFromPackage(File f) throws org.jdom.JDOMException, IOException { // this loads all linked files return new org.jopendocument.dom.ODPackage(f).toSingle(); } /** * Create a document from a flat XML. * * @param f an OpenDocument XML file. * @return the created file. * @throws org.jdom.JDOMException if the file is not a valid OpenDocument file. * @throws java.io.IOException if the file can't be read. */ public static ODSingleXMLDocument createFromFile(File f) throws org.jdom.JDOMException, IOException { final ODSingleXMLDocument res = new ODSingleXMLDocument(OOUtils.getBuilder().build(f)); res.getPackage().setFile(f); return res; } public static ODSingleXMLDocument createFromStream(InputStream ins) throws org.jdom.JDOMException, IOException { return new ODSingleXMLDocument(OOUtils.getBuilder().build(ins)); } /** * fix bug when a SingleXMLDoc is used to create a document (for example with P2 and 1_P2), and * then create another instance s2 with the previous document and add a second file (also with * P2 and 1_P2) => s2 will contain P2, 1_P2, 1_P2, 1_1_P2. */ private static final String COUNT = "SingleXMLDocument_count"; /** Le nombre de fichiers concat */ private int numero; /** Les styles prsent dans ce document */ private final Set<String> stylesNames; /** Les styles de liste prsent dans ce document */ private final Set<String> listStylesNames; /** Les fichiers rfrencs par ce document */ private org.jopendocument.dom.ODPackage pkg; private final ODMeta meta; // the element between each page private org.jdom.Element pageBreak; public ODSingleXMLDocument(org.jdom.Document content) { this(content, null); } /** * A new single document. NOTE: this document will put himself in <code>pkg</code>, replacing * any previous content. * * @param content the XML. * @param pkg the package this document belongs to. */ private ODSingleXMLDocument(org.jdom.Document content, final org.jopendocument.dom.ODPackage pkg) { super(content); // inited in getPageBreak() this.pageBreak = null; final boolean contentIsFlat = pkg == null; this.pkg = contentIsFlat ? new org.jopendocument.dom.ODPackage() : pkg; if (!contentIsFlat) { final Set<String> toRm = new HashSet<String>(); for (final org.jopendocument.dom.ODPackage.RootElement e : org.jopendocument.dom.ODPackage.RootElement .getPackageElements()) toRm.add(e.getZipEntry()); for (final String e : this.pkg.getEntries()) { if (e.startsWith(Library.DIR_NAME)) toRm.add(e); } this.pkg.rmFiles(toRm); } this.pkg.putFile(CONTENT.getZipEntry(), this, "text/xml"); // update href if (contentIsFlat) { // OD thinks of the ZIP archive as an additional folder for (final org.jdom.Attribute hrefAttr : ALL_HREF_ATTRIBUTES .selectNodes(getDocument().getRootElement())) { final String href = hrefAttr.getValue(); if (!URI.create(href).isAbsolute()) hrefAttr.setValue("../" + href); } } // decode Base64 binaries for (final org.jdom.Element binaryDataElem : ALL_BINARY_DATA_ELEMENTS .selectNodes(getDocument().getRootElement())) { final String name; int i = 1; final Set<String> entries = getPackage().getEntries(); final org.jdom.Element binaryParentElement = binaryDataElem.getParentElement(); while (entries.contains(binaryParentElement.getName() + "/" + i)) i++; name = binaryParentElement.getName() + "/" + i; getPackage().putFile(name, Base64.decode(binaryDataElem.getText())); binaryParentElement.setAttribute("href", name, binaryDataElem.getNamespace("xlink")); binaryDataElem.detach(); } this.meta = this.getPackage().getMeta(true); final ODUserDefinedMeta userMeta = this.meta.getUserMeta(COUNT); if (userMeta != null) { final Object countValue = userMeta.getValue(); if (countValue instanceof Number) { this.numero = ((Number) countValue).intValue(); } else { this.numero = new BigDecimal(countValue.toString()).intValue(); } } else { // if not hasCount(), it's not us that created content // so there should not be any 1_ this.setNumero(0); } this.stylesNames = new HashSet<String>(64); this.listStylesNames = new HashSet<String>(16); // little trick to find the common styles names (not to be prefixed so they remain // consistent across the added documents) final org.jdom.Element styles = this.getChild("styles"); if (styles != null) { // create a second document with our styles to collect names final org.jdom.Element root = this.getDocument().getRootElement(); final org.jdom.Document clonedDoc = new org.jdom.Document( new org.jdom.Element(root.getName(), root.getNamespace())); clonedDoc.getRootElement().addContent(styles.detach()); try { this.mergeStyles(new ODXMLDocument(clonedDoc), true); } catch (org.jdom.JDOMException e) { throw new IllegalArgumentException("can't find common styles names."); } // reattach our styles styles.detach(); this.setChild(styles); } } ODSingleXMLDocument(ODSingleXMLDocument doc, org.jopendocument.dom.ODPackage p) { super(doc); if (p == null) throw new NullPointerException("Null package"); this.stylesNames = new HashSet<String>(doc.stylesNames); this.listStylesNames = new HashSet<String>(doc.listStylesNames); this.pkg = p; this.meta = ODMeta.create(this); this.setNumero(doc.numero); } @Override public ODSingleXMLDocument clone() { final org.jopendocument.dom.ODPackage copy = new org.jopendocument.dom.ODPackage(this.pkg); return (ODSingleXMLDocument) copy.getContent(); } private void setNumero(int numero) { this.numero = numero; this.meta.getUserMeta(COUNT, true).setValue(this.numero); } /** * The number of files concatenated with {@link #add(ODSingleXMLDocument)}. * * @return number of files concatenated. */ public final int getNumero() { return this.numero; } public org.jopendocument.dom.ODPackage getPackage() { return this.pkg; } private final org.jdom.Element getBasicScriptElem() { return this.getBasicScriptElem(false); } private final org.jdom.Element getBasicScriptElem(final boolean create) { final OOXML xml = getXML(); final String officeScripts = xml.getOfficeScripts(); final org.jdom.Element scriptsElem = this.getChild(officeScripts, create); if (scriptsElem == null) return null; final org.jdom.Namespace scriptNS = this.getVersion().getNS("script"); final org.jdom.Namespace officeNS = this.getVersion().getOFFICE(); @SuppressWarnings("unchecked") final List<org.jdom.Element> scriptElems = scriptsElem.getChildren(xml.getOfficeScript(), officeNS); for (final org.jdom.Element scriptElem : scriptElems) { if (scriptElem.getAttributeValue("language", scriptNS).equals(BASIC_LANG_NAME)) return scriptElem; } if (create) { final org.jdom.Element res = new org.jdom.Element(xml.getOfficeScript(), officeNS); res.setAttribute("language", BASIC_LANG_NAME, scriptNS); scriptsElem.addContent(res); return res; } else { return null; } } /** * Parse BASIC libraries in this flat XML. * * @return the BASIC libraries by name. */ public final Map<String, Library> readBasicLibraries() { return this.readBasicLibraries(this.getBasicScriptElem()).get0(); } private final Tuple2<Map<String, Library>, Map<String, org.jdom.Element>> readBasicLibraries( final org.jdom.Element scriptElem) { if (scriptElem == null) return Tuple2.create(Collections.<String, Library>emptyMap(), Collections.<String, org.jdom.Element>emptyMap()); final org.jdom.Namespace libNS = this.getVersion().getLibrariesNS(); final org.jdom.Namespace linkNS = this.getVersion().getNS("xlink"); final Map<String, Library> res = new HashMap<String, Library>(); final Map<String, org.jdom.Element> resElems = new HashMap<String, org.jdom.Element>(); @SuppressWarnings("unchecked") final List<org.jdom.Element> libsElems = scriptElem.getChildren("libraries", libNS); for (final org.jdom.Element libsElem : libsElems) { @SuppressWarnings("unchecked") final List<org.jdom.Element> libElems = libsElem.getChildren(); for (final org.jdom.Element libElem : libElems) { final Library library = Library.fromFlatXML(libElem, this.getPackage(), linkNS); if (library != null) { if (res.put(library.getName(), library) != null) throw new IllegalStateException("Duplicate library named " + library.getName()); resElems.put(library.getName(), libElem); } } } return Tuple2.create(res, resElems); } /** * Append a document. * * @param doc the document to add. */ public synchronized void add(ODSingleXMLDocument doc) { // ajoute un saut de page entre chaque document this.add(doc, true); } /** * Append a document. * * @param doc the document to add, <code>null</code> means no-op. * @param pageBreak whether a page break should be inserted before <code>doc</code>. */ public synchronized void add(ODSingleXMLDocument doc, boolean pageBreak) { if (doc != null && pageBreak) // only add a page break, if a page was really added this.getBody().addContent(this.getPageBreak()); this.add(null, 0, doc); } public synchronized void replace(org.jdom.Element elem, ODSingleXMLDocument doc) { final org.jdom.Element parent = elem.getParentElement(); this.add(parent, parent.indexOf(elem), doc); elem.detach(); } public synchronized void add(org.jdom.Element where, int index, ODSingleXMLDocument doc) { if (doc == null) return; if (!this.getVersion().equals(doc.getVersion())) throw new IllegalArgumentException("version mismatch"); this.setNumero(this.numero + 1); try { copyNS(doc.getDocument(), this.getDocument()); this.mergeEmbedded(doc); this.mergeSettings(doc); this.mergeAllStyles(doc, false); this.mergeBody(where, index, doc); } catch (org.jdom.JDOMException exn) { throw new IllegalArgumentException("XML error", exn); } } /** * Merge the four elements of style. * * @param doc the xml document to merge. * @param sameDoc whether <code>doc</code> is the same OpenDocument than this, eg * <code>true</code> when merging content.xml and styles.xml. * @throws org.jdom.JDOMException if an error occurs. */ private void mergeAllStyles(ODXMLDocument doc, boolean sameDoc) throws org.jdom.JDOMException { // no reference this.mergeFontDecls(doc); // section 14.1 // Parent Style only refer to other common styles // Next Style cannot refer to an autostyle (only available in common styles) // List Style can refer to an autostyle // Master Page Name cannot (auto master pages does not exist) // Data Style Name (for cells) can // but since the UI for common styles doesn't allow to customize List Style // and there is no common styles for tables : office:styles doesn't reference any automatic // styles this.mergeStyles(doc, sameDoc); // on the contrary autostyles do refer to other autostyles : // choosing "activate bullets" will create an automatic paragraph style:style // referencing an automatic text:list-style. this.mergeAutoStyles(doc, !sameDoc); // section 14.4 // Page Layout can refer to an autostyle // Next Style Name refer to another masterPage this.mergeMasterStyles(doc, !sameDoc); } private void mergeEmbedded(ODSingleXMLDocument doc) { // since we are adding another document our existing thumbnail is obsolete this.pkg.rmFile("Thumbnails/thumbnail.png"); this.pkg.rmFile("layout-cache"); // copy the files (only non generated files, e.g. content.xml will be merged later) for (final String name : doc.pkg.getEntries()) { final ODPackageEntry e = doc.pkg.getEntry(name); if (!org.jopendocument.dom.ODPackage.isStandardFile(e.getName())) { this.pkg.putCopy(e, this.prefix(e.getName())); } } } private void mergeSettings(ODSingleXMLDocument doc) throws org.jdom.JDOMException { final String elemName = "settings"; if (this.getChild(elemName, false) == null) { final org.jdom.Element other = doc.getChild(elemName, false); if (other != null) { this.getChild(elemName, true).addContent(other.cloneContent()); } } } /** * Add the passed libraries to this document. Passed libraries with the same content as existing * ones are ignored. * * @param libraries what to add. * @return the actually added libraries. * @throws IllegalArgumentException if <code>libraries</code> contains duplicates or if it * cannot be merged into this. * @see Library#canBeMerged(Library) */ public final Set<String> addBasicLibraries(final Collection<? extends Library> libraries) { return this.addBasicLibraries(Library.toMap(libraries)); } public final Set<String> addBasicLibraries(final org.jopendocument.dom.ODPackage pkg) { if (pkg == this.pkg) return Collections.emptySet(); return this.addBasicLibraries(pkg.readBasicLibraries()); } final Set<String> addBasicLibraries(final Map<String, Library> oLibraries) { if (oLibraries.size() == 0) return Collections.emptySet(); final Tuple2<Map<String, Library>, Map<String, org.jdom.Element>> thisLibrariesAndElements = this .readBasicLibraries(this.getBasicScriptElem(false)); final Map<String, Library> thisLibraries = thisLibrariesAndElements.get0(); final Map<String, org.jdom.Element> thisLibrariesElements = thisLibrariesAndElements.get1(); // check that the libraries to add which are already in us can be merged (no elements // conflict) final Set<String> duplicateLibs = Library.canBeMerged(thisLibraries, oLibraries); final Set<String> newLibs = new HashSet<String>(oLibraries.keySet()); newLibs.removeAll(thisLibraries.keySet()); // merge modules for (final String duplicateLib : duplicateLibs) { final Library thisLib = thisLibraries.get(duplicateLib); final Library oLib = oLibraries.get(duplicateLib); assert thisLib != null && oLib != null : "Not duplicate " + duplicateLib; oLib.mergeToFlatXML(this.getFormatVersion(), thisLib, thisLibrariesElements.get(duplicateLib)); } if (newLibs.size() > 0) { final org.jdom.Element thisScriptElem = this.getBasicScriptElem(true); final org.jdom.Element librariesElem = JDOMUtils.getOrCreateChild(thisScriptElem, "libraries", this.getVersion().getLibrariesNS()); for (final String newLib : newLibs) librariesElem.addContent(oLibraries.get(newLib).toFlatXML(this.getFormatVersion())); } // merge dialogs for (final Library oLib : oLibraries.values()) { final String libName = oLib.getName(); // can be null final Library thisLib = thisLibraries.get(libName); oLib.mergeDialogs(this.getPackage(), thisLib); } return newLibs; } /** * Remove the passed libraries. * * @param libraries which libraries to remove. * @return the actually removed libraries. */ public final Set<String> removeBasicLibraries(final Collection<String> libraries) { final Map<String, org.jdom.Element> thisLibrariesElements = this .readBasicLibraries(this.getBasicScriptElem(false)).get1(); final Set<String> res = new HashSet<String>(); for (final String libToRm : libraries) { final org.jdom.Element elemToRm = thisLibrariesElements.get(libToRm); if (elemToRm != null) { elemToRm.detach(); res.add(libToRm); } if (Library.removeFromPackage(this.getPackage(), libToRm)) res.add(libToRm); } return res; } /** * Fusionne les office:font-decls/style:font-decl. On ne prfixe jamais, on ajoute seulement si * l'attribut style:name est diffrent. * * @param doc le document fusionner avec celui-ci. * @throws org.jdom.JDOMException */ private void mergeFontDecls(ODXMLDocument doc) throws org.jdom.JDOMException { final String[] fontDecls = this.getFontDecls(); this.mergeUnique(doc, fontDecls[0], fontDecls[1]); } private String[] getFontDecls() { return getXML().getFontDecls(); } // merge everything under office:styles private void mergeStyles(ODXMLDocument doc, boolean sameDoc) throws org.jdom.JDOMException { // les default-style (notamment tab-stop-distance) this.mergeUnique(doc, "styles", "style:default-style", "style:family", NOP_ElementTransformer); // data styles // we have to prefix data styles since they're automatically generated by LO // (e.g. user created cellStyle1, LO generated dataStyleN0 in a document, then in another // document created cellStyle2, LO also generated dataStyleN0) // MAYBE search for orphans that discarded (same name) styles might leave // Don't prefix if we're merging styles into content (content contains no styles, so there // can be no collision ; also we don't prefix the body) final String dsNS = "number"; final boolean prefixDataStyles = !sameDoc; final List<org.jdom.Element> addedDataStyles = this.addStyles(doc, "styles", Step.createElementStep(null, dsNS, new IPredicate<org.jdom.Element>() { private final Set<String> names; { this.names = new HashSet<String>( org.jopendocument.dom.style.data.DataStyle.DATA_STYLES.size()); for (final Class<? extends org.jopendocument.dom.style.data.DataStyle> cl : org.jopendocument.dom.style.data.DataStyle.DATA_STYLES) { final StyleDesc<? extends org.jopendocument.dom.style.data.DataStyle> styleDesc = org.jopendocument.dom.Style .getStyleDesc(cl, getVersion()); this.names.add(styleDesc.getElementName()); assert styleDesc.getElementNS().getPrefix().equals(dsNS) : styleDesc; } } @Override public boolean evaluateChecked(org.jdom.Element elem) { return this.names.contains(elem.getName()); } }), prefixDataStyles); if (prefixDataStyles) { // data styles reference each other (e.g. style:map) final SimpleXMLPath<org.jdom.Attribute> simplePath = SimpleXMLPath.allAttributes("apply-style-name", "style"); for (final org.jdom.Attribute attr : simplePath.selectNodes(addedDataStyles)) { attr.setValue(prefix(attr.getValue())); } } // les styles // if we prefixed data styles, we must prefix references this.stylesNames.addAll(this.mergeUnique(doc, "styles", "style:style", !prefixDataStyles ? NOP_ElementTransformer : new ElementTransformer() { @Override public org.jdom.Element transform(org.jdom.Element elem) throws org.jdom.JDOMException { final org.jdom.Attribute attr = elem.getAttribute("data-style-name", elem.getNamespace()); if (attr != null) attr.setValue(prefix(attr.getValue())); return elem; } })); // on ajoute outline-style si non prsent this.addStylesIfNotPresent(doc, "outline-style"); // les list-style this.listStylesNames.addAll(this.mergeUnique(doc, "styles", "text:list-style")); // les *notes-configuration if (getVersion() == org.jopendocument.dom.XMLVersion.OOo) { this.addStylesIfNotPresent(doc, "footnotes-configuration"); this.addStylesIfNotPresent(doc, "endnotes-configuration"); } else { // 16.29.3 : specifies values for each note class used in a document this.mergeUnique(doc, "styles", "text:notes-configuration", "text:note-class", NOP_ElementTransformer); } this.addStylesIfNotPresent(doc, "bibliography-configuration"); this.addStylesIfNotPresent(doc, "linenumbering-configuration"); } /** * Fusionne les office:automatic-styles, on prfixe tout. * * @param doc le document fusionner avec celui-ci. * @param ref whether to prefix hrefs. * @throws org.jdom.JDOMException */ private void mergeAutoStyles(ODXMLDocument doc, boolean ref) throws org.jdom.JDOMException { final List<org.jdom.Element> addedStyles = this.prefixAndAddAutoStyles(doc); for (final org.jdom.Element addedStyle : addedStyles) { this.prefix(addedStyle, ref); } } /** * Fusionne les office:master-styles. On ne prfixe jamais, on ajoute seulement si l'attribut * style:name est diffrent. * * @param doc le document fusionner avec celui-ci. * @param ref whether to prefix hrefs. * @throws org.jdom.JDOMException if an error occurs. */ private void mergeMasterStyles(ODXMLDocument doc, boolean ref) throws org.jdom.JDOMException { // est rfrenc dans les styles avec "style:master-page-name" this.mergeUnique(doc, "master-styles", "style:master-page", ref ? this.prefixTransf : this.prefixTransfNoRef); } /** * Fusionne les corps. * * @param doc le document fusionner avec celui-ci. * @throws org.jdom.JDOMException */ private void mergeBody(org.jdom.Element where, int index, ODSingleXMLDocument doc) throws org.jdom.JDOMException { // copy forms from doc to this final String formsName = "forms"; final org.jdom.Namespace formsNS = getVersion().getOFFICE(); final String bodyPath = this.getContentTypeVersioned().getBodyPath(); // office:forms?,text:variable-decls?,text:sequence-decls?,text:user-field-decls? this.add(new IFactory<org.jdom.Element>() { @Override public org.jdom.Element createChecked() { return JDOMUtils.getOrCreateChild(getBody(), "user-field-decls", getVersion().getTEXT(), 0); } }, -1, doc, bodyPath + "/" + "text" + ":" + "user-field-decls", new ElementTransformer() { public org.jdom.Element transform(org.jdom.Element elem) throws org.jdom.JDOMException { System.err.println( "ODSingleXMLDocument.mergeBody(...).new ElementTransformer() {...}.transform() " + elem); // user fields are global to a document, they do not vary across it. // hence they are initialized at declaration // we should assure that there's no 2 declaration with the same name detachDuplicate(elem); return ODSingleXMLDocument.this.prefixTransf.transform(elem); } }); this.add(new IFactory<org.jdom.Element>() { @Override public org.jdom.Element createChecked() { final org.jdom.Element ourForms = getBody().getChild(formsName, formsNS); if (ourForms != null) { return ourForms; } else { final org.jdom.Element res = new org.jdom.Element(formsName, formsNS); // forms should be the first child of the body getBody().addContent(0, res); return res; } } }, -1, doc, bodyPath + "/" + formsNS.getPrefix() + ":" + formsName, this.prefixTransf); this.add(where, index, doc, bodyPath, new ElementTransformer() { public org.jdom.Element transform(org.jdom.Element elem) throws org.jdom.JDOMException { // ATTN n'ajoute pas sequence-decls // forms already added above if (elem.getName().equals("sequence-decls") || (elem.getName().equals(formsName) && elem.getNamespace().equals(formsNS))) return null; if (elem.getName().equals("user-field-decls")) { return null; } if (elem.getName().equals("variable-decls")) { // variables are not initialized at declaration // we should still assure that there's no 2 declaration with the same name detachDuplicate(elem); } // par dfaut return ODSingleXMLDocument.this.prefixTransf.transform(elem); } }); } /** * Detach the children of elem whose names already exist in the body. * * @param elem the elem to be trimmed. * @throws org.jdom.JDOMException if an error occurs. */ protected final void detachDuplicate(org.jdom.Element elem) throws org.jdom.JDOMException { final String singularName = elem.getName().substring(0, elem.getName().length() - 1); final List thisNames = getXPath("./text:" + singularName + "s/text:" + singularName + "/@text:name") .selectNodes(getChild("body")); org.apache.commons.collections.CollectionUtils.transform(thisNames, new Transformer() { public Object transform(Object obj) { return ((org.jdom.Attribute) obj).getValue(); } }); final Iterator iter = elem.getChildren().iterator(); while (iter.hasNext()) { final org.jdom.Element decl = (org.jdom.Element) iter.next(); if (thisNames.contains(decl.getAttributeValue("name", getVersion().getTEXT()))) { // on retire les dj existant iter.remove(); } } } // *** Utils public final org.jdom.Element getBody() { return this.getContentTypeVersioned().getBody(getDocument()); } private ContentTypeVersioned getContentTypeVersioned() { return getPackage().getContentType(); } /** * Prfixe les attributs en ayant besoin. * * @param elem l'lment prfixer. * @param references whether to prefix hrefs. * @throws org.jdom.JDOMException if an error occurs. */ void prefix(org.jdom.Element elem, boolean references) throws org.jdom.JDOMException { Iterator attrs = this.getXPath( ".//@text:style-name | .//@table:style-name | .//@draw:style-name | .//@style:data-style-name | .//@style:apply-style-name") .selectNodes(elem).iterator(); while (attrs.hasNext()) { org.jdom.Attribute attr = (org.jdom.Attribute) attrs.next(); // text:list/@text:style-name references text:list-style if (!this.listStylesNames.contains(attr.getValue()) && !this.stylesNames.contains(attr.getValue())) { attr.setValue(this.prefix(attr.getValue())); } } attrs = this.getXPath(".//@style:list-style-name").selectNodes(elem).iterator(); while (attrs.hasNext()) { org.jdom.Attribute attr = (org.jdom.Attribute) attrs.next(); if (!this.listStylesNames.contains(attr.getValue())) { attr.setValue(this.prefix(attr.getValue())); } } attrs = this.getXPath( ".//@style:page-master-name | .//@style:page-layout-name | .//@text:name | .//@form:name | .//@form:property-name") .selectNodes(elem).iterator(); while (attrs.hasNext()) { final org.jdom.Attribute attr = (org.jdom.Attribute) attrs.next(); final String parentName = attr.getParent().getName(); if (!DONT_PREFIX.contains(parentName)) attr.setValue(this.prefix(attr.getValue())); } // prefix references if (references) { attrs = this.getXPath(".//@xlink:href[../@xlink:show='embed']").selectNodes(elem).iterator(); while (attrs.hasNext()) { final org.jdom.Attribute attr = (org.jdom.Attribute) attrs.next(); final String prefixedPath = this.prefixPath(attr.getValue()); if (prefixedPath != null) attr.setValue(prefixedPath); } } } /** * Prefix a path. * * @param href a path inside the pkg, eg "./Object 1/content.xml". * @return the prefixed path or <code>null</code> if href is external, eg "./3_Object * 1/content.xml". */ private String prefixPath(final String href) { if (this.getVersion().equals(org.jopendocument.dom.XMLVersion.OOo)) { // in OOo 1.x inPKG is denoted by a # final boolean sharp = href.startsWith("#"); if (sharp) // eg #Pictures/100000000000006C000000ABCC02339E.png return "#" + this.prefix(href.substring(1)); else // eg ../../../../Program%20Files/OpenOffice.org1.1.5/share/gallery/apples.gif return null; } else { URI uri; try { uri = new URI(href); } catch (URISyntaxException e) { // OO doesn't escape characters for files uri = null; } // section 17.5 final boolean inPKGFile = uri == null || uri.getScheme() == null && uri.getAuthority() == null && uri.getPath().charAt(0) != '/'; if (inPKGFile) { final String dotSlash = "./"; if (href.startsWith(dotSlash)) return dotSlash + this.prefix(href.substring(dotSlash.length())); else return this.prefix(href); } else return null; } } private String prefix(String value) { return "_" + this.numero + value; } private final ElementTransformer prefixTransf = new ElementTransformer() { public org.jdom.Element transform(org.jdom.Element elem) throws org.jdom.JDOMException { ODSingleXMLDocument.this.prefix(elem, true); return elem; } }; private final ElementTransformer prefixTransfNoRef = new ElementTransformer() { public org.jdom.Element transform(org.jdom.Element elem) throws org.jdom.JDOMException { ODSingleXMLDocument.this.prefix(elem, false); return elem; } }; /** * Ajoute dans ce document seulement les lments de doc correspondant au XPath spcifi et dont * la valeur de l'attribut style:name n'existe pas dj. * * @param doc le document fusionner avec celui-ci. * @param topElem eg "office:font-decls". * @param elemToMerge les lments fusionner (par rapport topElem), eg "style:font-decl". * @return les noms des lments ajouts. * @throws org.jdom.JDOMException * @see #mergeUnique(ODSingleXMLDocument, String, String, org.jopendocument.dom.ODXMLDocument.ElementTransformer) */ private List<String> mergeUnique(ODXMLDocument doc, String topElem, String elemToMerge) throws org.jdom.JDOMException { return this.mergeUnique(doc, topElem, elemToMerge, NOP_ElementTransformer); } /** * Ajoute dans ce document seulement les lments de doc correspondant au XPath spcifi et dont * la valeur de l'attribut style:name n'existe pas dj. En consquence n'ajoute que les * lments possdant un attribut style:name. * * @param doc le document fusionner avec celui-ci. * @param topElem eg "office:font-decls". * @param elemToMerge les lments fusionner (par rapport topElem), eg "style:font-decl". * @param addTransf la transformation appliquer avant d'ajouter. * @return les noms des lments ajouts. * @throws org.jdom.JDOMException */ private List<String> mergeUnique(ODXMLDocument doc, String topElem, String elemToMerge, ElementTransformer addTransf) throws org.jdom.JDOMException { return this.mergeUnique(doc, topElem, elemToMerge, "style:name", addTransf); } private List<String> mergeUnique(ODXMLDocument doc, String topElem, String elemToMerge, String attrFQName, ElementTransformer addTransf) throws org.jdom.JDOMException { List<String> added = new ArrayList<String>(); org.jdom.Element thisParent = this.getChild(topElem, true); org.jdom.xpath.XPath xp = this.getXPath("./" + elemToMerge + "/@" + attrFQName); // les styles de ce document List thisElemNames = xp.selectNodes(thisParent); // on transforme la liste d'attributs en liste de String org.apache.commons.collections.CollectionUtils.transform(thisElemNames, new Transformer() { public Object transform(Object obj) { return ((org.jdom.Attribute) obj).getValue(); } }); // pour chaque style de l'autre document Iterator otherElemNames = xp.selectNodes(doc.getChild(topElem)).iterator(); while (otherElemNames.hasNext()) { org.jdom.Attribute attr = (org.jdom.Attribute) otherElemNames.next(); // on l'ajoute si non dj dedans if (!thisElemNames.contains(attr.getValue())) { thisParent.addContent(addTransf.transform((org.jdom.Element) attr.getParent().clone())); added.add(attr.getValue()); } } return added; } /** * Ajoute l'lment elemName de doc, s'il n'est pas dans ce document. * * @param doc le document fusionner avec celui-ci. * @param elemName l'lment ajouter, eg "outline-style". * @throws org.jdom.JDOMException if elemName is not valid. */ private void addStylesIfNotPresent(ODXMLDocument doc, String elemName) throws org.jdom.JDOMException { this.addIfNotPresent(doc, "./office:styles/text:" + elemName); } /** * Prefixe les fils de auto-styles possdant un attribut "name" avant de les ajouter. * * @param doc le document fusionner avec celui-ci. * @return les lment ayant t ajouts. * @throws org.jdom.JDOMException */ private List<org.jdom.Element> prefixAndAddAutoStyles(ODXMLDocument doc) throws org.jdom.JDOMException { return addStyles(doc, "automatic-styles", Step.getAnyChildElementStep(), true); } // add styles from doc/rootElem/styleElemStep/@style:name, optionally prefixing private List<org.jdom.Element> addStyles(ODXMLDocument doc, final String rootElem, final Step<org.jdom.Element> styleElemStep, boolean prefix) throws org.jdom.JDOMException { // needed since we add to us directly under rootElem if (styleElemStep.getAxis() != Axis.child) throw new IllegalArgumentException("Not child axis : " + styleElemStep.getAxis()); final List<org.jdom.Element> result = new ArrayList<org.jdom.Element>(128); final org.jdom.Element thisChild = this.getChild(rootElem); // find all elements with a style:name in doc final SimpleXMLPath<org.jdom.Attribute> simplePath = SimpleXMLPath.create(styleElemStep, Step.createAttributeStep("name", "style")); for (final org.jdom.Attribute attr : simplePath.selectNodes(doc.getChild(rootElem))) { final org.jdom.Element parent = (org.jdom.Element) attr.getParent().clone(); // prefix their name if (prefix) parent.setAttribute(attr.getName(), this.prefix(attr.getValue()), attr.getNamespace()); // and add to us thisChild.addContent(parent); result.add(parent); } return result; } /** * Return <code>true</code> if this document was split. * * @return <code>true</code> if this has no package anymore. * @see org.jopendocument.dom.ODPackage#split() */ public final boolean isDead() { return this.getPackage() == null; } final Map<org.jopendocument.dom.ODPackage.RootElement, org.jdom.Document> split() { final Map<org.jopendocument.dom.ODPackage.RootElement, org.jdom.Document> res = new HashMap<org.jopendocument.dom.ODPackage.RootElement, org.jdom.Document>(); final org.jopendocument.dom.XMLVersion version = getVersion(); final org.jdom.Element root = this.getDocument().getRootElement(); final XMLFormatVersion officeVersion = getFormatVersion(); // meta { final org.jdom.Element thisMeta = root.getChild("meta", version.getOFFICE()); if (thisMeta != null) { final org.jdom.Document meta = createDocument(res, org.jopendocument.dom.ODPackage.RootElement.META, officeVersion); meta.getRootElement().addContent(thisMeta.detach()); } } // settings { final org.jdom.Element thisSettings = root.getChild("settings", version.getOFFICE()); if (thisSettings != null) { final org.jdom.Document settings = createDocument(res, org.jopendocument.dom.ODPackage.RootElement.SETTINGS, officeVersion); settings.getRootElement().addContent(thisSettings.detach()); } } // scripts { final org.jdom.Element thisScript = this.getBasicScriptElem(); if (thisScript != null) { final Map<String, Library> basicLibraries = this.readBasicLibraries(thisScript).get0(); final org.jdom.Element lcRootElem = new org.jdom.Element("libraries", org.jopendocument.dom.XMLVersion.LIBRARY_NS); for (final Library lib : basicLibraries.values()) { lcRootElem.addContent(lib.toPackageLibrariesElement(officeVersion)); for (final Entry<String, org.jdom.Document> e : lib.toPackageDocuments(officeVersion) .entrySet()) { this.pkg.putFile(Library.DIR_NAME + "/" + lib.getName() + "/" + e.getKey(), e.getValue(), FileUtils.XML_TYPE); } } final org.jdom.Document lc = new org.jdom.Document(lcRootElem, new org.jdom.DocType( "library:libraries", "-//OpenOffice.org//DTD OfficeDocument 1.0//EN", "libraries.dtd")); this.pkg.putFile(Library.DIR_NAME + "/" + Library.LIBRARY_LIST_FILENAME, lc, FileUtils.XML_TYPE); thisScript.detach(); // nothing to do for dialogs, since they cannot be in our Document } } // styles // we must move office:styles, office:master-styles and referenced office:automatic-styles { final org.jdom.Document styles = createDocument(res, org.jopendocument.dom.ODPackage.RootElement.STYLES, officeVersion); // don't bother finding out which font is used where since there isn't that many of them styles.getRootElement() .addContent((org.jdom.Element) root.getChild(getFontDecls()[0], version.getOFFICE()).clone()); // extract common styles styles.getRootElement().addContent(root.getChild("styles", version.getOFFICE()).detach()); // only automatic styles used in the styles themselves. final org.jdom.Element contentAutoStyles = root.getChild("automatic-styles", version.getOFFICE()); final org.jdom.Element stylesAutoStyles = new org.jdom.Element(contentAutoStyles.getName(), contentAutoStyles.getNamespace()); final org.jdom.Element masterStyles = root.getChild("master-styles", version.getOFFICE()); // style elements referenced, e.g. <style:page-layout style:name="pm1"> final Set<org.jdom.Element> referenced = new HashSet<org.jdom.Element>(); final SimpleXMLPath<org.jdom.Attribute> descAttrs = SimpleXMLPath.create( Step.createElementStep(Axis.descendantOrSelf, null), Step.createAttributeStep(null, null)); for (final org.jdom.Attribute attr : descAttrs.selectNodes(masterStyles)) { final org.jdom.Element referencedStyleElement = org.jopendocument.dom.Style .getReferencedStyleElement(this.pkg, attr); if (referencedStyleElement != null) referenced.add(referencedStyleElement); } for (final org.jdom.Element r : referenced) { // since we already removed common styles assert r.getParentElement() == contentAutoStyles; stylesAutoStyles.addContent(r.detach()); } styles.getRootElement().addContent(stylesAutoStyles); styles.getRootElement().addContent(masterStyles.detach()); } // content { // store before emptying package final ContentTypeVersioned contentTypeVersioned = getContentTypeVersioned(); // needed since the content will be emptied (which can cause methods of ODPackage to // fail, e.g. setTypeAndVersion()) this.pkg.rmFile(org.jopendocument.dom.ODPackage.RootElement.CONTENT.getZipEntry()); this.pkg = null; final org.jdom.Document content = createDocument(res, org.jopendocument.dom.ODPackage.RootElement.CONTENT, officeVersion); contentTypeVersioned.setType(content); content.getRootElement().addContent(root.removeContent()); } return res; } private org.jdom.Document createDocument( final Map<org.jopendocument.dom.ODPackage.RootElement, org.jdom.Document> res, org.jopendocument.dom.ODPackage.RootElement rootElement, final XMLFormatVersion version) { final org.jdom.Document doc = rootElement.createDocument(version); copyNS(this.getDocument(), doc); res.put(rootElement, doc); return doc; } /** * Saves this OO document to a file. * * @param f the file where this document will be saved, without extension, eg "dir/myfile". * @return the actual file where it has been saved (with extension), eg "dir/myfile.odt". * @throws java.io.IOException if an error occurs. */ public File saveToPackageAs(File f) throws IOException { return this.pkg.saveAs(f); } public File save() throws IOException { return this.saveAs(this.getPackage().getFile()); } public File saveAs(File fNoExt) throws IOException { final org.jdom.Document doc = (org.jdom.Document) getDocument().clone(); for (final org.jdom.Attribute hrefAttr : ALL_HREF_ATTRIBUTES.selectNodes(doc.getRootElement())) { final String href = hrefAttr.getValue(); if (href.startsWith("../")) { // update href hrefAttr.setValue(href.substring(3)); } else if (!URI.create(href).isAbsolute()) { // encode binaries final org.jdom.Element hrefParent = hrefAttr.getParent(); if (!BINARY_DATA_PARENTS.contains(hrefParent.getQualifiedName())) throw new IllegalStateException("Cannot convert to binary data element : " + hrefParent); final org.jdom.Element binaryData = new org.jdom.Element("binary-data", getPackage().getVersion().getOFFICE()); binaryData.setText(Base64.encodeBytes(getPackage().getBinaryFile(href))); hrefParent.addContent(binaryData); // If this element is present, an xlink:href attribute in its parent element // shall be ignored. But LO doesn't respect that hrefAttr.detach(); } } final File f = this.getPackage().getContentType().addExt(fNoExt, true); FileUtils.write(org.jopendocument.dom.ODPackage.createOutputter().outputString(doc), f); return f; } private org.jdom.Element getPageBreak() { if (this.pageBreak == null) { final String styleName = "PageBreak"; try { final org.jdom.xpath.XPath xp = this.getXPath("./style:style[@style:name='" + styleName + "']"); final org.jdom.Element styles = this.getChild("styles", true); if (xp.selectSingleNode(styles) == null) { final org.jdom.Element pageBreakStyle = new org.jdom.Element("style", this.getVersion().getSTYLE()); pageBreakStyle.setAttribute("name", styleName, this.getVersion().getSTYLE()); pageBreakStyle.setAttribute("family", "paragraph", this.getVersion().getSTYLE()); pageBreakStyle.setContent( getPProps().setAttribute("break-after", "page", this.getVersion().getNS("fo"))); // <element name="office:styles"> <interleave>... // so just append the new style styles.addContent(pageBreakStyle); this.stylesNames.add(styleName); } } catch (org.jdom.JDOMException e) { // static path, shouldn't happen throw new IllegalStateException("pb while searching for " + styleName, e); } this.pageBreak = new org.jdom.Element("p", this.getVersion().getTEXT()).setAttribute("style-name", styleName, this.getVersion().getTEXT()); } return (org.jdom.Element) this.pageBreak.clone(); } private final org.jdom.Element getPProps() { return this.getXML().createFormattingProperties("paragraph"); } }