OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

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