OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 57 | Rev 67 | 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;
61 ilm 18
import org.openconcerto.utils.Base64;
19
import org.openconcerto.utils.CollectionUtils;
17 ilm 20
import org.openconcerto.utils.CopyUtils;
61 ilm 21
import org.openconcerto.utils.FileUtils;
17 ilm 22
import org.openconcerto.utils.cc.IFactory;
23
import org.openconcerto.xml.JDOMUtils;
24
import org.openconcerto.xml.SimpleXMLPath;
25
import org.openconcerto.xml.Step;
26
import org.openconcerto.xml.Step.Axis;
27
 
28
import java.io.File;
29
import java.io.IOException;
57 ilm 30
import java.io.InputStream;
17 ilm 31
import java.math.BigDecimal;
32
import java.net.URI;
33
import java.net.URISyntaxException;
34
import java.util.ArrayList;
35
import java.util.HashMap;
36
import java.util.HashSet;
37
import java.util.Iterator;
38
import java.util.List;
39
import java.util.Map;
40
import java.util.Set;
41
 
42
import org.apache.commons.collections.Transformer;
43
import org.jdom.Attribute;
44
import org.jdom.Document;
45
import org.jdom.Element;
46
import org.jdom.JDOMException;
47
import org.jdom.Namespace;
48
import org.jdom.xpath.XPath;
49
 
50
/**
51
 * An XML document containing all of an office document, see section 2.1 of OpenDocument 1.1.
52
 *
53
 * @author Sylvain CUAZ 24 nov. 2004
54
 */
25 ilm 55
public class ODSingleXMLDocument extends ODXMLDocument implements Cloneable {
17 ilm 56
 
61 ilm 57
    private static final SimpleXMLPath<Attribute> ALL_HREF_ATTRIBUTES = SimpleXMLPath.allAttributes("href", "xlink");
58
    private static final SimpleXMLPath<Element> ALL_BINARY_DATA_ELEMENTS = SimpleXMLPath.allElements("binary-data", "office");
59
    // see 10.4.5 <office:binary-data> of OpenDocument-v1.2-os
60
    private static final Set<String> BINARY_DATA_PARENTS = CollectionUtils.createSet("draw:image", "draw:object-ole", "style:background-image", "text:list-level-style-image");
61
 
17 ilm 62
    final static Set<String> DONT_PREFIX;
63
    static {
64
        DONT_PREFIX = new HashSet<String>();
65
        // don't touch to user fields and variables
66
        // we want them to be the same across the document
67
        DONT_PREFIX.add("user-field-decl");
68
        DONT_PREFIX.add("user-field-get");
69
        DONT_PREFIX.add("variable-get");
70
        DONT_PREFIX.add("variable-decl");
71
        DONT_PREFIX.add("variable-set");
72
    }
73
 
74
    // Voir le TODO du ctor
75
    // public static OOSingleXMLDocument createEmpty() {
76
    // }
77
 
78
    /**
79
     * Create a document from a collection of subdocuments.
80
     *
81
     * @param content the content.
82
     * @param style the styles, can be <code>null</code>.
83
     * @return the merged document.
84
     */
85
    public static ODSingleXMLDocument createFromDocument(Document content, Document style) {
25 ilm 86
        return ODPackage.createFromDocuments(content, style).toSingle();
17 ilm 87
    }
88
 
25 ilm 89
    static ODSingleXMLDocument create(ODPackage files) {
90
        final Document content = files.getContent().getDocument();
91
        final Document style = files.getDocument(RootElement.STYLES.getZipEntry());
17 ilm 92
        // signal that the xml is a complete document (was document-content)
93
        final Document singleContent = RootElement.createSingle(content);
94
        copyNS(content, singleContent);
95
        files.getContentType().setType(singleContent);
96
        final Element root = singleContent.getRootElement();
97
        root.addContent(content.getRootElement().removeContent());
98
        // see section 2.1.1 first meta, then settings, then the rest
25 ilm 99
        prependToRoot(files.getDocument(RootElement.SETTINGS.getZipEntry()), root);
100
        prependToRoot(files.getDocument(RootElement.META.getZipEntry()), root);
17 ilm 101
        final ODSingleXMLDocument single = new ODSingleXMLDocument(singleContent, files);
102
        if (single.getChild("body") == null)
103
            throw new IllegalArgumentException("no body in " + single);
104
        if (style != null) {
105
            // section 2.1 : Styles used in the document content and automatic styles used in the
106
            // styles themselves.
107
            // more precisely in section 2.1.1 : office:document-styles contains style, master
108
            // style, auto style, font decls ; the last two being also in content.xml but are *not*
109
            // related : eg P1 of styles.xml is *not* the P1 of content.xml
110
            try {
111
                single.mergeAllStyles(new ODXMLDocument(style), true);
112
            } catch (JDOMException e) {
113
                throw new IllegalArgumentException("style is not valid", e);
114
            }
115
        }
116
        return single;
117
    }
118
 
119
    private static void prependToRoot(Document settings, final Element root) {
120
        if (settings != null) {
121
            copyNS(settings, root.getDocument());
122
            final Element officeSettings = (Element) settings.getRootElement().getChildren().get(0);
123
            root.addContent(0, (Element) officeSettings.clone());
124
        }
125
    }
126
 
127
    // some namespaces are needed even if not directly used, see § 18.3.19 namespacedToken
128
    // of v1.2-part1-cd04 (e.g. 19.31 config:name or 19.260 form:control-implementation)
129
    @SuppressWarnings("unchecked")
130
    private static void copyNS(final Document src, final Document dest) {
131
        JDOMUtils.addNamespaces(dest.getRootElement(), src.getRootElement().getAdditionalNamespaces());
132
    }
133
 
134
    /**
57 ilm 135
     * Create a document from a package.
17 ilm 136
     *
137
     * @param f an OpenDocument package file.
138
     * @return the merged file.
139
     * @throws JDOMException if the file is not a valid OpenDocument file.
140
     * @throws IOException if the file can't be read.
141
     */
57 ilm 142
    public static ODSingleXMLDocument createFromPackage(File f) throws JDOMException, IOException {
17 ilm 143
        // this loads all linked files
144
        return new ODPackage(f).toSingle();
145
    }
146
 
147
    /**
57 ilm 148
     * Create a document from a flat XML.
149
     *
150
     * @param f an OpenDocument XML file.
151
     * @return the created file.
152
     * @throws JDOMException if the file is not a valid OpenDocument file.
153
     * @throws IOException if the file can't be read.
154
     */
155
    public static ODSingleXMLDocument createFromFile(File f) throws JDOMException, IOException {
156
        final ODSingleXMLDocument res = new ODSingleXMLDocument(OOUtils.getBuilder().build(f));
157
        res.getPackage().setFile(f);
158
        return res;
159
    }
160
 
161
    public static ODSingleXMLDocument createFromStream(InputStream ins) throws JDOMException, IOException {
162
        return new ODSingleXMLDocument(OOUtils.getBuilder().build(ins));
163
    }
164
 
165
    /**
17 ilm 166
     * fix bug when a SingleXMLDoc is used to create a document (for example with P2 and 1_P2), and
167
     * then create another instance s2 with the previous document and add a second file (also with
168
     * P2 and 1_P2) => s2 will contain P2, 1_P2, 1_P2, 1_1_P2.
169
     */
170
    private static final String COUNT = "SingleXMLDocument_count";
171
 
172
    /** Le nombre de fichiers concat */
173
    private int numero;
174
    /** Les styles présent dans ce document */
175
    private final Set<String> stylesNames;
176
    /** Les styles de liste présent dans ce document */
177
    private final Set<String> listStylesNames;
178
    /** Les fichiers référencés par ce document */
179
    private ODPackage pkg;
180
    private final ODMeta meta;
181
    // the element between each page
182
    private Element pageBreak;
183
 
184
    public ODSingleXMLDocument(Document content) {
61 ilm 185
        this(content, null);
17 ilm 186
    }
187
 
188
    /**
189
     * A new single document. NOTE: this document will put himself in <code>pkg</code>, replacing
190
     * any previous content.
191
     *
192
     * @param content the XML.
193
     * @param pkg the package this document belongs to.
194
     */
195
    private ODSingleXMLDocument(Document content, final ODPackage pkg) {
196
        super(content);
197
 
198
        // inited in getPageBreak()
199
        this.pageBreak = null;
200
 
61 ilm 201
        final boolean contentIsFlat = pkg == null;
202
        this.pkg = contentIsFlat ? new ODPackage() : pkg;
17 ilm 203
        for (final RootElement e : RootElement.getPackageElements())
204
            this.pkg.rmFile(e.getZipEntry());
205
        this.pkg.putFile(CONTENT.getZipEntry(), this, "text/xml");
206
 
61 ilm 207
        // update href
208
        if (contentIsFlat) {
209
            // OD thinks of the ZIP archive as an additional folder
210
            for (final Attribute hrefAttr : ALL_HREF_ATTRIBUTES.selectNodes(getDocument().getRootElement())) {
211
                final String href = hrefAttr.getValue();
212
                if (!URI.create(href).isAbsolute())
213
                    hrefAttr.setValue("../" + href);
214
            }
215
        }
216
        // decode Base64 binaries
217
        for (final Element binaryDataElem : ALL_BINARY_DATA_ELEMENTS.selectNodes(getDocument().getRootElement())) {
218
            final String name;
219
            int i = 1;
220
            final Set<String> entries = getPackage().getEntries();
221
            final Element binaryParentElement = binaryDataElem.getParentElement();
222
            while (entries.contains(binaryParentElement.getName() + "/" + i))
223
                i++;
224
            name = binaryParentElement.getName() + "/" + i;
225
            getPackage().putFile(name, Base64.decode(binaryDataElem.getText()));
226
            binaryParentElement.setAttribute("href", name, binaryDataElem.getNamespace("xlink"));
227
            binaryDataElem.detach();
228
        }
17 ilm 229
 
61 ilm 230
        this.meta = this.getPackage().getMeta(true);
231
 
17 ilm 232
        final ODUserDefinedMeta userMeta = this.meta.getUserMeta(COUNT);
233
        if (userMeta != null) {
234
            final Object countValue = userMeta.getValue();
235
            if (countValue instanceof Number) {
236
                this.numero = ((Number) countValue).intValue();
237
            } else {
238
                this.numero = new BigDecimal(countValue.toString()).intValue();
239
            }
240
        } else {
241
            // if not hasCount(), it's not us that created content
242
            // so there should not be any 1_
243
            this.setNumero(0);
244
        }
245
 
246
        this.stylesNames = new HashSet<String>(64);
247
        this.listStylesNames = new HashSet<String>(16);
248
 
249
        // little trick to find the common styles names (not to be prefixed so they remain
250
        // consistent across the added documents)
251
        final Element styles = this.getChild("styles");
252
        if (styles != null) {
253
            // create a second document with our styles to collect names
254
            final Element root = this.getDocument().getRootElement();
255
            final Document clonedDoc = new Document(new Element(root.getName(), root.getNamespace()));
256
            clonedDoc.getRootElement().addContent(styles.detach());
257
            try {
258
                this.mergeStyles(new ODXMLDocument(clonedDoc));
259
            } catch (JDOMException e) {
260
                throw new IllegalArgumentException("can't find common styles names.");
261
            }
262
            // reattach our styles
263
            styles.detach();
264
            this.setChild(styles);
265
        }
266
    }
267
 
268
    ODSingleXMLDocument(ODSingleXMLDocument doc, ODPackage p) {
269
        super(doc);
270
        if (p == null)
271
            throw new NullPointerException("Null package");
272
        this.stylesNames = new HashSet<String>(doc.stylesNames);
273
        this.listStylesNames = new HashSet<String>(doc.listStylesNames);
274
        this.pkg = p;
275
        this.meta = ODMeta.create(this);
276
        this.setNumero(doc.numero);
277
    }
278
 
279
    @Override
280
    public ODSingleXMLDocument clone() {
281
        final ODPackage copy = new ODPackage(this.pkg);
282
        return (ODSingleXMLDocument) copy.getContent();
283
    }
284
 
285
    private void setNumero(int numero) {
286
        this.numero = numero;
287
        this.meta.getUserMeta(COUNT, true).setValue(this.numero);
288
    }
289
 
290
    /**
291
     * The number of files concatenated with {@link #add(ODSingleXMLDocument)}.
292
     *
293
     * @return number of files concatenated.
294
     */
295
    public final int getNumero() {
296
        return this.numero;
297
    }
298
 
299
    public ODPackage getPackage() {
300
        return this.pkg;
301
    }
302
 
303
    /**
304
     * Append a document.
305
     *
306
     * @param doc the document to add.
307
     */
308
    public synchronized void add(ODSingleXMLDocument doc) {
309
        // ajoute un saut de page entre chaque document
310
        this.add(doc, true);
311
    }
312
 
313
    /**
314
     * Append a document.
315
     *
316
     * @param doc the document to add, <code>null</code> means no-op.
317
     * @param pageBreak whether a page break should be inserted before <code>doc</code>.
318
     */
319
    public synchronized void add(ODSingleXMLDocument doc, boolean pageBreak) {
320
        if (doc != null && pageBreak)
321
            // only add a page break, if a page was really added
322
            this.getBody().addContent(this.getPageBreak());
323
        this.add(null, 0, doc);
324
    }
325
 
326
    public synchronized void replace(Element elem, ODSingleXMLDocument doc) {
327
        final Element parent = elem.getParentElement();
328
        this.add(parent, parent.indexOf(elem), doc);
329
        elem.detach();
330
    }
331
 
332
    public synchronized void add(Element where, int index, ODSingleXMLDocument doc) {
333
        if (doc == null)
334
            return;
335
        if (!this.getVersion().equals(doc.getVersion()))
336
            throw new IllegalArgumentException("version mismatch");
337
 
338
        this.setNumero(this.numero + 1);
339
        try {
340
            copyNS(doc.getDocument(), this.getDocument());
341
            this.mergeEmbedded(doc);
342
            this.mergeSettings(doc);
343
            this.mergeAllStyles(doc, false);
344
            this.mergeBody(where, index, doc);
345
        } catch (JDOMException exn) {
346
            throw new IllegalArgumentException("XML error", exn);
347
        }
348
    }
349
 
350
    /**
351
     * Merge the four elements of style.
352
     *
353
     * @param doc the xml document to merge.
354
     * @param sameDoc whether <code>doc</code> is the same OpenDocument than this, eg
355
     *        <code>true</code> when merging content.xml and styles.xml.
356
     * @throws JDOMException if an error occurs.
357
     */
358
    private void mergeAllStyles(ODXMLDocument doc, boolean sameDoc) throws JDOMException {
359
        // no reference
360
        this.mergeFontDecls(doc);
361
        // section 14.1
362
        // § Parent Style only refer to other common styles
363
        // § Next Style cannot refer to an autostyle (only available in common styles)
364
        // § List Style can refer to an autostyle
365
        // § Master Page Name cannot (auto master pages does not exist)
366
        // § Data Style Name (for cells) can
367
        // but since the UI for common styles doesn't allow to customize List Style
368
        // and there is no common styles for tables : office:styles doesn't reference any automatic
369
        // styles
370
        this.mergeStyles(doc);
371
        // on the contrary autostyles do refer to other autostyles :
372
        // choosing "activate bullets" will create an automatic paragraph style:style
373
        // referencing an automatic text:list-style.
374
        this.mergeAutoStyles(doc, !sameDoc);
375
        // section 14.4
376
        // § Page Layout can refer to an autostyle
377
        // § Next Style Name refer to another masterPage
378
        this.mergeMasterStyles(doc, !sameDoc);
379
    }
380
 
381
    private void mergeEmbedded(ODSingleXMLDocument doc) {
382
        // since we are adding another document our existing thumbnail is obsolete
383
        this.pkg.rmFile("Thumbnails/thumbnail.png");
384
        // copy the files
385
        final ODPackage opkg = CopyUtils.copy(doc.pkg);
386
        for (final String name : opkg.getEntries()) {
387
            final ODPackageEntry e = opkg.getEntry(name);
388
            if (!ODPackage.isStandardFile(e.getName())) {
389
                this.pkg.putFile(this.prefix(e.getName()), e.getData(), e.getType());
390
            }
391
        }
392
    }
393
 
394
    private void mergeSettings(ODSingleXMLDocument doc) throws JDOMException {
395
        this.addIfNotPresent(doc, "./office:settings", 0);
396
    }
397
 
398
    /**
399
     * Fusionne les office:font-decls/style:font-decl. On ne préfixe jamais, on ajoute seulement si
400
     * l'attribut style:name est différent.
401
     *
402
     * @param doc le document à fusionner avec celui-ci.
403
     * @throws JDOMException
404
     */
405
    private void mergeFontDecls(ODXMLDocument doc) throws JDOMException {
406
        final String[] fontDecls = this.getFontDecls();
407
        this.mergeUnique(doc, fontDecls[0], fontDecls[1]);
408
    }
409
 
410
    private String[] getFontDecls() {
19 ilm 411
        return getXML().getFontDecls();
17 ilm 412
    }
413
 
414
    // merge everything under office:styles
415
    private void mergeStyles(ODXMLDocument doc) throws JDOMException {
416
        // les default-style (notamment tab-stop-distance)
417
        this.mergeUnique(doc, "styles", "style:default-style", "style:family", NOP_ElementTransformer);
418
        // les styles
419
        this.stylesNames.addAll(this.mergeUnique(doc, "styles", "style:style"));
420
        // on ajoute outline-style si non présent
421
        this.addStylesIfNotPresent(doc, "outline-style");
422
        // les list-style
423
        this.listStylesNames.addAll(this.mergeUnique(doc, "styles", "text:list-style"));
424
        // les *notes-configuration
425
        if (getVersion() == XMLVersion.OOo) {
426
            this.addStylesIfNotPresent(doc, "footnotes-configuration");
427
            this.addStylesIfNotPresent(doc, "endnotes-configuration");
428
        } else {
429
            // 16.29.3 : specifies values for each note class used in a document
430
            this.mergeUnique(doc, "styles", "text:notes-configuration", "text:note-class", NOP_ElementTransformer);
431
        }
432
        this.addStylesIfNotPresent(doc, "bibliography-configuration");
433
        this.addStylesIfNotPresent(doc, "linenumbering-configuration");
434
    }
435
 
436
    /**
437
     * Fusionne les office:automatic-styles, on préfixe tout.
438
     *
439
     * @param doc le document à fusionner avec celui-ci.
440
     * @param ref whether to prefix hrefs.
441
     * @throws JDOMException
442
     */
443
    private void mergeAutoStyles(ODXMLDocument doc, boolean ref) throws JDOMException {
444
        final List<Element> addedStyles = this.prefixAndAddAutoStyles(doc);
445
        for (final Element addedStyle : addedStyles) {
446
            this.prefix(addedStyle, ref);
447
        }
448
    }
449
 
450
    /**
451
     * Fusionne les office:master-styles. On ne préfixe jamais, on ajoute seulement si l'attribut
452
     * style:name est différent.
453
     *
454
     * @param doc le document à fusionner avec celui-ci.
455
     * @param ref whether to prefix hrefs.
456
     * @throws JDOMException if an error occurs.
457
     */
458
    private void mergeMasterStyles(ODXMLDocument doc, boolean ref) throws JDOMException {
459
        // est référencé dans les styles avec "style:master-page-name"
460
        this.mergeUnique(doc, "master-styles", "style:master-page", ref ? this.prefixTransf : this.prefixTransfNoRef);
461
    }
462
 
463
    /**
464
     * Fusionne les corps.
465
     *
466
     * @param doc le document à fusionner avec celui-ci.
467
     * @throws JDOMException
468
     */
469
    private void mergeBody(Element where, int index, ODSingleXMLDocument doc) throws JDOMException {
470
        // copy forms from doc to this
471
        final String formsName = "forms";
472
        final Namespace formsNS = getVersion().getOFFICE();
473
        final String bodyPath = this.getContentTypeVersioned().getBodyPath();
474
        this.add(new IFactory<Element>() {
475
            @Override
476
            public Element createChecked() {
477
                final Element ourForms = getBody().getChild(formsName, formsNS);
478
                if (ourForms != null) {
479
                    return ourForms;
480
                } else {
481
                    final Element res = new Element(formsName, formsNS);
482
                    // forms should be the first child of the body
483
                    getBody().addContent(0, res);
484
                    return res;
485
                }
486
            }
487
        }, -1, doc, bodyPath + "/" + formsNS.getPrefix() + ":" + formsName, this.prefixTransf);
488
        this.add(where, index, doc, bodyPath, new ElementTransformer() {
489
            public Element transform(Element elem) throws JDOMException {
490
                // ATTN n'ajoute pas sequence-decls
491
                // forms already added above
492
                if (elem.getName().equals("sequence-decls") || (elem.getName().equals(formsName) && elem.getNamespace().equals(formsNS)))
493
                    return null;
494
 
495
                if (elem.getName().equals("user-field-decls")) {
496
                    // user fields are global to a document, they do not vary across it.
497
                    // hence they are initialized at declaration
498
                    // we should assure that there's no 2 declaration with the same name
499
                    detachDuplicate(elem);
500
                }
501
 
502
                if (elem.getName().equals("variable-decls")) {
503
                    // variables are not initialized at declaration
504
                    // we should still assure that there's no 2 declaration with the same name
505
                    detachDuplicate(elem);
506
                }
507
 
508
                // par défaut
509
                return ODSingleXMLDocument.this.prefixTransf.transform(elem);
510
            }
511
        });
512
    }
513
 
514
    /**
515
     * Detach the children of elem whose names already exist in the body.
516
     *
517
     * @param elem the elem to be trimmed.
518
     * @throws JDOMException if an error occurs.
519
     */
520
    protected final void detachDuplicate(Element elem) throws JDOMException {
521
        final String singularName = elem.getName().substring(0, elem.getName().length() - 1);
522
        final List thisNames = getXPath("./text:" + singularName + "s/text:" + singularName + "/@text:name").selectNodes(getChild("body"));
523
        CollectionUtils.transform(thisNames, new Transformer() {
524
            public Object transform(Object obj) {
525
                return ((Attribute) obj).getValue();
526
            }
527
        });
528
 
529
        final Iterator iter = elem.getChildren().iterator();
530
        while (iter.hasNext()) {
531
            final Element decl = (Element) iter.next();
532
            if (thisNames.contains(decl.getAttributeValue("name", getVersion().getTEXT()))) {
533
                // on retire les déjà existant
534
                iter.remove();
535
            }
536
        }
537
    }
538
 
539
    // *** Utils
540
 
541
    public final Element getBody() {
542
        return this.getContentTypeVersioned().getBody(getDocument());
543
    }
544
 
545
    private ContentTypeVersioned getContentTypeVersioned() {
61 ilm 546
        return getPackage().getContentType();
17 ilm 547
    }
548
 
549
    /**
550
     * Préfixe les attributs en ayant besoin.
551
     *
552
     * @param elem l'élément à préfixer.
553
     * @param references whether to prefix hrefs.
554
     * @throws JDOMException if an error occurs.
555
     */
556
    void prefix(Element elem, boolean references) throws JDOMException {
557
        Iterator attrs = this.getXPath(".//@text:style-name | .//@table:style-name | .//@draw:style-name | .//@style:data-style-name").selectNodes(elem).iterator();
558
        while (attrs.hasNext()) {
559
            Attribute attr = (Attribute) attrs.next();
560
            // text:list/@text:style-name references text:list-style
561
            if (!this.listStylesNames.contains(attr.getValue()) && !this.stylesNames.contains(attr.getValue())) {
562
                attr.setValue(this.prefix(attr.getValue()));
563
            }
564
        }
565
 
566
        attrs = this.getXPath(".//@style:list-style-name").selectNodes(elem).iterator();
567
        while (attrs.hasNext()) {
568
            Attribute attr = (Attribute) attrs.next();
569
            if (!this.listStylesNames.contains(attr.getValue())) {
570
                attr.setValue(this.prefix(attr.getValue()));
571
            }
572
        }
573
 
574
        attrs = this.getXPath(".//@style:page-master-name | .//@style:page-layout-name | .//@text:name | .//@form:name | .//@form:property-name").selectNodes(elem).iterator();
575
        while (attrs.hasNext()) {
576
            final Attribute attr = (Attribute) attrs.next();
577
            final String parentName = attr.getParent().getName();
578
            if (!DONT_PREFIX.contains(parentName))
579
                attr.setValue(this.prefix(attr.getValue()));
580
        }
581
 
582
        // prefix references
583
        if (references) {
584
            attrs = this.getXPath(".//@xlink:href[../@xlink:show='embed']").selectNodes(elem).iterator();
585
            while (attrs.hasNext()) {
586
                final Attribute attr = (Attribute) attrs.next();
587
                final String prefixedPath = this.prefixPath(attr.getValue());
588
                if (prefixedPath != null)
589
                    attr.setValue(prefixedPath);
590
            }
591
        }
592
    }
593
 
594
    /**
595
     * Prefix a path.
596
     *
597
     * @param href a path inside the pkg, eg "./Object 1/content.xml".
598
     * @return the prefixed path or <code>null</code> if href is external, eg "./3_Object
599
     *         1/content.xml".
600
     */
601
    private String prefixPath(final String href) {
602
        if (this.getVersion().equals(XMLVersion.OOo)) {
603
            // in OOo 1.x inPKG is denoted by a #
604
            final boolean sharp = href.startsWith("#");
605
            if (sharp)
606
                // eg #Pictures/100000000000006C000000ABCC02339E.png
607
                return "#" + this.prefix(href.substring(1));
608
            else
609
                // eg ../../../../Program%20Files/OpenOffice.org1.1.5/share/gallery/apples.gif
610
                return null;
611
        } else {
612
            URI uri;
613
            try {
614
                uri = new URI(href);
615
            } catch (URISyntaxException e) {
616
                // OO doesn't escape characters for files
617
                uri = null;
618
            }
619
            // section 17.5
620
            final boolean inPKGFile = uri == null || uri.getScheme() == null && uri.getAuthority() == null && uri.getPath().charAt(0) != '/';
621
            if (inPKGFile) {
622
                final String dotSlash = "./";
623
                if (href.startsWith(dotSlash))
624
                    return dotSlash + this.prefix(href.substring(dotSlash.length()));
625
                else
626
                    return this.prefix(href);
627
            } else
628
                return null;
629
        }
630
    }
631
 
632
    private String prefix(String value) {
633
        return "_" + this.numero + value;
634
    }
635
 
636
    private final ElementTransformer prefixTransf = new ElementTransformer() {
637
        public Element transform(Element elem) throws JDOMException {
638
            ODSingleXMLDocument.this.prefix(elem, true);
639
            return elem;
640
        }
641
    };
642
 
643
    private final ElementTransformer prefixTransfNoRef = new ElementTransformer() {
644
        public Element transform(Element elem) throws JDOMException {
645
            ODSingleXMLDocument.this.prefix(elem, false);
646
            return elem;
647
        }
648
    };
649
 
650
    /**
651
     * Ajoute dans ce document seulement les éléments de doc correspondant au XPath spécifié et dont
652
     * la valeur de l'attribut style:name n'existe pas déjà.
653
     *
654
     * @param doc le document à fusionner avec celui-ci.
655
     * @param topElem eg "office:font-decls".
656
     * @param elemToMerge les éléments à fusionner (par rapport à topElem), eg "style:font-decl".
657
     * @return les noms des éléments ajoutés.
658
     * @throws JDOMException
659
     * @see #mergeUnique(ODSingleXMLDocument, String, String, ElementTransformer)
660
     */
661
    private List<String> mergeUnique(ODXMLDocument doc, String topElem, String elemToMerge) throws JDOMException {
662
        return this.mergeUnique(doc, topElem, elemToMerge, NOP_ElementTransformer);
663
    }
664
 
665
    /**
666
     * Ajoute dans ce document seulement les éléments de doc correspondant au XPath spécifié et dont
667
     * la valeur de l'attribut style:name n'existe pas déjà. En conséquence n'ajoute que les
668
     * éléments possédant un attribut style:name.
669
     *
670
     * @param doc le document à fusionner avec celui-ci.
671
     * @param topElem eg "office:font-decls".
672
     * @param elemToMerge les éléments à fusionner (par rapport à topElem), eg "style:font-decl".
673
     * @param addTransf la transformation à appliquer avant d'ajouter.
674
     * @return les noms des éléments ajoutés.
675
     * @throws JDOMException
676
     */
677
    private List<String> mergeUnique(ODXMLDocument doc, String topElem, String elemToMerge, ElementTransformer addTransf) throws JDOMException {
678
        return this.mergeUnique(doc, topElem, elemToMerge, "style:name", addTransf);
679
    }
680
 
681
    private List<String> mergeUnique(ODXMLDocument doc, String topElem, String elemToMerge, String attrFQName, ElementTransformer addTransf) throws JDOMException {
682
        List<String> added = new ArrayList<String>();
683
        Element thisParent = this.getChild(topElem, true);
684
 
685
        XPath xp = this.getXPath("./" + elemToMerge + "/@" + attrFQName);
686
 
687
        // les styles de ce document
688
        List thisElemNames = xp.selectNodes(thisParent);
689
        // on transforme la liste d'attributs en liste de String
690
        CollectionUtils.transform(thisElemNames, new Transformer() {
691
            public Object transform(Object obj) {
692
                return ((Attribute) obj).getValue();
693
            }
694
        });
695
 
696
        // pour chaque style de l'autre document
697
        Iterator otherElemNames = xp.selectNodes(doc.getChild(topElem)).iterator();
698
        while (otherElemNames.hasNext()) {
699
            Attribute attr = (Attribute) otherElemNames.next();
700
            // on l'ajoute si non déjà dedans
701
            if (!thisElemNames.contains(attr.getValue())) {
702
                thisParent.addContent(addTransf.transform((Element) attr.getParent().clone()));
703
                added.add(attr.getValue());
704
            }
705
        }
706
 
707
        return added;
708
    }
709
 
710
    /**
711
     * Ajoute l'élément elemName de doc, s'il n'est pas dans ce document.
712
     *
713
     * @param doc le document à fusionner avec celui-ci.
714
     * @param elemName l'élément à ajouter, eg "outline-style".
715
     * @throws JDOMException if elemName is not valid.
716
     */
717
    private void addStylesIfNotPresent(ODXMLDocument doc, String elemName) throws JDOMException {
718
        this.addIfNotPresent(doc, "./office:styles/text:" + elemName);
719
    }
720
 
721
    /**
722
     * Prefixe les fils de auto-styles possédant un attribut "name" avant de les ajouter.
723
     *
724
     * @param doc le document à fusionner avec celui-ci.
725
     * @return les élément ayant été ajoutés.
726
     * @throws JDOMException
727
     */
728
    private List<Element> prefixAndAddAutoStyles(ODXMLDocument doc) throws JDOMException {
729
        final List<Element> result = new ArrayList<Element>(128);
730
        final List otherNames = this.getXPath("./*/@style:name").selectNodes(doc.getChild("automatic-styles"));
731
        Iterator iter = otherNames.iterator();
732
        while (iter.hasNext()) {
733
            Attribute attr = (Attribute) iter.next();
734
            Element parent = (Element) attr.getParent().clone();
735
            parent.setAttribute("name", this.prefix(attr.getValue()), this.getVersion().getSTYLE());
736
            this.getChild("automatic-styles").addContent(parent);
737
            result.add(parent);
738
        }
739
        return result;
740
    }
741
 
742
    /**
743
     * Return <code>true</code> if this document was split.
744
     *
745
     * @return <code>true</code> if this has no package anymore.
746
     * @see ODPackage#split()
747
     */
748
    public final boolean isDead() {
749
        return this.getPackage() == null;
750
    }
751
 
752
    final Map<RootElement, Document> split() {
753
        final Map<RootElement, Document> res = new HashMap<RootElement, Document>();
754
        final XMLVersion version = getVersion();
755
        final Element root = this.getDocument().getRootElement();
25 ilm 756
        final XMLFormatVersion officeVersion = getFormatVersion();
17 ilm 757
 
758
        // meta
759
        {
760
            final Element thisMeta = root.getChild("meta", version.getOFFICE());
761
            if (thisMeta != null) {
25 ilm 762
                final Document meta = createDocument(res, RootElement.META, officeVersion);
17 ilm 763
                meta.getRootElement().addContent(thisMeta.detach());
764
            }
765
        }
766
        // settings
767
        {
768
            final Element thisSettings = root.getChild("settings", version.getOFFICE());
769
            if (thisSettings != null) {
25 ilm 770
                final Document settings = createDocument(res, RootElement.SETTINGS, officeVersion);
17 ilm 771
                settings.getRootElement().addContent(thisSettings.detach());
772
            }
773
        }
774
        // styles
775
        // we must move office:styles, office:master-styles and referenced office:automatic-styles
776
        {
25 ilm 777
            final Document styles = createDocument(res, RootElement.STYLES, officeVersion);
17 ilm 778
            // don't bother finding out which font is used where since there isn't that many of them
779
            styles.getRootElement().addContent((Element) root.getChild(getFontDecls()[0], version.getOFFICE()).clone());
780
            // extract common styles
781
            styles.getRootElement().addContent(root.getChild("styles", version.getOFFICE()).detach());
782
            // only automatic styles used in the styles themselves.
783
            final Element contentAutoStyles = root.getChild("automatic-styles", version.getOFFICE());
784
            final Element stylesAutoStyles = new Element(contentAutoStyles.getName(), contentAutoStyles.getNamespace());
785
            final Element masterStyles = root.getChild("master-styles", version.getOFFICE());
786
 
787
            // style elements referenced, e.g. <style:page-layout style:name="pm1">
788
            final Set<Element> referenced = new HashSet<Element>();
789
 
790
            final SimpleXMLPath<Attribute> descAttrs = SimpleXMLPath.create(Step.createElementStep(Axis.descendantOrSelf, null), Step.createAttributeStep(null, null));
791
            for (final Attribute attr : descAttrs.selectNodes(masterStyles)) {
792
                final Element referencedStyleElement = Style.getReferencedStyleElement(this.pkg, attr);
793
                if (referencedStyleElement != null)
794
                    referenced.add(referencedStyleElement);
795
            }
796
            for (final Element r : referenced) {
797
                // since we already removed common styles
798
                assert r.getParentElement() == contentAutoStyles;
799
                stylesAutoStyles.addContent(r.detach());
800
            }
801
 
802
            styles.getRootElement().addContent(stylesAutoStyles);
803
            styles.getRootElement().addContent(masterStyles.detach());
804
        }
805
        // content
806
        {
61 ilm 807
            // store before emptying package
808
            final ContentTypeVersioned contentTypeVersioned = getContentTypeVersioned();
809
            // needed since the content will be emptied (which can cause methods of ODPackage to
810
            // fail, e.g. setTypeAndVersion())
811
            this.pkg.rmFile(RootElement.CONTENT.getZipEntry());
17 ilm 812
            this.pkg = null;
25 ilm 813
            final Document content = createDocument(res, RootElement.CONTENT, officeVersion);
61 ilm 814
            contentTypeVersioned.setType(content);
17 ilm 815
            content.getRootElement().addContent(root.removeContent());
816
        }
817
        return res;
818
    }
819
 
25 ilm 820
    private Document createDocument(final Map<RootElement, Document> res, RootElement rootElement, final XMLFormatVersion version) {
821
        final Document doc = rootElement.createDocument(version);
17 ilm 822
        copyNS(this.getDocument(), doc);
823
        res.put(rootElement, doc);
824
        return doc;
825
    }
826
 
827
    /**
828
     * Saves this OO document to a file.
829
     *
830
     * @param f the file where this document will be saved, without extension, eg "dir/myfile".
831
     * @return the actual file where it has been saved (with extension), eg "dir/myfile.odt".
832
     * @throws IOException if an error occurs.
833
     */
61 ilm 834
    public File saveToPackageAs(File f) throws IOException {
17 ilm 835
        return this.pkg.saveAs(f);
836
    }
837
 
61 ilm 838
    public File save() throws IOException {
839
        return this.saveAs(this.getPackage().getFile());
840
    }
841
 
842
    public File saveAs(File fNoExt) throws IOException {
843
        final Document doc = (Document) getDocument().clone();
844
        for (final Attribute hrefAttr : ALL_HREF_ATTRIBUTES.selectNodes(doc.getRootElement())) {
845
            final String href = hrefAttr.getValue();
846
            if (href.startsWith("../")) {
847
                // update href
848
                hrefAttr.setValue(href.substring(3));
849
            } else if (!URI.create(href).isAbsolute()) {
850
                // encode binaries
851
                final Element hrefParent = hrefAttr.getParent();
852
                if (!BINARY_DATA_PARENTS.contains(hrefParent.getQualifiedName()))
853
                    throw new IllegalStateException("Cannot convert to binary data element : " + hrefParent);
854
                final Element binaryData = new Element("binary-data", getPackage().getVersion().getOFFICE());
855
 
856
                binaryData.setText(Base64.encodeBytes(getPackage().getBinaryFile(href)));
857
                hrefParent.addContent(binaryData);
858
                // If this element is present, an xlink:href attribute in its parent element
859
                // shall be ignored. But LO doesn't respect that
860
                hrefAttr.detach();
861
            }
862
        }
863
 
864
        final File f = this.getPackage().getContentType().addExt(fNoExt, true);
865
        FileUtils.write(ODPackage.createOutputter().outputString(doc), f);
866
        return f;
867
    }
868
 
17 ilm 869
    private Element getPageBreak() {
870
        if (this.pageBreak == null) {
871
            final String styleName = "PageBreak";
872
            try {
873
                final XPath xp = this.getXPath("./style:style[@style:name='" + styleName + "']");
874
                final Element styles = this.getChild("styles", true);
875
                if (xp.selectSingleNode(styles) == null) {
876
                    final Element pageBreakStyle = new Element("style", this.getVersion().getSTYLE());
877
                    pageBreakStyle.setAttribute("name", styleName, this.getVersion().getSTYLE());
878
                    pageBreakStyle.setAttribute("family", "paragraph", this.getVersion().getSTYLE());
879
                    pageBreakStyle.setContent(getPProps().setAttribute("break-after", "page", this.getVersion().getNS("fo")));
880
                    // <element name="office:styles"> <interleave>...
881
                    // so just append the new style
882
                    styles.addContent(pageBreakStyle);
883
                }
884
            } catch (JDOMException e) {
885
                // static path, shouldn't happen
886
                throw new IllegalStateException("pb while searching for " + styleName, e);
887
            }
888
            this.pageBreak = new Element("p", this.getVersion().getTEXT()).setAttribute("style-name", styleName, this.getVersion().getTEXT());
889
        }
890
        return (Element) this.pageBreak.clone();
891
    }
892
 
893
    private final Element getPProps() {
19 ilm 894
        return this.getXML().createFormattingProperties("paragraph");
17 ilm 895
    }
896
}