OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 20 | Rev 57 | 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 static org.openconcerto.openoffice.ODPackage.RootElement.META;
18
import static org.openconcerto.openoffice.ODPackage.RootElement.STYLES;
25 ilm 19
import org.openconcerto.openoffice.spreadsheet.SpreadSheet;
17 ilm 20
import org.openconcerto.openoffice.text.ParagraphStyle;
25 ilm 21
import org.openconcerto.openoffice.text.TextDocument;
22
import org.openconcerto.utils.CollectionMap;
17 ilm 23
import org.openconcerto.utils.CopyUtils;
25 ilm 24
import org.openconcerto.utils.ExceptionUtils;
17 ilm 25
import org.openconcerto.utils.FileUtils;
26
import org.openconcerto.utils.StreamUtils;
27
import org.openconcerto.utils.StringInputStream;
25 ilm 28
import org.openconcerto.utils.Tuple3;
17 ilm 29
import org.openconcerto.utils.Zip;
30
import org.openconcerto.utils.ZippedFilesProcessor;
25 ilm 31
import org.openconcerto.utils.cc.ITransformer;
32
import org.openconcerto.xml.JDOMUtils;
19 ilm 33
import org.openconcerto.xml.Validator;
17 ilm 34
 
35
import java.io.BufferedInputStream;
36
import java.io.BufferedOutputStream;
37
import java.io.ByteArrayInputStream;
38
import java.io.ByteArrayOutputStream;
39
import java.io.File;
40
import java.io.FileInputStream;
41
import java.io.FileOutputStream;
42
import java.io.IOException;
43
import java.io.InputStream;
44
import java.io.OutputStream;
45
import java.nio.charset.Charset;
25 ilm 46
import java.util.ArrayList;
47
import java.util.Arrays;
17 ilm 48
import java.util.EnumSet;
49
import java.util.HashMap;
50
import java.util.HashSet;
25 ilm 51
import java.util.Iterator;
52
import java.util.List;
17 ilm 53
import java.util.Map;
54
import java.util.Map.Entry;
55
import java.util.Set;
56
import java.util.zip.ZipEntry;
57
 
25 ilm 58
import org.jdom.Attribute;
17 ilm 59
import org.jdom.DocType;
60
import org.jdom.Document;
61
import org.jdom.Element;
62
import org.jdom.JDOMException;
63
import org.jdom.Namespace;
64
import org.jdom.output.Format;
65
import org.jdom.output.XMLOutputter;
66
 
67
/**
68
 * An OpenDocument package, ie a zip containing XML documents and their associated files.
69
 *
70
 * @author ILM Informatique 2 août 2004
71
 */
72
public class ODPackage {
73
 
74
    // use raw format, otherwise spaces are added to every spreadsheet cell
75
    private static final XMLOutputter OUTPUTTER = new XMLOutputter(Format.getRawFormat());
25 ilm 76
    static final String MIMETYPE_ENTRY = "mimetype";
17 ilm 77
    /** Normally mimetype contains only ASCII characters */
78
    static final Charset MIMETYPE_ENC = Charset.forName("UTF-8");
79
 
80
    /**
81
     * Root element of an OpenDocument document. See section 22.2.1 of v1.2-part1-cd04.
82
     *
83
     * @author Sylvain CUAZ
84
     */
85
    public static enum RootElement {
86
        /** Contains the entire document, see 3.1.2 of OpenDocument-v1.2-part1-cd04 */
87
        SINGLE_CONTENT("office", "document", null),
88
        /** Document content and automatic styles used in the content, see 3.1.3.2 */
89
        CONTENT("office", "document-content", "content.xml"),
90
        // TODO uncomment and create ContentTypeVersioned for .odf and .otf, see 22.2.9 Conforming
91
        // OpenDocument Formula Document
92
        // MATH("math", "math", "content.xml"),
93
        /** Styles used in document content and automatic styles used in styles, see 3.1.3.3 */
94
        STYLES("office", "document-styles", "styles.xml"),
95
        /** Document metadata elements, see 3.1.3.4 */
96
        META("office", "document-meta", "meta.xml"),
97
        /** Implementation-specific settings, see 3.1.3.5 */
98
        SETTINGS("office", "document-settings", "settings.xml");
99
 
100
        public final static EnumSet<RootElement> getPackageElements() {
101
            return EnumSet.of(CONTENT, STYLES, META, SETTINGS);
102
        }
103
 
25 ilm 104
        public final static RootElement fromDocument(final Document doc) {
105
            return fromElementName(doc.getRootElement().getName());
106
        }
107
 
17 ilm 108
        public final static RootElement fromElementName(final String name) {
109
            for (final RootElement e : values()) {
110
                if (e.getElementName().equals(name))
111
                    return e;
112
            }
113
            return null;
114
        }
115
 
116
        static final Document createSingle(final Document from) {
25 ilm 117
            return SINGLE_CONTENT.createDocument(XMLFormatVersion.get(from));
17 ilm 118
        }
119
 
120
        private final String nsPrefix;
121
        private final String name;
122
        private final String zipEntry;
123
 
124
        private RootElement(String prefix, String rootName, String zipEntry) {
125
            this.nsPrefix = prefix;
126
            this.name = rootName;
127
            this.zipEntry = zipEntry;
128
        }
129
 
130
        public final String getElementNSPrefix() {
131
            return this.nsPrefix;
132
        }
133
 
134
        public final String getElementName() {
135
            return this.name;
136
        }
137
 
25 ilm 138
        public final Document createDocument(final XMLFormatVersion fv) {
139
            final XMLVersion version = fv.getXMLVersion();
17 ilm 140
            final Element root = new Element(getElementName(), version.getNS(getElementNSPrefix()));
141
            // 19.388 office:version identifies the version of ODF specification
25 ilm 142
            if (fv.getOfficeVersion() != null)
143
                root.setAttribute("version", fv.getOfficeVersion(), version.getOFFICE());
17 ilm 144
            // avoid declaring namespaces in each child
145
            for (final Namespace ns : version.getALL())
146
                root.addNamespaceDeclaration(ns);
147
 
25 ilm 148
            return new Document(root, createDocType(version));
149
        }
150
 
151
        public final DocType createDocType(final XMLVersion version) {
17 ilm 152
            // OpenDocument use relaxNG
153
            if (version == XMLVersion.OOo)
25 ilm 154
                return new DocType(getElementNSPrefix() + ":" + getElementName(), "-//OpenOffice.org//DTD OfficeDocument 1.0//EN", "office.dtd");
155
            else
156
                return null;
17 ilm 157
        }
158
 
159
        /**
160
         * The name of the zip entry in the package.
161
         *
162
         * @return the path of the file, <code>null</code> if this element shouldn't be in a
163
         *         package.
164
         */
165
        public final String getZipEntry() {
166
            return this.zipEntry;
167
        }
168
    }
169
 
170
    private static final Set<String> subdocNames;
171
    static {
172
        subdocNames = new HashSet<String>();
173
        for (final RootElement r : RootElement.getPackageElements())
174
            if (r.getZipEntry() != null)
175
                subdocNames.add(r.getZipEntry());
176
    }
177
 
178
    /**
179
     * Whether the passed entry is specific to a package.
180
     *
181
     * @param name a entry name, eg "mimetype"
182
     * @return <code>true</code> if <code>name</code> is a standard file, eg <code>true</code>.
183
     */
184
    public static final boolean isStandardFile(final String name) {
25 ilm 185
        return name.equals(MIMETYPE_ENTRY) || subdocNames.contains(name) || name.startsWith("Thumbnails") || name.startsWith("META-INF") || name.startsWith("Configurations");
17 ilm 186
    }
187
 
25 ilm 188
    /**
189
     * Create a package from a collection of sub-documents.
190
     *
191
     * @param content the content.
192
     * @param style the styles, can be <code>null</code>.
193
     * @return a package containing the XML documents.
194
     */
195
    public static ODPackage createFromDocuments(Document content, Document style) {
196
        return createFromDocuments(null, content, style, null, null);
197
    }
198
 
199
    public static ODPackage createFromDocuments(final ContentTypeVersioned type, Document content, Document style, Document meta, Document settings) {
200
        final ODPackage pkg = new ODPackage();
201
        if (type != null)
202
            pkg.setContentType(type);
203
        pkg.putFile(RootElement.CONTENT.getZipEntry(), content);
204
        pkg.putFile(RootElement.STYLES.getZipEntry(), style);
205
        pkg.putFile(RootElement.META.getZipEntry(), meta);
206
        pkg.putFile(RootElement.SETTINGS.getZipEntry(), settings);
207
        return pkg;
208
    }
209
 
210
    static private XMLVersion getVersion(final XMLFormatVersion fv, final ContentTypeVersioned ct) {
211
        final XMLVersion v;
212
        if (ct == null && fv == null)
213
            v = null;
214
        else if (ct != null)
215
            v = ct.getVersion();
216
        else
217
            v = fv.getXMLVersion();
218
        assert fv == null || ct == null || fv.getXMLVersion() == ct.getVersion();
219
        return v;
220
    }
221
 
222
    static private <T> void checkVersion(final Class<T> clazz, final String s, final T actual, final T required) {
223
        if (actual != null && required != null) {
224
            final boolean ok;
225
            if (actual instanceof ContentTypeVersioned) {
226
                // we can change our template status since it doesn't affect our content
227
                ok = ((ContentTypeVersioned) actual).getNonTemplate().equals(((ContentTypeVersioned) required).getNonTemplate());
228
            } else {
229
                ok = actual.equals(required);
230
            }
231
            if (!ok)
232
                throw new IllegalArgumentException("Cannot change " + s + " from " + required + " to " + actual);
233
        }
234
    }
235
 
17 ilm 236
    private final Map<String, ODPackageEntry> files;
237
    private ContentTypeVersioned type;
25 ilm 238
    private XMLFormatVersion version;
17 ilm 239
    private File file;
25 ilm 240
    private ODDocument doc;
17 ilm 241
 
242
    public ODPackage() {
243
        this.files = new HashMap<String, ODPackageEntry>();
244
        this.type = null;
25 ilm 245
        this.version = null;
17 ilm 246
        this.file = null;
25 ilm 247
        this.doc = null;
17 ilm 248
    }
249
 
250
    public ODPackage(InputStream ins) throws IOException {
251
        this();
252
 
253
        final ByteArrayOutputStream out = new ByteArrayOutputStream(4096);
254
        new ZippedFilesProcessor() {
255
            @Override
256
            protected void processEntry(ZipEntry entry, InputStream in) throws IOException {
257
                final String name = entry.getName();
258
                final Object res;
259
                if (subdocNames.contains(name)) {
260
                    try {
25 ilm 261
                        res = OOUtils.getBuilder().build(in);
17 ilm 262
                    } catch (JDOMException e) {
263
                        // always correct
264
                        throw new IllegalStateException("parse error", e);
265
                    }
266
                } else {
267
                    out.reset();
268
                    StreamUtils.copy(in, out);
269
                    res = out.toByteArray();
270
                }
271
                // we don't know yet the types
272
                putFile(name, res, null, entry.getMethod() == ZipEntry.DEFLATED);
273
            }
274
        }.process(ins);
275
        // fill in the missing types from the manifest, if any
276
        final ODPackageEntry me = this.files.remove(Manifest.ENTRY_NAME);
277
        if (me != null) {
278
            final byte[] m = (byte[]) me.getData();
279
            try {
280
                final Map<String, String> manifestEntries = Manifest.parse(new ByteArrayInputStream(m));
281
                for (final Map.Entry<String, String> e : manifestEntries.entrySet()) {
282
                    final String path = e.getKey();
283
                    final ODPackageEntry entry = this.files.get(path);
284
                    // eg directory
285
                    if (entry == null)
286
                        this.files.put(path, new ODPackageEntry(path, e.getValue(), null));
287
                    else
288
                        entry.setType(e.getValue());
289
                }
290
            } catch (JDOMException e) {
291
                throw new IllegalArgumentException("bad manifest " + new String(m), e);
292
            }
293
        }
294
    }
295
 
296
    public ODPackage(File f) throws IOException {
297
        this(new BufferedInputStream(new FileInputStream(f), 512 * 1024));
298
        this.file = f;
299
    }
300
 
301
    public ODPackage(ODPackage o) {
302
        this();
303
        // ATTN this works because, all files are read upfront
304
        for (final String name : o.getEntries()) {
305
            final ODPackageEntry entry = o.getEntry(name);
306
            final Object data = entry.getData();
307
            final Object myData;
308
            if (data instanceof byte[])
309
                // assume byte[] are immutable
310
                myData = data;
311
            else if (data instanceof ODSingleXMLDocument) {
312
                myData = new ODSingleXMLDocument((ODSingleXMLDocument) data, this);
313
            } else {
314
                myData = CopyUtils.copy(data);
315
            }
316
            this.putFile(name, myData, entry.getType(), entry.isCompressed());
317
        }
318
        this.type = o.type;
25 ilm 319
        this.version = o.version;
17 ilm 320
        this.file = o.file;
25 ilm 321
        this.doc = null;
17 ilm 322
    }
323
 
324
    public final File getFile() {
325
        return this.file;
326
    }
327
 
328
    public final void setFile(File f) {
329
        this.file = this.addExt(f);
330
    }
331
 
332
    private final File addExt(File f) {
333
        final String ext = '.' + this.getContentType().getExtension();
334
        if (!f.getName().endsWith(ext))
335
            f = new File(f.getParentFile(), f.getName() + ext);
336
        return f;
337
    }
338
 
339
    /**
340
     * The version of this package, <code>null</code> if it cannot be found (eg this package is
341
     * empty, or contains no xml).
342
     *
343
     * @return the version of this package, can be <code>null</code>.
344
     */
345
    public final XMLVersion getVersion() {
25 ilm 346
        return getVersion(this.version, this.type);
19 ilm 347
    }
348
 
349
    public final XMLFormatVersion getFormatVersion() {
25 ilm 350
        return this.version;
17 ilm 351
    }
352
 
353
    /**
354
     * The type of this package, <code>null</code> if it cannot be found (eg this package is empty).
355
     *
356
     * @return the type of this package, can be <code>null</code>.
357
     */
358
    public final ContentTypeVersioned getContentType() {
25 ilm 359
        return this.type;
360
    }
361
 
362
    public final void setContentType(final ContentTypeVersioned newType) {
363
        this.putFile(MIMETYPE_ENTRY, newType.getMimeType().getBytes(MIMETYPE_ENC));
364
    }
365
 
366
    private void updateTypeAndVersion(final String entry, ODXMLDocument xml) {
367
        this.setTypeAndVersion(entry.equals(CONTENT.getZipEntry()) ? ContentTypeVersioned.fromContent(xml) : null, xml.getFormatVersion(), entry);
368
    }
369
 
370
    private void updateTypeAndVersion(byte[] mimetype) {
371
        this.setTypeAndVersion(ContentTypeVersioned.fromMime(mimetype), null, MIMETYPE_ENTRY);
372
    }
373
 
374
    private final void setTypeAndVersion(final ContentTypeVersioned ct, final XMLFormatVersion fv, final String entry) {
375
        final Tuple3<XMLVersion, ContentTypeVersioned, XMLFormatVersion> requiredByPkg = this.getRequired(entry);
376
        if (requiredByPkg != null) {
377
            checkVersion(XMLVersion.class, "version", getVersion(fv, ct), requiredByPkg.get0());
378
            checkVersion(ContentTypeVersioned.class, "type", ct, requiredByPkg.get1());
379
            checkVersion(XMLFormatVersion.class, "format version", fv, requiredByPkg.get2());
380
        }
381
 
382
        // since we're adding "entry" never set attributes to null
383
        if (fv != null && !fv.equals(this.version))
384
            this.version = fv;
385
        // don't let non-template from content overwrite the correct one
386
        if (ct != null && !ct.equals(this.type) && (this.type == null || entry.equals(MIMETYPE_ENTRY)))
387
            this.type = ct;
388
    }
389
 
390
    // find the versions required by the package without the passed entry
391
    private final Tuple3<XMLVersion, ContentTypeVersioned, XMLFormatVersion> getRequired(final String entryToIgnore) {
392
        if (this.files.size() == 0 || (this.files.size() == 1 && this.files.containsKey(entryToIgnore)))
393
            return null;
394
 
395
        final byte[] mimetype;
396
        if (this.files.containsKey(MIMETYPE_ENTRY) && !MIMETYPE_ENTRY.equals(entryToIgnore)) {
397
            mimetype = this.getBinaryFile(MIMETYPE_ENTRY);
398
        } else {
399
            mimetype = null;
400
        }
401
        XMLFormatVersion fv = null;
402
        final Map<String, Object> versionFiles = new HashMap<String, Object>();
403
        for (final String e : subdocNames) {
404
            if (this.files.containsKey(e) && !e.equals(entryToIgnore)) {
405
                final ODXMLDocument xmlFile = this.getXMLFile(e);
406
                versionFiles.put(e, xmlFile);
407
                if (fv == null)
408
                    fv = xmlFile.getFormatVersion();
409
                else
410
                    assert fv.equals(xmlFile.getFormatVersion()) : "Incoherence";
17 ilm 411
            }
412
        }
25 ilm 413
        final ODXMLDocument content = (ODXMLDocument) versionFiles.get(CONTENT.getZipEntry());
414
 
415
        final ContentTypeVersioned ct;
416
        if (mimetype != null)
417
            ct = ContentTypeVersioned.fromMime(mimetype);
418
        else if (content != null)
419
            ct = ContentTypeVersioned.fromContent(content);
420
        else
421
            ct = null;
422
 
423
        return Tuple3.create(getVersion(fv, ct), ct, fv);
17 ilm 424
    }
425
 
426
    public final String getMimeType() {
427
        return this.getContentType().getMimeType();
428
    }
429
 
25 ilm 430
    public final boolean isTemplate() {
431
        return this.getContentType().isTemplate();
432
    }
433
 
434
    public final void setTemplate(boolean b) {
435
        if (this.type == null)
436
            throw new IllegalStateException("No type");
437
        final ContentTypeVersioned newType = b ? this.type.getTemplate() : this.type.getNonTemplate();
438
        if (newType == null)
439
            throw new IllegalStateException("Missing " + (b ? "" : "non-") + "template for " + this.type);
440
        this.setContentType(newType);
441
    }
442
 
17 ilm 443
    /**
19 ilm 444
     * Call {@link Validator#isValid()} on each XML subdocuments.
17 ilm 445
     *
19 ilm 446
     * @return all problems indexed by subdocuments names, i.e. empty if all OK, <code>null</code>
447
     *         if validation couldn't occur.
17 ilm 448
     */
449
    public final Map<String, String> validateSubDocuments() {
25 ilm 450
        return this.validateSubDocuments(true);
451
    }
452
 
453
    public final Map<String, String> validateSubDocuments(final boolean allowChangeToValidate) {
19 ilm 454
        final OOXML ooxml = this.getFormatVersion().getXML();
455
        if (!ooxml.canValidate())
456
            return null;
17 ilm 457
        final Map<String, String> res = new HashMap<String, String>();
458
        for (final String s : subdocNames) {
25 ilm 459
            final Document doc = this.getDocument(s);
460
            if (doc != null) {
461
                if (allowChangeToValidate) {
462
                    // OpenOffice do not generate DocType declaration
463
                    final DocType docType = RootElement.fromDocument(doc).createDocType(ooxml.getVersion());
464
                    if (docType != null && doc.getDocType() == null)
465
                        doc.setDocType(docType);
466
                }
467
                final String valid = ooxml.getValidator(doc).isValid();
17 ilm 468
                if (valid != null)
469
                    res.put(s, valid);
470
            }
471
        }
472
        return res;
473
    }
474
 
25 ilm 475
    public final ODDocument getODDocument() {
476
        // cache ODDocument otherwise a second one can modify the XML (e.g. remove rows) without the
477
        // first one knowing
478
        if (this.doc == null) {
479
            final ContentType ct = this.getContentType().getType();
480
            if (ct.equals(ContentType.SPREADSHEET))
481
                this.doc = SpreadSheet.get(this);
482
            else if (ct.equals(ContentType.TEXT))
483
                this.doc = TextDocument.get(this);
484
        }
485
        return this.doc;
486
    }
487
 
488
    public final boolean hasODDocument() {
489
        return this.doc != null;
490
    }
491
 
492
    public final SpreadSheet getSpreadSheet() {
493
        return (SpreadSheet) this.getODDocument();
494
    }
495
 
496
    public final TextDocument getTextDocument() {
497
        return (TextDocument) this.getODDocument();
498
    }
499
 
17 ilm 500
    // *** getter on files
501
 
502
    public final Set<String> getEntries() {
503
        return this.files.keySet();
504
    }
505
 
506
    public final ODPackageEntry getEntry(String entry) {
507
        return this.files.get(entry);
508
    }
509
 
510
    protected final Object getData(String entry) {
511
        final ODPackageEntry e = this.getEntry(entry);
512
        return e == null ? null : e.getData();
513
    }
514
 
515
    public final byte[] getBinaryFile(String entry) {
516
        return (byte[]) this.getData(entry);
517
    }
518
 
519
    public final ODXMLDocument getXMLFile(String xmlEntry) {
520
        return (ODXMLDocument) this.getData(xmlEntry);
521
    }
522
 
523
    public final ODXMLDocument getXMLFile(final Document doc) {
524
        for (final String s : subdocNames) {
525
            final ODXMLDocument xmlFile = getXMLFile(s);
526
            if (xmlFile != null && xmlFile.getDocument() == doc) {
527
                return xmlFile;
528
            }
529
        }
530
        return null;
531
    }
532
 
20 ilm 533
    /**
534
     * The XML document where are located the common styles.
535
     *
536
     * @return the document where are located styles.
537
     */
538
    public final ODXMLDocument getStyles() {
539
        final ODXMLDocument res;
540
        if (this.isSingle())
541
            res = this.getContent();
542
        else {
543
            res = this.getXMLFile(STYLES.getZipEntry());
544
        }
545
        return res;
546
    }
547
 
17 ilm 548
    public final ODXMLDocument getContent() {
549
        return this.getXMLFile(CONTENT.getZipEntry());
550
    }
551
 
552
    public final ODMeta getMeta() {
553
        final ODMeta meta;
554
        if (this.getEntries().contains(META.getZipEntry()))
555
            meta = ODMeta.create(this.getXMLFile(META.getZipEntry()));
556
        else
557
            meta = ODMeta.create(this.getContent());
558
        return meta;
559
    }
560
 
561
    /**
562
     * Return an XML document.
563
     *
564
     * @param xmlEntry the filename, eg "styles.xml".
565
     * @return the matching document, or <code>null</code> if there's none.
566
     * @throws JDOMException if error about the XML.
567
     * @throws IOException if an error occurs while reading the file.
568
     */
569
    public Document getDocument(String xmlEntry) {
570
        final ODXMLDocument xml = this.getXMLFile(xmlEntry);
571
        return xml == null ? null : xml.getDocument();
572
    }
573
 
574
    /**
575
     * Find the passed automatic or common style referenced from the content.
576
     *
577
     * @param desc the family, eg {@link ParagraphStyle#DESC}.
578
     * @param name the name, eg "P1".
579
     * @return the corresponding XML element.
580
     */
581
    public final Element getStyle(final StyleDesc<?> desc, final String name) {
582
        return this.getStyle(this.getContent().getDocument(), desc, name);
583
    }
584
 
585
    /**
586
     * Find the passed automatic or common style. NOTE : <code>referent</code> is needed because
587
     * there can exist automatic styles with the same name in both "content.xml" and "styles.xml".
588
     *
589
     * @param referent the document referencing the style.
590
     * @param desc the family, eg {@link ParagraphStyle#DESC}.
591
     * @param name the name, eg "P1".
592
     * @return the corresponding XML element.
593
     * @see ODXMLDocument#getStyle(StyleDesc, String)
594
     */
595
    public final Element getStyle(final Document referent, final StyleDesc<?> desc, final String name) {
596
        // avoid searching in content then styles if it cannot be found
597
        if (name == null)
598
            return null;
599
 
600
        String refSubDoc = null;
601
        final String[] stylesContainer = new String[] { CONTENT.getZipEntry(), STYLES.getZipEntry() };
602
        for (final String subDoc : stylesContainer)
603
            if (this.getDocument(subDoc) == referent)
604
                refSubDoc = subDoc;
605
        if (refSubDoc == null)
606
            throw new IllegalArgumentException("neither in content nor styles : " + referent);
607
 
608
        Element res = this.getXMLFile(refSubDoc).getStyle(desc, name);
609
        // if it isn't in content.xml it might be in styles.xml
610
        if (res == null && refSubDoc.equals(stylesContainer[0]) && this.getXMLFile(stylesContainer[1]) != null)
611
            res = this.getXMLFile(stylesContainer[1]).getStyle(desc, name);
612
        return res;
613
    }
614
 
20 ilm 615
    public final Element getDefaultStyle(final StyleStyleDesc<?> desc) {
616
        // from 16.4 of OpenDocument-v1.2-cs01-part1, default-style only usable in office:styles
617
        return getStyles().getDefaultStyle(desc);
618
    }
619
 
25 ilm 620
    /**
621
     * Verify that styles referenced by this document are indeed defined. NOTE this method is not
622
     * perfect : not all problems are detected.
623
     *
624
     * @return <code>null</code> if no problem has been found, else a String describing it.
625
     */
626
    public final String checkStyles() {
627
        final ODXMLDocument stylesDoc = this.getStyles();
628
        final ODXMLDocument contentDoc = this.getContent();
629
        final Element styles;
630
        if (stylesDoc != null) {
631
            styles = stylesDoc.getChild("styles");
632
            // check styles.xml
633
            final String res = checkStyles(stylesDoc, styles);
634
            if (res != null)
635
                return res;
636
        } else {
637
            styles = contentDoc.getChild("styles");
638
        }
639
 
640
        // check content.xml
641
        return checkStyles(contentDoc, styles);
642
    }
643
 
644
    static private final String checkStyles(ODXMLDocument doc, Element styles) {
645
        try {
646
            final CollectionMap<String, String> stylesNames = getStylesNames(doc, styles, doc.getChild("automatic-styles"));
647
            // text:style-name : text:p, text:span
648
            // table:style-name : table:table, table:row, table:column, table:cell
649
            // draw:style-name : draw:text-box
650
            // style:data-style-name : <style:style style:family="table-cell">
651
            // TODO check by family
652
            final Set<String> names = new HashSet<String>(stylesNames.values());
653
            final Iterator attrs = doc.getXPath(".//@text:style-name | .//@table:style-name | .//@draw:style-name | .//@style:data-style-name | .//@style:list-style-name")
654
                    .selectNodes(doc.getDocument()).iterator();
655
            while (attrs.hasNext()) {
656
                final Attribute attr = (Attribute) attrs.next();
657
                if (!names.contains(attr.getValue()))
658
                    return "unknown style referenced by " + attr.getName() + " in " + JDOMUtils.output(attr.getParent());
659
            }
660
            // TODO check other references like page-*-name (§3 of #prefix())
661
        } catch (IllegalStateException e) {
662
            return ExceptionUtils.getStackTrace(e);
663
        } catch (JDOMException e) {
664
            return ExceptionUtils.getStackTrace(e);
665
        }
666
        return null;
667
    }
668
 
669
    static private final CollectionMap<String, String> getStylesNames(final ODXMLDocument doc, final Element styles, final Element autoStyles) throws IllegalStateException {
670
        // section 14.1 § Style Name : style:family + style:name is unique
671
        final CollectionMap<String, String> res = new CollectionMap<String, String>(HashSet.class);
672
 
673
        final List<Element> nodes = new ArrayList<Element>();
674
        if (styles != null)
675
            nodes.add(styles);
676
        if (autoStyles != null)
677
            nodes.add(autoStyles);
678
 
679
        try {
680
            {
681
                final Iterator iter = doc.getXPath("./style:style/@style:name").selectNodes(nodes).iterator();
682
                while (iter.hasNext()) {
683
                    final Attribute attr = (Attribute) iter.next();
684
                    final String styleName = attr.getValue();
685
                    final String family = attr.getParent().getAttributeValue("family", attr.getNamespace());
686
                    if (res.getNonNull(family).contains(styleName))
687
                        throw new IllegalStateException("duplicate style in " + family + " :  " + styleName);
688
                    res.put(family, styleName);
689
                }
690
            }
691
            {
692
                final List<String> dataStyles = Arrays.asList("number-style", "currency-style", "percentage-style", "date-style", "time-style", "boolean-style", "text-style");
693
                final String xpDataStyles = org.openconcerto.utils.CollectionUtils.join(dataStyles, " | ", new ITransformer<String, String>() {
694
                    @Override
695
                    public String transformChecked(String input) {
696
                        return "./number:" + input;
697
                    }
698
                });
699
                final Iterator listIter = doc.getXPath("./text:list-style | " + xpDataStyles).selectNodes(nodes).iterator();
700
                while (listIter.hasNext()) {
701
                    final Element elem = (Element) listIter.next();
702
                    res.put(elem.getQualifiedName(), elem.getAttributeValue("name", doc.getVersion().getSTYLE()));
703
                }
704
            }
705
        } catch (JDOMException e) {
706
            throw new IllegalStateException(e);
707
        }
708
        return res;
709
    }
710
 
17 ilm 711
    // *** setter
712
 
713
    public void putFile(String entry, Object data) {
714
        this.putFile(entry, data, null);
715
    }
716
 
717
    public void putFile(final String entry, final Object data, final String mediaType) {
718
        this.putFile(entry, data, mediaType, true);
719
    }
720
 
721
    public void putFile(final String entry, final Object data, final String mediaType, final boolean compress) {
722
        if (entry == null)
723
            throw new NullPointerException("null name");
25 ilm 724
        if (data == null) {
725
            this.rmFile(entry);
726
            return;
727
        }
17 ilm 728
        final Object myData;
729
        if (subdocNames.contains(entry)) {
730
            final ODXMLDocument oodoc;
731
            if (data instanceof Document)
25 ilm 732
                oodoc = ODXMLDocument.create((Document) data);
17 ilm 733
            else
734
                oodoc = (ODXMLDocument) data;
25 ilm 735
            checkEntryForDocument(entry);
736
            this.updateTypeAndVersion(entry, oodoc);
17 ilm 737
            myData = oodoc;
25 ilm 738
        } else if (!(data instanceof byte[])) {
17 ilm 739
            throw new IllegalArgumentException("should be byte[] for " + entry + ": " + data);
25 ilm 740
        } else {
741
            if (entry.equals(MIMETYPE_ENTRY))
742
                this.updateTypeAndVersion((byte[]) data);
17 ilm 743
            myData = data;
25 ilm 744
        }
17 ilm 745
        final String inferredType = mediaType != null ? mediaType : FileUtils.findMimeType(entry);
746
        this.files.put(entry, new ODPackageEntry(entry, inferredType, myData, compress));
747
    }
748
 
25 ilm 749
    // Perhaps add a clearODDocument() method to set doc to null and in ODDocument set pkg to null
750
    // (after having verified !hasDocument()). For now just copy the package.
751
    private void checkEntryForDocument(final String entry) {
752
        if (this.hasODDocument() && (entry.equals(RootElement.CONTENT.getZipEntry()) || entry.equals(RootElement.STYLES.getZipEntry())))
753
            throw new IllegalArgumentException("Cannot change content or styles with existing ODDocument");
754
    }
755
 
17 ilm 756
    public void rmFile(String entry) {
25 ilm 757
        this.checkEntryForDocument(entry);
17 ilm 758
        this.files.remove(entry);
25 ilm 759
        if (entry.equals(MIMETYPE_ENTRY) || subdocNames.contains(entry)) {
760
            final Tuple3<XMLVersion, ContentTypeVersioned, XMLFormatVersion> required = this.getRequired(entry);
761
            this.type = required == null ? null : required.get1();
762
            this.version = required == null ? null : required.get2();
763
        }
17 ilm 764
    }
765
 
25 ilm 766
    public void clear() {
767
        this.files.clear();
768
        this.type = null;
769
        this.version = null;
770
    }
771
 
17 ilm 772
    /**
773
     * Transform this to use a {@link ODSingleXMLDocument}. Ie after this method, only "content.xml"
774
     * remains and it's an instance of ODSingleXMLDocument.
775
     *
776
     * @return the created ODSingleXMLDocument.
777
     */
778
    public ODSingleXMLDocument toSingle() {
779
        if (!this.isSingle()) {
25 ilm 780
            return ODSingleXMLDocument.create(this);
17 ilm 781
        } else
782
            return (ODSingleXMLDocument) this.getContent();
783
    }
784
 
785
    public final boolean isSingle() {
786
        return this.getContent() instanceof ODSingleXMLDocument;
787
    }
788
 
789
    /**
790
     * Split the {@link RootElement#SINGLE_CONTENT}. If this was {@link #isSingle() single} the
791
     * former {@link #getContent() content} won't be useable anymore, you can check it with
792
     * {@link ODSingleXMLDocument#isDead()}.
793
     *
794
     * @return <code>true</code> if this was modified.
795
     */
796
    public final boolean split() {
797
        final boolean res;
798
        if (this.isSingle()) {
799
            final Map<RootElement, Document> split = ((ODSingleXMLDocument) this.getContent()).split();
800
            // from 22.2.1 (D1.1.2) of OpenDocument-v1.2-part1-cd04
801
            assert (split.containsKey(RootElement.CONTENT) || split.containsKey(RootElement.STYLES)) && RootElement.getPackageElements().containsAll(split.keySet()) : "wrong elements " + split;
19 ilm 802
            final XMLFormatVersion version = getFormatVersion();
17 ilm 803
            for (final Entry<RootElement, Document> e : split.entrySet()) {
804
                this.putFile(e.getKey().getZipEntry(), new ODXMLDocument(e.getValue(), version));
805
            }
806
            res = true;
807
        } else {
808
            res = false;
809
        }
810
        assert !this.isSingle();
811
        return res;
812
    }
813
 
814
    // *** save
815
 
816
    public final void save(OutputStream out) throws IOException {
817
        // from 22.2.1 (D1.2)
818
        if (this.isSingle()) {
819
            // assert we can use this copy constructor (instead of the slower CopyUtils)
820
            assert this.getClass() == ODPackage.class;
821
            final ODPackage copy = new ODPackage(this);
822
            copy.split();
823
            copy.save(out);
824
            return;
825
        }
826
 
827
        final Zip z = new Zip(out);
828
 
829
        // magic number, see section 17.4
25 ilm 830
        z.zipNonCompressed(MIMETYPE_ENTRY, this.getMimeType().getBytes(MIMETYPE_ENC));
17 ilm 831
 
832
        final Manifest manifest = new Manifest(this.getVersion(), this.getMimeType());
833
        for (final String name : this.files.keySet()) {
834
            // added at the end
25 ilm 835
            if (name.equals(MIMETYPE_ENTRY) || name.equals(Manifest.ENTRY_NAME))
17 ilm 836
                continue;
837
 
838
            final ODPackageEntry entry = this.files.get(name);
839
            final Object val = entry.getData();
840
            if (val != null) {
841
                if (val instanceof ODXMLDocument) {
842
                    final OutputStream o = z.createEntry(name);
843
                    OUTPUTTER.output(((ODXMLDocument) val).getDocument(), o);
844
                    o.close();
845
                } else {
846
                    z.zip(name, (byte[]) val, entry.isCompressed());
847
                }
848
            }
849
            final String mediaType = entry.getType();
850
            manifest.addEntry(name, mediaType == null ? "" : mediaType);
851
        }
852
 
853
        z.zip(Manifest.ENTRY_NAME, new StringInputStream(manifest.asString()));
854
        z.close();
855
    }
856
 
857
    /**
858
     * Save the content of this package to our file, overwriting it if it exists.
859
     *
860
     * @return the saved file.
861
     * @throws IOException if an error occurs while saving.
862
     */
863
    public File save() throws IOException {
864
        return this.saveAs(this.getFile());
865
    }
866
 
867
    public File saveAs(final File fNoExt) throws IOException {
868
        final File f = this.addExt(fNoExt);
869
        if (f.getParentFile() != null)
870
            f.getParentFile().mkdirs();
871
        // ATTN at this point, we must have read all the content of this file
872
        // otherwise we could save to File.createTempFile("oofd", null).deleteOnExit();
873
        final FileOutputStream out = new FileOutputStream(f);
874
        final BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(out, 512 * 1024);
875
        try {
876
            this.save(bufferedOutputStream);
877
        } finally {
878
            bufferedOutputStream.close();
879
        }
880
        return f;
881
    }
882
}