OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

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