OpenConcerto

Dépôt officiel du code source de l'ERP OpenConcerto
sonarqube

svn://code.openconcerto.org/openconcerto

Rev

Rev 67 | Rev 83 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
17 ilm 1
/*
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3
 *
4
 * Copyright 2011 OpenConcerto, by ILM Informatique. All rights reserved.
5
 *
6
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
7
 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
8
 * copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
9
 * language governing permissions and limitations under the License.
10
 *
11
 * When distributing the software, include this License Header Notice in each file.
12
 */
13
 
14
 package org.openconcerto.openoffice;
15
 
16
import static org.openconcerto.openoffice.ODPackage.RootElement.CONTENT;
17
import org.openconcerto.openoffice.ODPackage.RootElement;
73 ilm 18
import org.openconcerto.openoffice.style.data.DataStyle;
61 ilm 19
import org.openconcerto.utils.Base64;
20
import org.openconcerto.utils.CollectionUtils;
21
import org.openconcerto.utils.FileUtils;
73 ilm 22
import org.openconcerto.utils.Tuple2;
23
import org.openconcerto.utils.cc.IPredicate;
17 ilm 24
import org.openconcerto.xml.JDOMUtils;
25
import org.openconcerto.xml.SimpleXMLPath;
26
import org.openconcerto.xml.Step;
27
import org.openconcerto.xml.Step.Axis;
28
 
73 ilm 29
import java.awt.Point;
17 ilm 30
import java.io.File;
31
import java.io.IOException;
57 ilm 32
import java.io.InputStream;
17 ilm 33
import java.math.BigDecimal;
34
import java.net.URI;
35
import java.net.URISyntaxException;
36
import java.util.ArrayList;
73 ilm 37
import java.util.Collection;
38
import java.util.Collections;
17 ilm 39
import java.util.HashMap;
40
import java.util.HashSet;
41
import java.util.Iterator;
42
import java.util.List;
73 ilm 43
import java.util.ListIterator;
17 ilm 44
import java.util.Map;
73 ilm 45
import java.util.Map.Entry;
17 ilm 46
import java.util.Set;
47
 
48
import org.apache.commons.collections.Transformer;
49
import org.jdom.Attribute;
73 ilm 50
import org.jdom.Content;
51
import org.jdom.DocType;
17 ilm 52
import org.jdom.Document;
53
import org.jdom.Element;
54
import org.jdom.JDOMException;
55
import org.jdom.Namespace;
56
import org.jdom.xpath.XPath;
57
 
58
/**
59
 * An XML document containing all of an office document, see section 2.1 of OpenDocument 1.1.
60
 *
61
 * @author Sylvain CUAZ 24 nov. 2004
62
 */
25 ilm 63
public class ODSingleXMLDocument extends ODXMLDocument implements Cloneable {
17 ilm 64
 
73 ilm 65
    static private enum ContentPart {
66
        PROLOGUE, MAIN, EPILOGUE
67
    }
68
 
69
    private static final String BASIC_LANG_NAME = "ooo:Basic";
61 ilm 70
    private static final SimpleXMLPath<Attribute> ALL_HREF_ATTRIBUTES = SimpleXMLPath.allAttributes("href", "xlink");
71
    private static final SimpleXMLPath<Element> ALL_BINARY_DATA_ELEMENTS = SimpleXMLPath.allElements("binary-data", "office");
72
    // see 10.4.5 <office:binary-data> of OpenDocument-v1.2-os
73
    private static final Set<String> BINARY_DATA_PARENTS = CollectionUtils.createSet("draw:image", "draw:object-ole", "style:background-image", "text:list-level-style-image");
74
 
17 ilm 75
    final static Set<String> DONT_PREFIX;
73 ilm 76
    static private final Map<XMLVersion, List<Set<Element>>> ELEMS_ORDER;
77
    static private final Map<XMLVersion, Map<Tuple2<Namespace, String>, ContentPart>> ELEMS_PARTS;
17 ilm 78
    static {
79
        DONT_PREFIX = new HashSet<String>();
80
        // don't touch to user fields and variables
81
        // we want them to be the same across the document
82
        DONT_PREFIX.add("user-field-decl");
83
        DONT_PREFIX.add("user-field-get");
84
        DONT_PREFIX.add("variable-get");
85
        DONT_PREFIX.add("variable-decl");
86
        DONT_PREFIX.add("variable-set");
73 ilm 87
 
88
        final XMLVersion[] versions = XMLVersion.values();
89
        ELEMS_ORDER = new HashMap<XMLVersion, List<Set<Element>>>(versions.length);
90
        ELEMS_PARTS = new HashMap<XMLVersion, Map<Tuple2<Namespace, String>, ContentPart>>(versions.length);
91
        for (final XMLVersion v : versions) {
92
            // some elements can only appear in some types but since we have the null wild card,
93
            // they'd match that. So always include all elements.
94
            final List<Set<Element>> children = createChildren(v, null);
95
 
96
            ELEMS_ORDER.put(v, children);
97
 
98
            final Map<Tuple2<Namespace, String>, ContentPart> m = new HashMap<Tuple2<Namespace, String>, ODSingleXMLDocument.ContentPart>(ContentPart.values().length);
99
            boolean beforeNull = true;
100
            for (final Set<Element> s : children) {
101
                if (s == null) {
102
                    m.put(null, ContentPart.MAIN);
103
                    assert beforeNull : "more than one null";
104
                    beforeNull = false;
105
                } else {
106
                    for (final Element elem : s)
107
                        m.put(Tuple2.create(elem.getNamespace(), elem.getName()), beforeNull ? ContentPart.PROLOGUE : ContentPart.EPILOGUE);
108
                }
109
            }
110
            ELEMS_PARTS.put(v, m);
111
        }
17 ilm 112
    }
113
 
73 ilm 114
    private static final List<Set<Element>> createChildren(XMLVersion v, ContentType t) {
115
        final Namespace textNS = v.getTEXT();
116
        final Namespace tableNS = v.getTABLE();
117
        final List<Set<Element>> res = new ArrayList<Set<Element>>(24);
118
 
119
        if (t == null || t == ContentType.TEXT)
120
            res.add(Collections.singleton(new Element("forms", v.getOFFICE())));
121
        // first only for text, second only for spreadsheet
122
        final Element textTrackedChanges = new Element("tracked-changes", textNS);
123
        final Element tableTrackedChanges = new Element("tracked-changes", tableNS);
124
        if (t == null)
125
            res.add(CollectionUtils.createSet(textTrackedChanges, tableTrackedChanges));
126
        else if (t == ContentType.TEXT)
127
            res.add(Collections.singleton(textTrackedChanges));
128
        else if (t == ContentType.SPREADSHEET)
129
            res.add(Collections.singleton(tableTrackedChanges));
130
 
131
        // text-decls
132
        res.add(Collections.singleton(new Element("variable-decls", textNS)));
133
        res.add(Collections.singleton(new Element("sequence-decls", textNS)));
134
        res.add(Collections.singleton(new Element("user-field-decls", textNS)));
135
        res.add(Collections.singleton(new Element("dde-connection-decls", textNS)));
136
        res.add(Collections.singleton(new Element("alphabetical-index-auto-mark-file", textNS)));
137
 
138
        // table-decls
139
        res.add(Collections.singleton(new Element("calculation-settings", tableNS)));
140
        res.add(Collections.singleton(new Element("content-validations", tableNS)));
141
        res.add(Collections.singleton(new Element("label-ranges", tableNS)));
142
 
143
        if (v == XMLVersion.OD && (t == null || t == ContentType.PRESENTATION)) {
144
            // perhaps add presentation-decls (needs new namespace in XMLVersion)
145
        }
146
 
147
        // main content
148
        res.add(null);
149
 
150
        if (v == XMLVersion.OD && (t == null || t == ContentType.PRESENTATION)) {
151
            // perhaps add presentation:settings
152
        }
153
 
154
        // table-functions
155
        res.add(Collections.singleton(new Element("named-expressions", tableNS)));
156
        res.add(Collections.singleton(new Element("database-ranges", tableNS)));
157
        res.add(Collections.singleton(new Element("data-pilot-tables", tableNS)));
158
        res.add(Collections.singleton(new Element("consolidation", tableNS)));
159
        res.add(Collections.singleton(new Element("dde-links", tableNS)));
160
 
161
        if (v == XMLVersion.OOo && (t == null || t == ContentType.PRESENTATION)) {
162
            // perhaps add presentation:settings
163
        }
164
 
165
        return res;
166
    }
167
 
168
    // return null if not an element
169
    // return the part the element is in (assume MAIN for unknown elements)
170
    static private ContentPart getPart(final Map<Tuple2<Namespace, String>, ContentPart> parts, final Content bodyContent) {
171
        if (!(bodyContent instanceof Element))
172
            return null;
173
        final Element elem = (Element) bodyContent;
174
        ContentPart res = parts.get(Tuple2.create(elem.getNamespace(), elem.getName()));
175
        if (res == null)
176
            res = parts.get(null);
177
        assert res != null;
178
        return res;
179
    }
180
 
181
    static private int[] getLastNulls(final Map<Tuple2<Namespace, String>, ContentPart> parts, final Element body) {
182
        @SuppressWarnings("unchecked")
183
        final List<Content> content = body.getContent();
184
        return getLastNulls(parts, content, content.size());
185
    }
186
 
187
    // return the start of the EPILOGUE, at 0 with non-elements (i.e. having a null ContentPart), at
188
    // 1 the first EPILOGUE element
189
    static private int[] getLastNulls(final Map<Tuple2<Namespace, String>, ContentPart> parts, final List<Content> content, final int contentSize) {
190
        // start from the end until we leave the epilogue (quicker than traversing the main part as
191
        // prologue and epilogue sizes are bounded and small)
192
        ContentPart contentPart = null;
193
        final ListIterator<Content> thisChildrenIter = content.listIterator(contentSize);
194
        int nullsStartIndex = -1;
195
        while ((contentPart == null || contentPart == ContentPart.EPILOGUE) && thisChildrenIter.hasPrevious()) {
196
            contentPart = getPart(parts, thisChildrenIter.previous());
197
            if (contentPart != null) {
198
                nullsStartIndex = -1;
199
            } else if (nullsStartIndex < 0) {
200
                nullsStartIndex = thisChildrenIter.nextIndex();
201
            }
202
        }
203
        final int lastNullsStart = contentPart == null || contentPart == ContentPart.EPILOGUE ? thisChildrenIter.nextIndex() : thisChildrenIter.nextIndex() + 1;
204
        final int lastNullsEnd = nullsStartIndex < 0 ? lastNullsStart : nullsStartIndex + 1;
205
        return new int[] { lastNullsStart, lastNullsEnd };
206
    }
207
 
208
    /**
209
     * Slice the body into parts. Since some content have no part (e.g. comment), they can be added
210
     * to the previous or next range. If <code>overlapping</code> is <code>true</code> they will be
211
     * added to both, else only to the next range.
212
     *
213
     * @param parts parts definition.
214
     * @param body the element to slice.
215
     * @param overlapping <code>true</code> if ranges can overlap.
216
     * @return the start (inclusive, {@link Point#x}) and end (exclusive, {@link Point#y}) for each
217
     *         {@link ContentPart}.
218
     */
219
    static private Point[] getBounds(final Map<Tuple2<Namespace, String>, ContentPart> parts, final Element body, final boolean overlapping) {
220
        @SuppressWarnings("unchecked")
221
        final List<Content> content = body.getContent();
222
        final int contentSize = content.size();
223
        if (contentSize == 0)
224
            return new Point[] { new Point(0, 0), new Point(0, 0), new Point(0, 0) };
225
 
226
        // start from the beginning until we leave the prologue
227
        ContentPart contentPart = null;
228
        ListIterator<Content> thisChildrenIter = content.listIterator(0);
229
        final int prologueStart = 0;
230
        int nullsStartIndex = -1;
231
        while ((contentPart == null || contentPart == ContentPart.PROLOGUE) && thisChildrenIter.hasNext()) {
232
            contentPart = getPart(parts, thisChildrenIter.next());
233
            if (contentPart != null) {
234
                nullsStartIndex = -1;
235
            } else if (nullsStartIndex < 0) {
236
                nullsStartIndex = thisChildrenIter.previousIndex();
237
            }
238
        }
239
        final int nullsEnd = contentPart == null || contentPart == ContentPart.PROLOGUE ? thisChildrenIter.nextIndex() : thisChildrenIter.previousIndex();
240
        final int nullsStart = nullsStartIndex < 0 ? nullsEnd : nullsStartIndex;
241
        assert nullsStart >= 0 && nullsStart <= nullsEnd;
242
        final int mainStart = nullsStart;
243
        final int prologueStop = overlapping ? nullsEnd : nullsStart;
244
 
245
        final int epilogueEnd = contentSize;
246
        final int[] lastNulls = getLastNulls(parts, content, contentSize);
247
        final int lastNullsStart = lastNulls[0];
248
        final int lastNullsEnd = lastNulls[1];
249
        assert lastNullsStart >= mainStart && lastNullsStart <= lastNullsEnd;
250
        final int epilogueStart = lastNullsStart;
251
        final int mainEnd = overlapping ? lastNullsEnd : lastNullsStart;
252
 
253
        final Point[] res = new Point[] { new Point(prologueStart, prologueStop), new Point(mainStart, mainEnd), new Point(epilogueStart, epilogueEnd) };
254
        assert res.length == ContentPart.values().length;
255
        return res;
256
    }
257
 
258
    static private int getValidIndex(final Map<Tuple2<Namespace, String>, ContentPart> parts, final Element body, final int index) {
259
        // overlapping ranges to have longest main part possible and thus avoid changing index
260
        final Point[] bounds = getBounds(parts, body, true);
261
        final Point mainBounds = bounds[ContentPart.MAIN.ordinal()];
262
 
263
        final int mainEnd = mainBounds.y;
264
        if (index < 0 || index > mainEnd)
265
            return mainEnd;
266
 
267
        final int mainStart = mainBounds.x;
268
        if (index < mainStart)
269
            return mainStart;
270
 
271
        return index;
272
    }
273
 
17 ilm 274
    // Voir le TODO du ctor
275
    // public static OOSingleXMLDocument createEmpty() {
276
    // }
277
 
278
    /**
279
     * Create a document from a collection of subdocuments.
280
     *
281
     * @param content the content.
282
     * @param style the styles, can be <code>null</code>.
283
     * @return the merged document.
284
     */
285
    public static ODSingleXMLDocument createFromDocument(Document content, Document style) {
25 ilm 286
        return ODPackage.createFromDocuments(content, style).toSingle();
17 ilm 287
    }
288
 
25 ilm 289
    static ODSingleXMLDocument create(ODPackage files) {
290
        final Document content = files.getContent().getDocument();
291
        final Document style = files.getDocument(RootElement.STYLES.getZipEntry());
17 ilm 292
        // signal that the xml is a complete document (was document-content)
293
        final Document singleContent = RootElement.createSingle(content);
294
        copyNS(content, singleContent);
295
        files.getContentType().setType(singleContent);
296
        final Element root = singleContent.getRootElement();
297
        root.addContent(content.getRootElement().removeContent());
298
        // see section 2.1.1 first meta, then settings, then the rest
73 ilm 299
        createScriptsElement(root, files);
25 ilm 300
        prependToRoot(files.getDocument(RootElement.SETTINGS.getZipEntry()), root);
301
        prependToRoot(files.getDocument(RootElement.META.getZipEntry()), root);
17 ilm 302
        final ODSingleXMLDocument single = new ODSingleXMLDocument(singleContent, files);
303
        if (single.getChild("body") == null)
304
            throw new IllegalArgumentException("no body in " + single);
305
        if (style != null) {
306
            // section 2.1 : Styles used in the document content and automatic styles used in the
307
            // styles themselves.
308
            // more precisely in section 2.1.1 : office:document-styles contains style, master
309
            // style, auto style, font decls ; the last two being also in content.xml but are *not*
310
            // related : eg P1 of styles.xml is *not* the P1 of content.xml
311
            try {
312
                single.mergeAllStyles(new ODXMLDocument(style), true);
313
            } catch (JDOMException e) {
314
                throw new IllegalArgumentException("style is not valid", e);
315
            }
316
        }
317
        return single;
318
    }
319
 
73 ilm 320
    private static void createScriptsElement(final Element root, final ODPackage pkg) {
321
        final Map<String, Library> basicLibraries = pkg.readBasicLibraries();
322
        if (basicLibraries.size() > 0) {
323
            final XMLFormatVersion formatVersion = pkg.getFormatVersion();
324
            final XMLVersion version = formatVersion.getXMLVersion();
325
            final Namespace officeNS = version.getOFFICE();
326
 
327
            // scripts must be before the body and automatic styles
328
            final Element scriptsElem = JDOMUtils.getOrCreateChild(root, formatVersion.getXML().getOfficeScripts(), officeNS, 0);
329
            final Element scriptElem = new Element(formatVersion.getXML().getOfficeScript(), officeNS);
330
            scriptElem.setAttribute("language", BASIC_LANG_NAME, version.getNS("script"));
331
            // script must be before events
332
            scriptsElem.addContent(0, scriptElem);
333
 
334
            final Element libsElem = new Element("libraries", version.getLibrariesNS());
335
            for (final Library lib : basicLibraries.values()) {
336
                libsElem.addContent(lib.toFlatXML(formatVersion));
337
            }
338
            scriptElem.addContent(libsElem);
339
        }
340
    }
341
 
17 ilm 342
    private static void prependToRoot(Document settings, final Element root) {
343
        if (settings != null) {
344
            copyNS(settings, root.getDocument());
345
            final Element officeSettings = (Element) settings.getRootElement().getChildren().get(0);
346
            root.addContent(0, (Element) officeSettings.clone());
347
        }
348
    }
349
 
350
    // some namespaces are needed even if not directly used, see § 18.3.19 namespacedToken
351
    // of v1.2-part1-cd04 (e.g. 19.31 config:name or 19.260 form:control-implementation)
352
    @SuppressWarnings("unchecked")
353
    private static void copyNS(final Document src, final Document dest) {
354
        JDOMUtils.addNamespaces(dest.getRootElement(), src.getRootElement().getAdditionalNamespaces());
355
    }
356
 
357
    /**
57 ilm 358
     * Create a document from a package.
17 ilm 359
     *
360
     * @param f an OpenDocument package file.
361
     * @return the merged file.
362
     * @throws JDOMException if the file is not a valid OpenDocument file.
363
     * @throws IOException if the file can't be read.
364
     */
57 ilm 365
    public static ODSingleXMLDocument createFromPackage(File f) throws JDOMException, IOException {
17 ilm 366
        // this loads all linked files
367
        return new ODPackage(f).toSingle();
368
    }
369
 
370
    /**
57 ilm 371
     * Create a document from a flat XML.
372
     *
373
     * @param f an OpenDocument XML file.
374
     * @return the created file.
375
     * @throws JDOMException if the file is not a valid OpenDocument file.
376
     * @throws IOException if the file can't be read.
377
     */
378
    public static ODSingleXMLDocument createFromFile(File f) throws JDOMException, IOException {
379
        final ODSingleXMLDocument res = new ODSingleXMLDocument(OOUtils.getBuilder().build(f));
380
        res.getPackage().setFile(f);
381
        return res;
382
    }
383
 
384
    public static ODSingleXMLDocument createFromStream(InputStream ins) throws JDOMException, IOException {
385
        return new ODSingleXMLDocument(OOUtils.getBuilder().build(ins));
386
    }
387
 
388
    /**
17 ilm 389
     * fix bug when a SingleXMLDoc is used to create a document (for example with P2 and 1_P2), and
390
     * then create another instance s2 with the previous document and add a second file (also with
391
     * P2 and 1_P2) => s2 will contain P2, 1_P2, 1_P2, 1_1_P2.
392
     */
393
    private static final String COUNT = "SingleXMLDocument_count";
394
 
395
    /** Le nombre de fichiers concat */
396
    private int numero;
397
    /** Les styles présent dans ce document */
398
    private final Set<String> stylesNames;
399
    /** Les styles de liste présent dans ce document */
400
    private final Set<String> listStylesNames;
401
    /** Les fichiers référencés par ce document */
402
    private ODPackage pkg;
403
    private final ODMeta meta;
404
    // the element between each page
405
    private Element pageBreak;
406
 
407
    public ODSingleXMLDocument(Document content) {
61 ilm 408
        this(content, null);
17 ilm 409
    }
410
 
411
    /**
412
     * A new single document. NOTE: this document will put himself in <code>pkg</code>, replacing
413
     * any previous content.
414
     *
415
     * @param content the XML.
416
     * @param pkg the package this document belongs to.
417
     */
418
    private ODSingleXMLDocument(Document content, final ODPackage pkg) {
419
        super(content);
420
 
421
        // inited in getPageBreak()
422
        this.pageBreak = null;
423
 
61 ilm 424
        final boolean contentIsFlat = pkg == null;
425
        this.pkg = contentIsFlat ? new ODPackage() : pkg;
73 ilm 426
        if (!contentIsFlat) {
427
            final Set<String> toRm = new HashSet<String>();
428
            for (final RootElement e : RootElement.getPackageElements())
429
                toRm.add(e.getZipEntry());
430
            for (final String e : this.pkg.getEntries()) {
431
                if (e.startsWith(Library.DIR_NAME))
432
                    toRm.add(e);
433
            }
434
            this.pkg.rmFiles(toRm);
435
        }
17 ilm 436
        this.pkg.putFile(CONTENT.getZipEntry(), this, "text/xml");
437
 
61 ilm 438
        // update href
439
        if (contentIsFlat) {
440
            // OD thinks of the ZIP archive as an additional folder
441
            for (final Attribute hrefAttr : ALL_HREF_ATTRIBUTES.selectNodes(getDocument().getRootElement())) {
442
                final String href = hrefAttr.getValue();
443
                if (!URI.create(href).isAbsolute())
444
                    hrefAttr.setValue("../" + href);
445
            }
446
        }
447
        // decode Base64 binaries
448
        for (final Element binaryDataElem : ALL_BINARY_DATA_ELEMENTS.selectNodes(getDocument().getRootElement())) {
449
            final String name;
450
            int i = 1;
451
            final Set<String> entries = getPackage().getEntries();
452
            final Element binaryParentElement = binaryDataElem.getParentElement();
453
            while (entries.contains(binaryParentElement.getName() + "/" + i))
454
                i++;
455
            name = binaryParentElement.getName() + "/" + i;
456
            getPackage().putFile(name, Base64.decode(binaryDataElem.getText()));
457
            binaryParentElement.setAttribute("href", name, binaryDataElem.getNamespace("xlink"));
458
            binaryDataElem.detach();
459
        }
17 ilm 460
 
61 ilm 461
        this.meta = this.getPackage().getMeta(true);
462
 
17 ilm 463
        final ODUserDefinedMeta userMeta = this.meta.getUserMeta(COUNT);
464
        if (userMeta != null) {
465
            final Object countValue = userMeta.getValue();
466
            if (countValue instanceof Number) {
467
                this.numero = ((Number) countValue).intValue();
468
            } else {
469
                this.numero = new BigDecimal(countValue.toString()).intValue();
470
            }
471
        } else {
472
            // if not hasCount(), it's not us that created content
473
            // so there should not be any 1_
474
            this.setNumero(0);
475
        }
476
 
477
        this.stylesNames = new HashSet<String>(64);
478
        this.listStylesNames = new HashSet<String>(16);
479
 
480
        // little trick to find the common styles names (not to be prefixed so they remain
481
        // consistent across the added documents)
482
        final Element styles = this.getChild("styles");
483
        if (styles != null) {
484
            // create a second document with our styles to collect names
485
            final Element root = this.getDocument().getRootElement();
486
            final Document clonedDoc = new Document(new Element(root.getName(), root.getNamespace()));
487
            clonedDoc.getRootElement().addContent(styles.detach());
488
            try {
73 ilm 489
                this.mergeStyles(new ODXMLDocument(clonedDoc), true);
17 ilm 490
            } catch (JDOMException e) {
491
                throw new IllegalArgumentException("can't find common styles names.");
492
            }
493
            // reattach our styles
494
            styles.detach();
495
            this.setChild(styles);
496
        }
497
    }
498
 
499
    ODSingleXMLDocument(ODSingleXMLDocument doc, ODPackage p) {
500
        super(doc);
501
        if (p == null)
502
            throw new NullPointerException("Null package");
503
        this.stylesNames = new HashSet<String>(doc.stylesNames);
504
        this.listStylesNames = new HashSet<String>(doc.listStylesNames);
505
        this.pkg = p;
506
        this.meta = ODMeta.create(this);
507
        this.setNumero(doc.numero);
508
    }
509
 
510
    @Override
511
    public ODSingleXMLDocument clone() {
512
        final ODPackage copy = new ODPackage(this.pkg);
513
        return (ODSingleXMLDocument) copy.getContent();
514
    }
515
 
516
    private void setNumero(int numero) {
517
        this.numero = numero;
518
        this.meta.getUserMeta(COUNT, true).setValue(this.numero);
519
    }
520
 
521
    /**
522
     * The number of files concatenated with {@link #add(ODSingleXMLDocument)}.
523
     *
524
     * @return number of files concatenated.
525
     */
526
    public final int getNumero() {
527
        return this.numero;
528
    }
529
 
530
    public ODPackage getPackage() {
531
        return this.pkg;
532
    }
533
 
73 ilm 534
    private final Element getBasicScriptElem() {
535
        return this.getBasicScriptElem(false);
536
    }
537
 
538
    private final Element getBasicScriptElem(final boolean create) {
539
        final OOXML xml = getXML();
540
        final String officeScripts = xml.getOfficeScripts();
541
        final Element scriptsElem = this.getChild(officeScripts, create);
542
        if (scriptsElem == null)
543
            return null;
544
        final Namespace scriptNS = this.getVersion().getNS("script");
545
        final Namespace officeNS = this.getVersion().getOFFICE();
546
        @SuppressWarnings("unchecked")
547
        final List<Element> scriptElems = scriptsElem.getChildren(xml.getOfficeScript(), officeNS);
548
        for (final Element scriptElem : scriptElems) {
549
            if (scriptElem.getAttributeValue("language", scriptNS).equals(BASIC_LANG_NAME))
550
                return scriptElem;
551
        }
552
        if (create) {
553
            final Element res = new Element(xml.getOfficeScript(), officeNS);
554
            res.setAttribute("language", BASIC_LANG_NAME, scriptNS);
555
            scriptsElem.addContent(res);
556
            return res;
557
        } else {
558
            return null;
559
        }
560
    }
561
 
17 ilm 562
    /**
73 ilm 563
     * Parse BASIC libraries in this flat XML.
564
     *
565
     * @return the BASIC libraries by name.
566
     */
567
    public final Map<String, Library> readBasicLibraries() {
568
        return this.readBasicLibraries(this.getBasicScriptElem()).get0();
569
    }
570
 
571
    private final Tuple2<Map<String, Library>, Map<String, Element>> readBasicLibraries(final Element scriptElem) {
572
        if (scriptElem == null)
573
            return Tuple2.create(Collections.<String, Library> emptyMap(), Collections.<String, Element> emptyMap());
574
 
575
        final Namespace libNS = this.getVersion().getLibrariesNS();
576
        final Namespace linkNS = this.getVersion().getNS("xlink");
577
        final Map<String, Library> res = new HashMap<String, Library>();
578
        final Map<String, Element> resElems = new HashMap<String, Element>();
579
        @SuppressWarnings("unchecked")
580
        final List<Element> libsElems = scriptElem.getChildren("libraries", libNS);
581
        for (final Element libsElem : libsElems) {
582
            @SuppressWarnings("unchecked")
583
            final List<Element> libElems = libsElem.getChildren();
584
            for (final Element libElem : libElems) {
585
                final Library library = Library.fromFlatXML(libElem, this.getPackage(), linkNS);
586
                if (library != null) {
587
                    if (res.put(library.getName(), library) != null)
588
                        throw new IllegalStateException("Duplicate library named " + library.getName());
589
                    resElems.put(library.getName(), libElem);
590
                }
591
            }
592
        }
593
 
594
        return Tuple2.create(res, resElems);
595
    }
596
 
597
    /**
17 ilm 598
     * Append a document.
599
     *
600
     * @param doc the document to add.
601
     */
602
    public synchronized void add(ODSingleXMLDocument doc) {
603
        // ajoute un saut de page entre chaque document
604
        this.add(doc, true);
605
    }
606
 
607
    /**
608
     * Append a document.
609
     *
610
     * @param doc the document to add, <code>null</code> means no-op.
611
     * @param pageBreak whether a page break should be inserted before <code>doc</code>.
612
     */
613
    public synchronized void add(ODSingleXMLDocument doc, boolean pageBreak) {
73 ilm 614
        if (doc != null && pageBreak) {
17 ilm 615
            // only add a page break, if a page was really added
73 ilm 616
            final Element thisBody = this.getBody();
617
            thisBody.addContent(getLastNulls(ELEMS_PARTS.get(getVersion()), thisBody)[0], this.getPageBreak());
618
        }
619
        this.add(null, -1, doc);
17 ilm 620
    }
621
 
622
    public synchronized void replace(Element elem, ODSingleXMLDocument doc) {
623
        final Element parent = elem.getParentElement();
624
        this.add(parent, parent.indexOf(elem), doc);
625
        elem.detach();
626
    }
627
 
73 ilm 628
    // use content index and not children (element) index, since it's more accurate (we can add
629
    // after or before a comment) and faster (no filter and adjusted index)
630
    /**
631
     * Add the passed document at the specified place.
632
     *
633
     * @param where a descendant of the body, <code>null</code> meaning the body itself.
634
     * @param index the content index inside <code>where</code>, -1 meaning the end.
635
     * @param doc the document to add, <code>null</code> means no-op.
636
     */
17 ilm 637
    public synchronized void add(Element where, int index, ODSingleXMLDocument doc) {
638
        if (doc == null)
639
            return;
640
        if (!this.getVersion().equals(doc.getVersion()))
641
            throw new IllegalArgumentException("version mismatch");
642
 
643
        this.setNumero(this.numero + 1);
644
        try {
645
            copyNS(doc.getDocument(), this.getDocument());
646
            this.mergeEmbedded(doc);
647
            this.mergeSettings(doc);
73 ilm 648
            this.mergeScripts(doc);
17 ilm 649
            this.mergeAllStyles(doc, false);
650
            this.mergeBody(where, index, doc);
651
        } catch (JDOMException exn) {
652
            throw new IllegalArgumentException("XML error", exn);
653
        }
654
    }
655
 
656
    /**
657
     * Merge the four elements of style.
658
     *
659
     * @param doc the xml document to merge.
660
     * @param sameDoc whether <code>doc</code> is the same OpenDocument than this, eg
661
     *        <code>true</code> when merging content.xml and styles.xml.
662
     * @throws JDOMException if an error occurs.
663
     */
664
    private void mergeAllStyles(ODXMLDocument doc, boolean sameDoc) throws JDOMException {
665
        // no reference
666
        this.mergeFontDecls(doc);
667
        // section 14.1
668
        // § Parent Style only refer to other common styles
669
        // § Next Style cannot refer to an autostyle (only available in common styles)
670
        // § List Style can refer to an autostyle
671
        // § Master Page Name cannot (auto master pages does not exist)
672
        // § Data Style Name (for cells) can
673
        // but since the UI for common styles doesn't allow to customize List Style
674
        // and there is no common styles for tables : office:styles doesn't reference any automatic
675
        // styles
73 ilm 676
        this.mergeStyles(doc, sameDoc);
17 ilm 677
        // on the contrary autostyles do refer to other autostyles :
678
        // choosing "activate bullets" will create an automatic paragraph style:style
679
        // referencing an automatic text:list-style.
680
        this.mergeAutoStyles(doc, !sameDoc);
681
        // section 14.4
682
        // § Page Layout can refer to an autostyle
683
        // § Next Style Name refer to another masterPage
684
        this.mergeMasterStyles(doc, !sameDoc);
685
    }
686
 
687
    private void mergeEmbedded(ODSingleXMLDocument doc) {
688
        // since we are adding another document our existing thumbnail is obsolete
689
        this.pkg.rmFile("Thumbnails/thumbnail.png");
73 ilm 690
        this.pkg.rmFile("layout-cache");
691
        // copy the files (only non generated files, e.g. content.xml will be merged later)
692
        for (final String name : doc.pkg.getEntries()) {
693
            final ODPackageEntry e = doc.pkg.getEntry(name);
17 ilm 694
            if (!ODPackage.isStandardFile(e.getName())) {
73 ilm 695
                this.pkg.putCopy(e, this.prefix(e.getName()));
17 ilm 696
            }
697
        }
698
    }
699
 
700
    private void mergeSettings(ODSingleXMLDocument doc) throws JDOMException {
73 ilm 701
        // used to call addIfNotPresent(), but it cannot create the element at the correct position
702
        final String elemName = "settings";
703
        if (this.getChild(elemName, false) == null) {
704
            final Element other = doc.getChild(elemName, false);
705
            if (other != null) {
706
                this.getChild(elemName, true).addContent(other.cloneContent());
707
            }
708
        }
17 ilm 709
    }
710
 
73 ilm 711
    private void mergeScripts(ODSingleXMLDocument doc) {
712
        // <office:script>*
713
        this.addBasicLibraries(doc.readBasicLibraries());
714
 
715
        // <office:event-listeners>?
716
        final Map<String, EventListener> oEvents = doc.getPackage().readEventListeners();
717
        if (oEvents.size() > 0) {
718
            // check if they can be merged
719
            final Map<String, EventListener> thisEvents = this.getPackage().readEventListeners();
720
            final Set<String> duplicateEvents = CollectionUtils.inter(thisEvents.keySet(), oEvents.keySet());
721
            for (final String eventName : duplicateEvents) {
722
                final Element thisEvent = thisEvents.get(eventName).getElement();
723
                final Element oEvent = oEvents.get(eventName).getElement();
724
                if (!JDOMUtils.equalsDeep(oEvent, thisEvent)) {
725
                    throw new IllegalArgumentException("Incompatible elements for " + eventName);
726
                }
727
            }
728
 
729
            final OOXML xml = getXML();
730
            final Element thisScripts = this.getChild(xml.getOfficeScripts(), true);
731
            final Element thisEventListeners = JDOMUtils.getOrCreateChild(thisScripts, xml.getOfficeEventListeners(), this.getVersion().getOFFICE());
732
            for (final Entry<String, EventListener> e : oEvents.entrySet()) {
733
                if (!thisEvents.containsKey(e.getKey())) {
734
                    // we can just clone since libraries aren't renamed when merged
735
                    thisEventListeners.addContent((Element) e.getValue().getElement().clone());
736
                }
737
            }
738
        }
739
    }
740
 
17 ilm 741
    /**
73 ilm 742
     * Add the passed libraries to this document. Passed libraries with the same content as existing
743
     * ones are ignored.
744
     *
745
     * @param libraries what to add.
746
     * @return the actually added libraries.
747
     * @throws IllegalArgumentException if <code>libraries</code> contains duplicates or if it
748
     *         cannot be merged into this.
749
     * @see Library#canBeMerged(Library)
750
     */
751
    public final Set<String> addBasicLibraries(final Collection<? extends Library> libraries) {
752
        return this.addBasicLibraries(Library.toMap(libraries));
753
    }
754
 
755
    public final Set<String> addBasicLibraries(final ODPackage pkg) {
756
        if (pkg == this.pkg)
757
            return Collections.emptySet();
758
        return this.addBasicLibraries(pkg.readBasicLibraries());
759
    }
760
 
761
    final Set<String> addBasicLibraries(final Map<String, Library> oLibraries) {
762
        if (oLibraries.size() == 0)
763
            return Collections.emptySet();
764
 
765
        final Tuple2<Map<String, Library>, Map<String, Element>> thisLibrariesAndElements = this.readBasicLibraries(this.getBasicScriptElem(false));
766
        final Map<String, Library> thisLibraries = thisLibrariesAndElements.get0();
767
        final Map<String, Element> thisLibrariesElements = thisLibrariesAndElements.get1();
768
        // check that the libraries to add which are already in us can be merged (no elements
769
        // conflict)
770
        final Set<String> duplicateLibs = Library.canBeMerged(thisLibraries, oLibraries);
771
        final Set<String> newLibs = new HashSet<String>(oLibraries.keySet());
772
        newLibs.removeAll(thisLibraries.keySet());
773
 
774
        // merge modules
775
        for (final String duplicateLib : duplicateLibs) {
776
            final Library thisLib = thisLibraries.get(duplicateLib);
777
            final Library oLib = oLibraries.get(duplicateLib);
778
            assert thisLib != null && oLib != null : "Not duplicate " + duplicateLib;
779
            oLib.mergeToFlatXML(this.getFormatVersion(), thisLib, thisLibrariesElements.get(duplicateLib));
780
        }
781
        if (newLibs.size() > 0) {
782
            final Element thisScriptElem = this.getBasicScriptElem(true);
783
            final Element librariesElem = JDOMUtils.getOrCreateChild(thisScriptElem, "libraries", this.getVersion().getLibrariesNS());
784
            for (final String newLib : newLibs)
785
                librariesElem.addContent(oLibraries.get(newLib).toFlatXML(this.getFormatVersion()));
786
        }
787
 
788
        // merge dialogs
789
        for (final Library oLib : oLibraries.values()) {
790
            final String libName = oLib.getName();
791
            // can be null
792
            final Library thisLib = thisLibraries.get(libName);
793
            oLib.mergeDialogs(this.getPackage(), thisLib);
794
        }
795
 
796
        return newLibs;
797
    }
798
 
799
    /**
800
     * Remove the passed libraries.
801
     *
802
     * @param libraries which libraries to remove.
803
     * @return the actually removed libraries.
804
     */
805
    public final Set<String> removeBasicLibraries(final Collection<String> libraries) {
806
        final Map<String, Element> thisLibrariesElements = this.readBasicLibraries(this.getBasicScriptElem(false)).get1();
807
        final Set<String> res = new HashSet<String>();
808
        for (final String libToRm : libraries) {
809
            final Element elemToRm = thisLibrariesElements.get(libToRm);
810
            if (elemToRm != null) {
811
                elemToRm.detach();
812
                res.add(libToRm);
813
            }
814
            if (Library.removeFromPackage(this.getPackage(), libToRm))
815
                res.add(libToRm);
816
        }
817
        return res;
818
    }
819
 
820
    /**
17 ilm 821
     * Fusionne les office:font-decls/style:font-decl. On ne préfixe jamais, on ajoute seulement si
822
     * l'attribut style:name est différent.
823
     *
824
     * @param doc le document à fusionner avec celui-ci.
825
     * @throws JDOMException
826
     */
827
    private void mergeFontDecls(ODXMLDocument doc) throws JDOMException {
828
        final String[] fontDecls = this.getFontDecls();
829
        this.mergeUnique(doc, fontDecls[0], fontDecls[1]);
830
    }
831
 
832
    private String[] getFontDecls() {
19 ilm 833
        return getXML().getFontDecls();
17 ilm 834
    }
835
 
836
    // merge everything under office:styles
73 ilm 837
    private void mergeStyles(ODXMLDocument doc, boolean sameDoc) throws JDOMException {
17 ilm 838
        // les default-style (notamment tab-stop-distance)
839
        this.mergeUnique(doc, "styles", "style:default-style", "style:family", NOP_ElementTransformer);
73 ilm 840
        // data styles
841
        // we have to prefix data styles since they're automatically generated by LO
842
        // (e.g. user created cellStyle1, LO generated dataStyleN0 in a document, then in another
843
        // document created cellStyle2, LO also generated dataStyleN0)
844
        // MAYBE search for orphans that discarded (same name) styles might leave
845
        // Don't prefix if we're merging styles into content (content contains no styles, so there
846
        // can be no collision ; also we don't prefix the body)
847
        final String dsNS = "number";
848
        final boolean prefixDataStyles = !sameDoc;
849
        final List<Element> addedDataStyles = this.addStyles(doc, "styles", Step.createElementStep(null, dsNS, new IPredicate<Element>() {
850
            private final Set<String> names;
851
            {
852
                this.names = new HashSet<String>(DataStyle.DATA_STYLES.size());
853
                for (final Class<? extends DataStyle> cl : DataStyle.DATA_STYLES) {
854
                    final StyleDesc<? extends DataStyle> styleDesc = Style.getStyleDesc(cl, getVersion());
855
                    this.names.add(styleDesc.getElementName());
856
                    assert styleDesc.getElementNS().getPrefix().equals(dsNS) : styleDesc;
857
                }
858
            }
859
 
860
            @Override
861
            public boolean evaluateChecked(Element elem) {
862
                return this.names.contains(elem.getName());
863
            }
864
        }), prefixDataStyles);
865
        if (prefixDataStyles) {
866
            // data styles reference each other (e.g. style:map)
867
            final SimpleXMLPath<Attribute> simplePath = SimpleXMLPath.allAttributes("apply-style-name", "style");
868
            for (final Attribute attr : simplePath.selectNodes(addedDataStyles)) {
869
                attr.setValue(prefix(attr.getValue()));
870
            }
871
        }
17 ilm 872
        // les styles
73 ilm 873
        // if we prefixed data styles, we must prefix references
874
        this.stylesNames.addAll(this.mergeUnique(doc, "styles", "style:style", !prefixDataStyles ? NOP_ElementTransformer : new ElementTransformer() {
875
            @Override
876
            public Element transform(Element elem) throws JDOMException {
877
                final Attribute attr = elem.getAttribute("data-style-name", elem.getNamespace());
878
                if (attr != null)
879
                    attr.setValue(prefix(attr.getValue()));
880
                return elem;
881
            }
882
        }));
17 ilm 883
        // on ajoute outline-style si non présent
884
        this.addStylesIfNotPresent(doc, "outline-style");
885
        // les list-style
886
        this.listStylesNames.addAll(this.mergeUnique(doc, "styles", "text:list-style"));
887
        // les *notes-configuration
888
        if (getVersion() == XMLVersion.OOo) {
889
            this.addStylesIfNotPresent(doc, "footnotes-configuration");
890
            this.addStylesIfNotPresent(doc, "endnotes-configuration");
891
        } else {
892
            // 16.29.3 : specifies values for each note class used in a document
893
            this.mergeUnique(doc, "styles", "text:notes-configuration", "text:note-class", NOP_ElementTransformer);
894
        }
895
        this.addStylesIfNotPresent(doc, "bibliography-configuration");
896
        this.addStylesIfNotPresent(doc, "linenumbering-configuration");
897
    }
898
 
899
    /**
900
     * Fusionne les office:automatic-styles, on préfixe tout.
901
     *
902
     * @param doc le document à fusionner avec celui-ci.
903
     * @param ref whether to prefix hrefs.
904
     * @throws JDOMException
905
     */
906
    private void mergeAutoStyles(ODXMLDocument doc, boolean ref) throws JDOMException {
907
        final List<Element> addedStyles = this.prefixAndAddAutoStyles(doc);
908
        for (final Element addedStyle : addedStyles) {
909
            this.prefix(addedStyle, ref);
910
        }
911
    }
912
 
913
    /**
914
     * Fusionne les office:master-styles. On ne préfixe jamais, on ajoute seulement si l'attribut
915
     * style:name est différent.
916
     *
917
     * @param doc le document à fusionner avec celui-ci.
918
     * @param ref whether to prefix hrefs.
919
     * @throws JDOMException if an error occurs.
920
     */
921
    private void mergeMasterStyles(ODXMLDocument doc, boolean ref) throws JDOMException {
922
        // est référencé dans les styles avec "style:master-page-name"
923
        this.mergeUnique(doc, "master-styles", "style:master-page", ref ? this.prefixTransf : this.prefixTransfNoRef);
924
    }
925
 
926
    /**
927
     * Fusionne les corps.
928
     *
73 ilm 929
     * @param where the element where to add the main content, <code>null</code> meaning at the root
930
     *        of the body.
931
     * @param index the content index inside <code>where</code>, -1 meaning at the end.
17 ilm 932
     * @param doc le document à fusionner avec celui-ci.
933
     * @throws JDOMException
934
     */
935
    private void mergeBody(Element where, int index, ODSingleXMLDocument doc) throws JDOMException {
73 ilm 936
        final Element thisBody = this.getBody();
937
        final Map<Tuple2<Namespace, String>, ContentPart> parts = ELEMS_PARTS.get(getVersion());
17 ilm 938
 
73 ilm 939
        // find where to add
940
        final Element nonNullWhere = where != null ? where : thisBody;
941
        if (nonNullWhere == thisBody) {
942
            // don't add in prologue or epilogue (ATTN the caller passed the index in reference to
943
            // the existing body but it might change and thus the index might need correction)
944
            index = getValidIndex(parts, thisBody, index);
945
        } else {
946
            // check that the element is rooted in the main part
947
            final Element movedChild = JDOMUtils.getAncestor(nonNullWhere, new IPredicate<Element>() {
948
                @Override
949
                public boolean evaluateChecked(Element input) {
950
                    return input.getParent() == thisBody;
17 ilm 951
                }
73 ilm 952
            });
953
            if (movedChild == null)
954
                throw new IllegalStateException("not adding in body : " + nonNullWhere);
955
            final ContentPart contentPart = getPart(parts, movedChild);
956
            if (contentPart != ContentPart.MAIN)
957
                throw new IllegalStateException("not adding in main : " + contentPart + " ; " + nonNullWhere);
958
        }
17 ilm 959
 
73 ilm 960
        final ChildCreator childCreator = ChildCreator.createFromSets(thisBody, ELEMS_ORDER.get(getVersion()));
961
        int prologueAddedCount = 0;
962
        final Element otherBody = doc.getBody();
963
        // split doc body in to three parts to keep non-elements
964
        final Point[] bounds = getBounds(parts, otherBody, false);
965
        @SuppressWarnings("unchecked")
966
        final List<Content> otherContent = otherBody.getContent();
967
        // prologue and epilogue have small and bounded size
968
        final List<Content> mainElements = new ArrayList<Content>(otherContent.size());
969
        final ListIterator<Content> listIter = otherContent.listIterator();
970
        for (final ContentPart part : ContentPart.values()) {
971
            final Point partBounds = bounds[part.ordinal()];
972
            final int partEnd = partBounds.y;
973
            while (listIter.nextIndex() < partEnd) {
974
                assert listIter.hasNext() : "wrong bounds";
975
                final Content c = listIter.next();
976
                if (c instanceof Element) {
977
                    final Element bodyChild = (Element) c;
978
                    if (part == ContentPart.PROLOGUE) {
979
                        final int preSize = thisBody.getContentSize();
980
                        final String childName = bodyChild.getName();
981
                        if (childName.equals("forms")) {
982
                            final Element elem = (Element) bodyChild.clone();
983
                            // TODO prefix xml:id and their draw:control references
984
                            this.prefix(elem, true);
985
                            childCreator.getChild(bodyChild, true).addContent(elem.removeContent());
986
                        } else if (childName.equals("variable-decls") || childName.equals("sequence-decls") || childName.equals("user-field-decls")) {
987
                            final Element elem = (Element) bodyChild.clone();
988
                            // * user fields are global to a document, they do not vary across it.
989
                            // Hence they are initialized at declaration
990
                            // * variables are not initialized at declaration
991
                            detachDuplicate(elem);
992
                            this.prefix(elem, true);
993
                            childCreator.getChild(bodyChild, true).addContent(elem.removeContent());
994
                        } else {
995
                            Log.get().fine("Ignoring in " + part + " : " + bodyChild);
996
                        }
997
                        final int postSize = thisBody.getContentSize();
998
                        prologueAddedCount += postSize - preSize;
999
                    } else if (part == ContentPart.MAIN) {
1000
                        mainElements.add(this.prefix((Element) bodyChild.clone(), true));
1001
                    } else if (part == ContentPart.EPILOGUE) {
1002
                        Log.get().fine("Ignoring in " + part + " : " + bodyChild);
1003
                    }
1004
                } else if (part == ContentPart.MAIN) {
1005
                    mainElements.add((Content) c.clone());
1006
                } else {
1007
                    Log.get().finer("Ignoring non-element in " + part);
17 ilm 1008
                }
73 ilm 1009
            }
1010
        }
17 ilm 1011
 
73 ilm 1012
        if (nonNullWhere == thisBody) {
1013
            assert index >= 0;
1014
            index += prologueAddedCount;
1015
            assert index >= 0 && index <= thisBody.getContentSize();
1016
        }
1017
        if (index < 0)
1018
            nonNullWhere.addContent(mainElements);
1019
        else
1020
            nonNullWhere.addContent(index, mainElements);
17 ilm 1021
    }
1022
 
1023
    /**
1024
     * Detach the children of elem whose names already exist in the body.
1025
     *
1026
     * @param elem the elem to be trimmed.
1027
     * @throws JDOMException if an error occurs.
1028
     */
1029
    protected final void detachDuplicate(Element elem) throws JDOMException {
1030
        final String singularName = elem.getName().substring(0, elem.getName().length() - 1);
1031
        final List thisNames = getXPath("./text:" + singularName + "s/text:" + singularName + "/@text:name").selectNodes(getChild("body"));
67 ilm 1032
        org.apache.commons.collections.CollectionUtils.transform(thisNames, new Transformer() {
17 ilm 1033
            public Object transform(Object obj) {
1034
                return ((Attribute) obj).getValue();
1035
            }
1036
        });
1037
 
1038
        final Iterator iter = elem.getChildren().iterator();
1039
        while (iter.hasNext()) {
1040
            final Element decl = (Element) iter.next();
1041
            if (thisNames.contains(decl.getAttributeValue("name", getVersion().getTEXT()))) {
1042
                // on retire les déjà existant
1043
                iter.remove();
1044
            }
1045
        }
1046
    }
1047
 
1048
    // *** Utils
1049
 
1050
    public final Element getBody() {
1051
        return this.getContentTypeVersioned().getBody(getDocument());
1052
    }
1053
 
1054
    private ContentTypeVersioned getContentTypeVersioned() {
61 ilm 1055
        return getPackage().getContentType();
17 ilm 1056
    }
1057
 
1058
    /**
1059
     * Préfixe les attributs en ayant besoin.
1060
     *
1061
     * @param elem l'élément à préfixer.
1062
     * @param references whether to prefix hrefs.
73 ilm 1063
     * @return <code>elem</code>.
17 ilm 1064
     * @throws JDOMException if an error occurs.
1065
     */
73 ilm 1066
    Element prefix(Element elem, boolean references) throws JDOMException {
1067
        Iterator attrs = this.getXPath(".//@text:style-name | .//@table:style-name | .//@draw:style-name | .//@style:data-style-name | .//@style:apply-style-name").selectNodes(elem).iterator();
17 ilm 1068
        while (attrs.hasNext()) {
1069
            Attribute attr = (Attribute) attrs.next();
1070
            // text:list/@text:style-name references text:list-style
1071
            if (!this.listStylesNames.contains(attr.getValue()) && !this.stylesNames.contains(attr.getValue())) {
1072
                attr.setValue(this.prefix(attr.getValue()));
1073
            }
1074
        }
1075
 
1076
        attrs = this.getXPath(".//@style:list-style-name").selectNodes(elem).iterator();
1077
        while (attrs.hasNext()) {
1078
            Attribute attr = (Attribute) attrs.next();
1079
            if (!this.listStylesNames.contains(attr.getValue())) {
1080
                attr.setValue(this.prefix(attr.getValue()));
1081
            }
1082
        }
1083
 
1084
        attrs = this.getXPath(".//@style:page-master-name | .//@style:page-layout-name | .//@text:name | .//@form:name | .//@form:property-name").selectNodes(elem).iterator();
1085
        while (attrs.hasNext()) {
1086
            final Attribute attr = (Attribute) attrs.next();
1087
            final String parentName = attr.getParent().getName();
1088
            if (!DONT_PREFIX.contains(parentName))
1089
                attr.setValue(this.prefix(attr.getValue()));
1090
        }
1091
 
1092
        // prefix references
1093
        if (references) {
1094
            attrs = this.getXPath(".//@xlink:href[../@xlink:show='embed']").selectNodes(elem).iterator();
1095
            while (attrs.hasNext()) {
1096
                final Attribute attr = (Attribute) attrs.next();
1097
                final String prefixedPath = this.prefixPath(attr.getValue());
1098
                if (prefixedPath != null)
1099
                    attr.setValue(prefixedPath);
1100
            }
1101
        }
73 ilm 1102
        return elem;
17 ilm 1103
    }
1104
 
1105
    /**
1106
     * Prefix a path.
1107
     *
1108
     * @param href a path inside the pkg, eg "./Object 1/content.xml".
1109
     * @return the prefixed path or <code>null</code> if href is external, eg "./3_Object
1110
     *         1/content.xml".
1111
     */
1112
    private String prefixPath(final String href) {
1113
        if (this.getVersion().equals(XMLVersion.OOo)) {
1114
            // in OOo 1.x inPKG is denoted by a #
1115
            final boolean sharp = href.startsWith("#");
1116
            if (sharp)
1117
                // eg #Pictures/100000000000006C000000ABCC02339E.png
1118
                return "#" + this.prefix(href.substring(1));
1119
            else
1120
                // eg ../../../../Program%20Files/OpenOffice.org1.1.5/share/gallery/apples.gif
1121
                return null;
1122
        } else {
1123
            URI uri;
1124
            try {
1125
                uri = new URI(href);
1126
            } catch (URISyntaxException e) {
1127
                // OO doesn't escape characters for files
1128
                uri = null;
1129
            }
1130
            // section 17.5
1131
            final boolean inPKGFile = uri == null || uri.getScheme() == null && uri.getAuthority() == null && uri.getPath().charAt(0) != '/';
1132
            if (inPKGFile) {
1133
                final String dotSlash = "./";
1134
                if (href.startsWith(dotSlash))
1135
                    return dotSlash + this.prefix(href.substring(dotSlash.length()));
1136
                else
1137
                    return this.prefix(href);
1138
            } else
1139
                return null;
1140
        }
1141
    }
1142
 
1143
    private String prefix(String value) {
1144
        return "_" + this.numero + value;
1145
    }
1146
 
1147
    private final ElementTransformer prefixTransf = new ElementTransformer() {
1148
        public Element transform(Element elem) throws JDOMException {
73 ilm 1149
            return ODSingleXMLDocument.this.prefix(elem, true);
17 ilm 1150
        }
1151
    };
1152
 
1153
    private final ElementTransformer prefixTransfNoRef = new ElementTransformer() {
1154
        public Element transform(Element elem) throws JDOMException {
73 ilm 1155
            return ODSingleXMLDocument.this.prefix(elem, false);
17 ilm 1156
        }
1157
    };
1158
 
1159
    /**
1160
     * Ajoute dans ce document seulement les éléments de doc correspondant au XPath spécifié et dont
1161
     * la valeur de l'attribut style:name n'existe pas déjà.
1162
     *
1163
     * @param doc le document à fusionner avec celui-ci.
1164
     * @param topElem eg "office:font-decls".
1165
     * @param elemToMerge les éléments à fusionner (par rapport à topElem), eg "style:font-decl".
1166
     * @return les noms des éléments ajoutés.
1167
     * @throws JDOMException
1168
     * @see #mergeUnique(ODSingleXMLDocument, String, String, ElementTransformer)
1169
     */
1170
    private List<String> mergeUnique(ODXMLDocument doc, String topElem, String elemToMerge) throws JDOMException {
1171
        return this.mergeUnique(doc, topElem, elemToMerge, NOP_ElementTransformer);
1172
    }
1173
 
1174
    /**
1175
     * Ajoute dans ce document seulement les éléments de doc correspondant au XPath spécifié et dont
1176
     * la valeur de l'attribut style:name n'existe pas déjà. En conséquence n'ajoute que les
1177
     * éléments possédant un attribut style:name.
1178
     *
1179
     * @param doc le document à fusionner avec celui-ci.
1180
     * @param topElem eg "office:font-decls".
1181
     * @param elemToMerge les éléments à fusionner (par rapport à topElem), eg "style:font-decl".
1182
     * @param addTransf la transformation à appliquer avant d'ajouter.
1183
     * @return les noms des éléments ajoutés.
1184
     * @throws JDOMException
1185
     */
1186
    private List<String> mergeUnique(ODXMLDocument doc, String topElem, String elemToMerge, ElementTransformer addTransf) throws JDOMException {
1187
        return this.mergeUnique(doc, topElem, elemToMerge, "style:name", addTransf);
1188
    }
1189
 
1190
    private List<String> mergeUnique(ODXMLDocument doc, String topElem, String elemToMerge, String attrFQName, ElementTransformer addTransf) throws JDOMException {
1191
        List<String> added = new ArrayList<String>();
1192
        Element thisParent = this.getChild(topElem, true);
1193
 
1194
        XPath xp = this.getXPath("./" + elemToMerge + "/@" + attrFQName);
1195
 
1196
        // les styles de ce document
1197
        List thisElemNames = xp.selectNodes(thisParent);
1198
        // on transforme la liste d'attributs en liste de String
67 ilm 1199
        org.apache.commons.collections.CollectionUtils.transform(thisElemNames, new Transformer() {
17 ilm 1200
            public Object transform(Object obj) {
1201
                return ((Attribute) obj).getValue();
1202
            }
1203
        });
1204
 
1205
        // pour chaque style de l'autre document
1206
        Iterator otherElemNames = xp.selectNodes(doc.getChild(topElem)).iterator();
1207
        while (otherElemNames.hasNext()) {
1208
            Attribute attr = (Attribute) otherElemNames.next();
1209
            // on l'ajoute si non déjà dedans
1210
            if (!thisElemNames.contains(attr.getValue())) {
1211
                thisParent.addContent(addTransf.transform((Element) attr.getParent().clone()));
1212
                added.add(attr.getValue());
1213
            }
1214
        }
1215
 
1216
        return added;
1217
    }
1218
 
1219
    /**
1220
     * Ajoute l'élément elemName de doc, s'il n'est pas dans ce document.
1221
     *
1222
     * @param doc le document à fusionner avec celui-ci.
1223
     * @param elemName l'élément à ajouter, eg "outline-style".
1224
     * @throws JDOMException if elemName is not valid.
1225
     */
1226
    private void addStylesIfNotPresent(ODXMLDocument doc, String elemName) throws JDOMException {
1227
        this.addIfNotPresent(doc, "./office:styles/text:" + elemName);
1228
    }
1229
 
1230
    /**
1231
     * Prefixe les fils de auto-styles possédant un attribut "name" avant de les ajouter.
1232
     *
1233
     * @param doc le document à fusionner avec celui-ci.
1234
     * @return les élément ayant été ajoutés.
1235
     * @throws JDOMException
1236
     */
1237
    private List<Element> prefixAndAddAutoStyles(ODXMLDocument doc) throws JDOMException {
73 ilm 1238
        return addStyles(doc, "automatic-styles", Step.getAnyChildElementStep(), true);
1239
    }
1240
 
1241
    // add styles from doc/rootElem/styleElemStep/@style:name, optionally prefixing
1242
    private List<Element> addStyles(ODXMLDocument doc, final String rootElem, final Step<Element> styleElemStep, boolean prefix) throws JDOMException {
1243
        // needed since we add to us directly under rootElem
1244
        if (styleElemStep.getAxis() != Axis.child)
1245
            throw new IllegalArgumentException("Not child axis : " + styleElemStep.getAxis());
17 ilm 1246
        final List<Element> result = new ArrayList<Element>(128);
73 ilm 1247
        final Element thisChild = this.getChild(rootElem);
1248
        // find all elements with a style:name in doc
1249
        final SimpleXMLPath<Attribute> simplePath = SimpleXMLPath.create(styleElemStep, Step.createAttributeStep("name", "style"));
1250
        for (final Attribute attr : simplePath.selectNodes(doc.getChild(rootElem))) {
1251
            final Element parent = (Element) attr.getParent().clone();
1252
            // prefix their name
1253
            if (prefix)
1254
                parent.setAttribute(attr.getName(), this.prefix(attr.getValue()), attr.getNamespace());
1255
            // and add to us
1256
            thisChild.addContent(parent);
17 ilm 1257
            result.add(parent);
1258
        }
1259
        return result;
1260
    }
1261
 
1262
    /**
1263
     * Return <code>true</code> if this document was split.
1264
     *
1265
     * @return <code>true</code> if this has no package anymore.
1266
     * @see ODPackage#split()
1267
     */
1268
    public final boolean isDead() {
1269
        return this.getPackage() == null;
1270
    }
1271
 
1272
    final Map<RootElement, Document> split() {
1273
        final Map<RootElement, Document> res = new HashMap<RootElement, Document>();
1274
        final XMLVersion version = getVersion();
1275
        final Element root = this.getDocument().getRootElement();
25 ilm 1276
        final XMLFormatVersion officeVersion = getFormatVersion();
17 ilm 1277
 
1278
        // meta
1279
        {
1280
            final Element thisMeta = root.getChild("meta", version.getOFFICE());
1281
            if (thisMeta != null) {
25 ilm 1282
                final Document meta = createDocument(res, RootElement.META, officeVersion);
17 ilm 1283
                meta.getRootElement().addContent(thisMeta.detach());
1284
            }
1285
        }
1286
        // settings
1287
        {
1288
            final Element thisSettings = root.getChild("settings", version.getOFFICE());
1289
            if (thisSettings != null) {
25 ilm 1290
                final Document settings = createDocument(res, RootElement.SETTINGS, officeVersion);
17 ilm 1291
                settings.getRootElement().addContent(thisSettings.detach());
1292
            }
1293
        }
73 ilm 1294
        // scripts
1295
        {
1296
            final Element thisScript = this.getBasicScriptElem();
1297
            if (thisScript != null) {
1298
                final Map<String, Library> basicLibraries = this.readBasicLibraries(thisScript).get0();
1299
                final Element lcRootElem = new Element("libraries", XMLVersion.LIBRARY_NS);
1300
                for (final Library lib : basicLibraries.values()) {
1301
                    lcRootElem.addContent(lib.toPackageLibrariesElement(officeVersion));
1302
                    for (final Entry<String, Document> e : lib.toPackageDocuments(officeVersion).entrySet()) {
1303
                        this.pkg.putFile(Library.DIR_NAME + "/" + lib.getName() + "/" + e.getKey(), e.getValue(), FileUtils.XML_TYPE);
1304
                    }
1305
                }
1306
                final Document lc = new Document(lcRootElem, new DocType("library:libraries", "-//OpenOffice.org//DTD OfficeDocument 1.0//EN", "libraries.dtd"));
1307
                this.pkg.putFile(Library.DIR_NAME + "/" + Library.LIBRARY_LIST_FILENAME, lc, FileUtils.XML_TYPE);
1308
                thisScript.detach();
1309
                // nothing to do for dialogs, since they cannot be in our Document
1310
            }
1311
        }
17 ilm 1312
        // styles
1313
        // we must move office:styles, office:master-styles and referenced office:automatic-styles
1314
        {
25 ilm 1315
            final Document styles = createDocument(res, RootElement.STYLES, officeVersion);
17 ilm 1316
            // don't bother finding out which font is used where since there isn't that many of them
1317
            styles.getRootElement().addContent((Element) root.getChild(getFontDecls()[0], version.getOFFICE()).clone());
1318
            // extract common styles
1319
            styles.getRootElement().addContent(root.getChild("styles", version.getOFFICE()).detach());
1320
            // only automatic styles used in the styles themselves.
1321
            final Element contentAutoStyles = root.getChild("automatic-styles", version.getOFFICE());
1322
            final Element stylesAutoStyles = new Element(contentAutoStyles.getName(), contentAutoStyles.getNamespace());
1323
            final Element masterStyles = root.getChild("master-styles", version.getOFFICE());
1324
 
1325
            // style elements referenced, e.g. <style:page-layout style:name="pm1">
1326
            final Set<Element> referenced = new HashSet<Element>();
1327
 
1328
            final SimpleXMLPath<Attribute> descAttrs = SimpleXMLPath.create(Step.createElementStep(Axis.descendantOrSelf, null), Step.createAttributeStep(null, null));
1329
            for (final Attribute attr : descAttrs.selectNodes(masterStyles)) {
1330
                final Element referencedStyleElement = Style.getReferencedStyleElement(this.pkg, attr);
1331
                if (referencedStyleElement != null)
1332
                    referenced.add(referencedStyleElement);
1333
            }
1334
            for (final Element r : referenced) {
1335
                // since we already removed common styles
1336
                assert r.getParentElement() == contentAutoStyles;
1337
                stylesAutoStyles.addContent(r.detach());
1338
            }
1339
 
1340
            styles.getRootElement().addContent(stylesAutoStyles);
1341
            styles.getRootElement().addContent(masterStyles.detach());
1342
        }
1343
        // content
1344
        {
61 ilm 1345
            // store before emptying package
1346
            final ContentTypeVersioned contentTypeVersioned = getContentTypeVersioned();
1347
            // needed since the content will be emptied (which can cause methods of ODPackage to
1348
            // fail, e.g. setTypeAndVersion())
1349
            this.pkg.rmFile(RootElement.CONTENT.getZipEntry());
17 ilm 1350
            this.pkg = null;
25 ilm 1351
            final Document content = createDocument(res, RootElement.CONTENT, officeVersion);
61 ilm 1352
            contentTypeVersioned.setType(content);
17 ilm 1353
            content.getRootElement().addContent(root.removeContent());
1354
        }
1355
        return res;
1356
    }
1357
 
25 ilm 1358
    private Document createDocument(final Map<RootElement, Document> res, RootElement rootElement, final XMLFormatVersion version) {
1359
        final Document doc = rootElement.createDocument(version);
17 ilm 1360
        copyNS(this.getDocument(), doc);
1361
        res.put(rootElement, doc);
1362
        return doc;
1363
    }
1364
 
1365
    /**
1366
     * Saves this OO document to a file.
1367
     *
1368
     * @param f the file where this document will be saved, without extension, eg "dir/myfile".
1369
     * @return the actual file where it has been saved (with extension), eg "dir/myfile.odt".
1370
     * @throws IOException if an error occurs.
1371
     */
61 ilm 1372
    public File saveToPackageAs(File f) throws IOException {
17 ilm 1373
        return this.pkg.saveAs(f);
1374
    }
1375
 
61 ilm 1376
    public File save() throws IOException {
1377
        return this.saveAs(this.getPackage().getFile());
1378
    }
1379
 
1380
    public File saveAs(File fNoExt) throws IOException {
1381
        final Document doc = (Document) getDocument().clone();
1382
        for (final Attribute hrefAttr : ALL_HREF_ATTRIBUTES.selectNodes(doc.getRootElement())) {
1383
            final String href = hrefAttr.getValue();
1384
            if (href.startsWith("../")) {
1385
                // update href
1386
                hrefAttr.setValue(href.substring(3));
1387
            } else if (!URI.create(href).isAbsolute()) {
1388
                // encode binaries
1389
                final Element hrefParent = hrefAttr.getParent();
1390
                if (!BINARY_DATA_PARENTS.contains(hrefParent.getQualifiedName()))
1391
                    throw new IllegalStateException("Cannot convert to binary data element : " + hrefParent);
1392
                final Element binaryData = new Element("binary-data", getPackage().getVersion().getOFFICE());
1393
 
1394
                binaryData.setText(Base64.encodeBytes(getPackage().getBinaryFile(href)));
1395
                hrefParent.addContent(binaryData);
1396
                // If this element is present, an xlink:href attribute in its parent element
1397
                // shall be ignored. But LO doesn't respect that
1398
                hrefAttr.detach();
1399
            }
1400
        }
1401
 
1402
        final File f = this.getPackage().getContentType().addExt(fNoExt, true);
1403
        FileUtils.write(ODPackage.createOutputter().outputString(doc), f);
1404
        return f;
1405
    }
1406
 
17 ilm 1407
    private Element getPageBreak() {
1408
        if (this.pageBreak == null) {
1409
            final String styleName = "PageBreak";
1410
            try {
1411
                final XPath xp = this.getXPath("./style:style[@style:name='" + styleName + "']");
1412
                final Element styles = this.getChild("styles", true);
1413
                if (xp.selectSingleNode(styles) == null) {
1414
                    final Element pageBreakStyle = new Element("style", this.getVersion().getSTYLE());
1415
                    pageBreakStyle.setAttribute("name", styleName, this.getVersion().getSTYLE());
1416
                    pageBreakStyle.setAttribute("family", "paragraph", this.getVersion().getSTYLE());
1417
                    pageBreakStyle.setContent(getPProps().setAttribute("break-after", "page", this.getVersion().getNS("fo")));
1418
                    // <element name="office:styles"> <interleave>...
1419
                    // so just append the new style
1420
                    styles.addContent(pageBreakStyle);
73 ilm 1421
                    this.stylesNames.add(styleName);
17 ilm 1422
                }
1423
            } catch (JDOMException e) {
1424
                // static path, shouldn't happen
1425
                throw new IllegalStateException("pb while searching for " + styleName, e);
1426
            }
1427
            this.pageBreak = new Element("p", this.getVersion().getTEXT()).setAttribute("style-name", styleName, this.getVersion().getTEXT());
1428
        }
1429
        return (Element) this.pageBreak.clone();
1430
    }
1431
 
1432
    private final Element getPProps() {
19 ilm 1433
        return this.getXML().createFormattingProperties("paragraph");
17 ilm 1434
    }
1435
}