OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 174 | 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;
144 ilm 19
 
174 ilm 20
import org.openconcerto.openoffice.spreadsheet.CellStyle;
25 ilm 21
import org.openconcerto.openoffice.spreadsheet.SpreadSheet;
174 ilm 22
import org.openconcerto.openoffice.style.data.DataStyle;
25 ilm 23
import org.openconcerto.openoffice.text.TextDocument;
17 ilm 24
import org.openconcerto.utils.CopyUtils;
25 ilm 25
import org.openconcerto.utils.ExceptionUtils;
17 ilm 26
import org.openconcerto.utils.FileUtils;
61 ilm 27
import org.openconcerto.utils.ProductInfo;
28
import org.openconcerto.utils.PropertiesUtils;
83 ilm 29
import org.openconcerto.utils.SetMap;
17 ilm 30
import org.openconcerto.utils.StreamUtils;
31
import org.openconcerto.utils.StringInputStream;
57 ilm 32
import org.openconcerto.utils.StringUtils;
33
import org.openconcerto.utils.Tuple2;
25 ilm 34
import org.openconcerto.utils.Tuple3;
17 ilm 35
import org.openconcerto.utils.Zip;
36
import org.openconcerto.utils.ZippedFilesProcessor;
25 ilm 37
import org.openconcerto.utils.cc.ITransformer;
57 ilm 38
import org.openconcerto.utils.io.DataInputStream;
25 ilm 39
import org.openconcerto.xml.JDOMUtils;
19 ilm 40
import org.openconcerto.xml.Validator;
17 ilm 41
 
42
import java.io.BufferedInputStream;
43
import java.io.BufferedOutputStream;
44
import java.io.ByteArrayInputStream;
45
import java.io.ByteArrayOutputStream;
46
import java.io.File;
47
import java.io.FileInputStream;
48
import java.io.FileOutputStream;
49
import java.io.IOException;
50
import java.io.InputStream;
51
import java.io.OutputStream;
52
import java.nio.charset.Charset;
174 ilm 53
import java.text.NumberFormat;
25 ilm 54
import java.util.ArrayList;
55
import java.util.Arrays;
73 ilm 56
import java.util.Collection;
57
import java.util.Collections;
17 ilm 58
import java.util.EnumSet;
59
import java.util.HashMap;
60
import java.util.HashSet;
25 ilm 61
import java.util.Iterator;
62
import java.util.List;
174 ilm 63
import java.util.Locale;
17 ilm 64
import java.util.Map;
65
import java.util.Map.Entry;
61 ilm 66
import java.util.Properties;
17 ilm 67
import java.util.Set;
68
import java.util.zip.ZipEntry;
69
 
25 ilm 70
import org.jdom.Attribute;
17 ilm 71
import org.jdom.DocType;
72
import org.jdom.Document;
73
import org.jdom.Element;
74
import org.jdom.JDOMException;
75
import org.jdom.Namespace;
174 ilm 76
import org.jdom.input.SAXBuilder;
17 ilm 77
import org.jdom.output.Format;
78
import org.jdom.output.XMLOutputter;
79
 
144 ilm 80
import net.jcip.annotations.GuardedBy;
81
 
17 ilm 82
/**
83
 * An OpenDocument package, ie a zip containing XML documents and their associated files.
84
 *
85
 * @author ILM Informatique 2 août 2004
86
 */
87
public class ODPackage {
88
 
25 ilm 89
    static final String MIMETYPE_ENTRY = "mimetype";
17 ilm 90
    /** Normally mimetype contains only ASCII characters */
91
    static final Charset MIMETYPE_ENC = Charset.forName("UTF-8");
92
 
80 ilm 93
    @GuardedBy("ODPackage")
73 ilm 94
    private static String PAGE_COUNT = null;
95
 
96
    /**
97
     * Allow to specify a fixed number of pages for all text documents. This provides a workaround
98
     * for LibreOffice 4.0.x on Ubuntu which takes a long time to open documents without statistics.
99
     * E.g. 40s for 35 pages but only 4s if the page count is 500 pages.
100
     *
101
     * @param count page count, negative means remove.
102
     */
103
    public static final synchronized void setPageCount(final int count) {
104
        if (count < 0)
105
            PAGE_COUNT = null;
106
        else
107
            PAGE_COUNT = String.valueOf(count);
108
    }
109
 
110
    public static final synchronized String getPageCount() {
111
        return PAGE_COUNT;
112
    }
113
 
61 ilm 114
    // not a constant since XMLOutputter isn't thread-safe
115
    static final XMLOutputter createOutputter() {
116
        // use raw format, otherwise spaces are added to every spreadsheet cell
117
        return new XMLOutputter(Format.getRawFormat());
118
    }
119
 
17 ilm 120
    /**
121
     * Root element of an OpenDocument document. See section 22.2.1 of v1.2-part1-cd04.
122
     *
123
     * @author Sylvain CUAZ
124
     */
83 ilm 125
    // full name needed for javac 1.6.0_45
126
    @net.jcip.annotations.Immutable
17 ilm 127
    public static enum RootElement {
128
        /** Contains the entire document, see 3.1.2 of OpenDocument-v1.2-part1-cd04 */
129
        SINGLE_CONTENT("office", "document", null),
130
        /** Document content and automatic styles used in the content, see 3.1.3.2 */
131
        CONTENT("office", "document-content", "content.xml"),
132
        // TODO uncomment and create ContentTypeVersioned for .odf and .otf, see 22.2.9 Conforming
133
        // OpenDocument Formula Document
134
        // MATH("math", "math", "content.xml"),
135
        /** Styles used in document content and automatic styles used in styles, see 3.1.3.3 */
136
        STYLES("office", "document-styles", "styles.xml"),
137
        /** Document metadata elements, see 3.1.3.4 */
138
        META("office", "document-meta", "meta.xml"),
139
        /** Implementation-specific settings, see 3.1.3.5 */
140
        SETTINGS("office", "document-settings", "settings.xml");
141
 
142
        public final static EnumSet<RootElement> getPackageElements() {
143
            return EnumSet.of(CONTENT, STYLES, META, SETTINGS);
144
        }
145
 
25 ilm 146
        public final static RootElement fromDocument(final Document doc) {
147
            return fromElementName(doc.getRootElement().getName());
148
        }
149
 
17 ilm 150
        public final static RootElement fromElementName(final String name) {
151
            for (final RootElement e : values()) {
152
                if (e.getElementName().equals(name))
153
                    return e;
154
            }
155
            return null;
156
        }
157
 
158
        static final Document createSingle(final Document from) {
25 ilm 159
            return SINGLE_CONTENT.createDocument(XMLFormatVersion.get(from));
17 ilm 160
        }
161
 
162
        private final String nsPrefix;
163
        private final String name;
164
        private final String zipEntry;
165
 
166
        private RootElement(String prefix, String rootName, String zipEntry) {
167
            this.nsPrefix = prefix;
168
            this.name = rootName;
169
            this.zipEntry = zipEntry;
170
        }
171
 
172
        public final String getElementNSPrefix() {
173
            return this.nsPrefix;
174
        }
175
 
176
        public final String getElementName() {
177
            return this.name;
178
        }
179
 
25 ilm 180
        public final Document createDocument(final XMLFormatVersion fv) {
181
            final XMLVersion version = fv.getXMLVersion();
17 ilm 182
            final Element root = new Element(getElementName(), version.getNS(getElementNSPrefix()));
183
            // 19.388 office:version identifies the version of ODF specification
25 ilm 184
            if (fv.getOfficeVersion() != null)
185
                root.setAttribute("version", fv.getOfficeVersion(), version.getOFFICE());
17 ilm 186
            // avoid declaring namespaces in each child
187
            for (final Namespace ns : version.getALL())
188
                root.addNamespaceDeclaration(ns);
189
 
25 ilm 190
            return new Document(root, createDocType(version));
191
        }
192
 
193
        public final DocType createDocType(final XMLVersion version) {
17 ilm 194
            // OpenDocument use relaxNG
195
            if (version == XMLVersion.OOo)
25 ilm 196
                return new DocType(getElementNSPrefix() + ":" + getElementName(), "-//OpenOffice.org//DTD OfficeDocument 1.0//EN", "office.dtd");
197
            else
198
                return null;
17 ilm 199
        }
200
 
201
        /**
202
         * The name of the zip entry in the package.
203
         *
204
         * @return the path of the file, <code>null</code> if this element shouldn't be in a
205
         *         package.
206
         */
207
        public final String getZipEntry() {
208
            return this.zipEntry;
209
        }
210
    }
211
 
212
    private static final Set<String> subdocNames;
213
    static {
80 ilm 214
        final Set<String> tmp = new HashSet<String>();
17 ilm 215
        for (final RootElement r : RootElement.getPackageElements())
216
            if (r.getZipEntry() != null)
80 ilm 217
                tmp.add(r.getZipEntry());
218
        subdocNames = Collections.unmodifiableSet(tmp);
17 ilm 219
    }
220
 
221
    /**
222
     * Whether the passed entry is specific to a package.
223
     *
224
     * @param name a entry name, eg "mimetype"
225
     * @return <code>true</code> if <code>name</code> is a standard file, eg <code>true</code>.
226
     */
227
    public static final boolean isStandardFile(final String name) {
73 ilm 228
        return name.equals(MIMETYPE_ENTRY) || subdocNames.contains(name) || name.startsWith("Thumbnails") || name.startsWith("META-INF") || name.startsWith("Configurations")
229
                || name.equals("layout-cache") || name.equals("manifest.rdf") || name.startsWith(Library.DIR_NAME) || name.startsWith(Library.DIALOG_DIR_NAME);
17 ilm 230
    }
231
 
25 ilm 232
    /**
233
     * Create a package from a collection of sub-documents.
234
     *
235
     * @param content the content.
236
     * @param style the styles, can be <code>null</code>.
237
     * @return a package containing the XML documents.
238
     */
239
    public static ODPackage createFromDocuments(Document content, Document style) {
240
        return createFromDocuments(null, content, style, null, null);
241
    }
242
 
243
    public static ODPackage createFromDocuments(final ContentTypeVersioned type, Document content, Document style, Document meta, Document settings) {
244
        final ODPackage pkg = new ODPackage();
245
        if (type != null)
246
            pkg.setContentType(type);
247
        pkg.putFile(RootElement.CONTENT.getZipEntry(), content);
248
        pkg.putFile(RootElement.STYLES.getZipEntry(), style);
249
        pkg.putFile(RootElement.META.getZipEntry(), meta);
250
        pkg.putFile(RootElement.SETTINGS.getZipEntry(), settings);
251
        return pkg;
252
    }
253
 
57 ilm 254
    /**
255
     * Read from the input stream into memory and close it.
256
     *
257
     * @param ins the package or flat XML.
258
     * @param name the name, can be <code>null</code>.
259
     * @return a package containing the document.
260
     * @throws IOException if an error occurs.
261
     */
262
    public static ODPackage createFromStream(final InputStream ins, final String name) throws IOException {
263
        try {
264
            return create(null, ins, name);
265
        } finally {
266
            ins.close();
267
        }
268
    }
269
 
270
    public static ODPackage createFromFile(final File f) throws IOException {
180 ilm 271
        try (final FileInputStream ins = new FileInputStream(f)) {
57 ilm 272
            return create(f, ins, f.getName());
273
        }
274
    }
275
 
276
    private static final int mimetypeZipEndOffset = 250;
277
 
83 ilm 278
    // ATTN ins is *not* closed if f != null
57 ilm 279
    private static ODPackage create(final File f, InputStream ins, final String name) throws IOException {
280
        // first use extension
281
        final Tuple2<ContentTypeVersioned, Boolean> fromExt = name != null ? ContentTypeVersioned.fromExtension(FileUtils.getExtension(name)) : Tuple2.<ContentTypeVersioned, Boolean> nullInstance();
282
        ContentTypeVersioned contentType = fromExt.get0();
283
        Boolean flat = fromExt.get1();
284
        // then content
285
        if (flat == null) {
286
            ins = new BufferedInputStream(ins);
287
            final String xmlStart = "<?xml";
288
            if (ins.markSupported())
289
                ins.mark(Math.max(xmlStart.length(), mimetypeZipEndOffset));
290
            else
291
                throw new IllegalStateException("Mark unsupported on " + ins);
292
            final byte[] buffer = new byte[xmlStart.length()];
293
            ins.read(buffer);
294
            if (xmlStart.equals(new String(buffer, StringUtils.ASCII))) {
295
                // would have to parse the whole document
296
                contentType = null;
297
                flat = true;
298
            } else {
299
                ins.reset();
300
                contentType = getType(ins);
301
                if (contentType != null)
302
                    flat = false;
303
            }
304
            ins.reset();
305
        }
306
        final ODPackage res;
307
        if (flat == null) {
308
            res = null;
309
        } else if (flat) {
310
            try {
311
                res = (f != null ? ODSingleXMLDocument.createFromFile(f) : ODSingleXMLDocument.createFromStream(ins)).getPackage();
312
            } catch (JDOMException e) {
313
                throw new IOException(e);
314
            }
315
        } else {
316
            res = f != null ? new ODPackage(f) : new ODPackage(ins);
317
        }
318
        assert contentType == null || contentType == res.getContentType();
319
        return res;
320
    }
321
 
322
    private static ContentTypeVersioned getType(final InputStream in) throws IOException {
83 ilm 323
        // don't want to close underlying stream
324
        @SuppressWarnings("resource")
57 ilm 325
        final DataInputStream ins = new DataInputStream(in, true);
326
        if (ins.read() != 'P' || ins.read() != 'K')
327
            return null;
328
        if (ins.skip(16) != 16)
329
            return null;
330
        final int compressedSize = ins.readInt();
331
        final int uncompressedSize = ins.readInt();
332
        // not a valid package and beyond that we would need to actually inflate the data
333
        if (compressedSize != uncompressedSize)
334
            return null;
335
        final short fnameLength = ins.readShort();
336
        if (fnameLength != MIMETYPE_ENTRY.length())
337
            return null;
338
        final short extraLength = ins.readShort();
339
        final byte[] array = new byte[Math.max(fnameLength, compressedSize)];
340
        ins.read(array, 0, fnameLength);
341
        if (!new String(array, 0, fnameLength, StringUtils.ASCII).equals(MIMETYPE_ENTRY))
342
            return null;
343
        if (ins.skip(extraLength) != extraLength)
344
            return null;
345
        ins.read(array, 0, compressedSize);
346
        final String data = new String(array, 0, compressedSize, MIMETYPE_ENC);
347
        return ContentTypeVersioned.fromMime(data);
348
    }
349
 
25 ilm 350
    static private XMLVersion getVersion(final XMLFormatVersion fv, final ContentTypeVersioned ct) {
351
        final XMLVersion v;
352
        if (ct == null && fv == null)
353
            v = null;
354
        else if (ct != null)
355
            v = ct.getVersion();
356
        else
357
            v = fv.getXMLVersion();
358
        assert fv == null || ct == null || fv.getXMLVersion() == ct.getVersion();
359
        return v;
360
    }
361
 
73 ilm 362
    static private <T> void checkVersion(final Class<T> clazz, final String s, final String entry, final T actual, final T required) {
25 ilm 363
        if (actual != null && required != null) {
364
            final boolean ok;
365
            if (actual instanceof ContentTypeVersioned) {
366
                // we can change our template status since it doesn't affect our content
367
                ok = ((ContentTypeVersioned) actual).getNonTemplate().equals(((ContentTypeVersioned) required).getNonTemplate());
368
            } else {
369
                ok = actual.equals(required);
370
            }
371
            if (!ok)
73 ilm 372
                throw new IllegalArgumentException(entry + " would change " + s + " from " + required + " to " + actual);
25 ilm 373
        }
374
    }
375
 
17 ilm 376
    private final Map<String, ODPackageEntry> files;
377
    private ContentTypeVersioned type;
25 ilm 378
    private XMLFormatVersion version;
61 ilm 379
    private ODMeta meta;
17 ilm 380
    private File file;
25 ilm 381
    private ODDocument doc;
174 ilm 382
    private Locale locale;
17 ilm 383
 
384
    public ODPackage() {
385
        this.files = new HashMap<String, ODPackageEntry>();
386
        this.type = null;
25 ilm 387
        this.version = null;
61 ilm 388
        this.meta = null;
17 ilm 389
        this.file = null;
25 ilm 390
        this.doc = null;
17 ilm 391
    }
392
 
57 ilm 393
    /**
394
     * Read from the input stream into memory and close it.
395
     *
396
     * @param ins the package.
397
     * @throws IOException if <code>ins</code> couldn't be read.
398
     */
17 ilm 399
    public ODPackage(InputStream ins) throws IOException {
400
        this();
401
 
402
        final ByteArrayOutputStream out = new ByteArrayOutputStream(4096);
403
        new ZippedFilesProcessor() {
404
            @Override
405
            protected void processEntry(ZipEntry entry, InputStream in) throws IOException {
406
                final String name = entry.getName();
407
                final Object res;
174 ilm 408
                out.reset();
409
                StreamUtils.copy(in, out);
17 ilm 410
                if (subdocNames.contains(name)) {
411
                    try {
174 ilm 412
                        final SAXBuilder builder = OOUtils.getBuilder();
413
                        // bytes are read fully first in order to be compatible
414
                        // with SAXParser like Piccolo
415
                        res = builder.build(new ByteArrayInputStream(out.toByteArray()));
17 ilm 416
                    } catch (JDOMException e) {
417
                        // always correct
174 ilm 418
                        throw new IllegalStateException("parse error on " + name, e);
17 ilm 419
                    }
420
                } else {
421
                    res = out.toByteArray();
422
                }
423
                // we don't know yet the types
424
                putFile(name, res, null, entry.getMethod() == ZipEntry.DEFLATED);
425
            }
426
        }.process(ins);
427
        // fill in the missing types from the manifest, if any
428
        final ODPackageEntry me = this.files.remove(Manifest.ENTRY_NAME);
429
        if (me != null) {
430
            final byte[] m = (byte[]) me.getData();
431
            try {
432
                final Map<String, String> manifestEntries = Manifest.parse(new ByteArrayInputStream(m));
433
                for (final Map.Entry<String, String> e : manifestEntries.entrySet()) {
434
                    final String path = e.getKey();
73 ilm 435
                    final String type = e.getValue();
17 ilm 436
                    final ODPackageEntry entry = this.files.get(path);
437
                    // eg directory
73 ilm 438
                    if (entry == null) {
439
                        this.files.put(path, new ODPackageEntry(path, type, null));
440
                        // subdocs are already parsed to ODXMLDocument
441
                    } else if (type.equals(FileUtils.XML_TYPE) && entry.getData() instanceof byte[]) {
442
                        final Document doc = OOUtils.getBuilder().build(new ByteArrayInputStream((byte[]) entry.getData()));
443
                        this.putFile(path, doc, type, entry.isCompressed());
444
                    } else {
445
                        entry.setType(type);
446
                    }
17 ilm 447
                }
448
            } catch (JDOMException e) {
449
                throw new IllegalArgumentException("bad manifest " + new String(m), e);
450
            }
451
        }
452
    }
453
 
454
    public ODPackage(File f) throws IOException {
455
        this(new BufferedInputStream(new FileInputStream(f), 512 * 1024));
456
        this.file = f;
457
    }
458
 
459
    public ODPackage(ODPackage o) {
460
        this();
461
        for (final String name : o.getEntries()) {
462
            final ODPackageEntry entry = o.getEntry(name);
73 ilm 463
            this.putCopy(entry);
17 ilm 464
        }
465
        this.type = o.type;
25 ilm 466
        this.version = o.version;
61 ilm 467
        this.meta = null;
17 ilm 468
        this.file = o.file;
25 ilm 469
        this.doc = null;
17 ilm 470
    }
471
 
472
    public final File getFile() {
473
        return this.file;
474
    }
475
 
476
    public final void setFile(File f) {
477
        this.file = this.addExt(f);
478
    }
479
 
480
    private final File addExt(File f) {
61 ilm 481
        return this.getContentType().addExt(f, false);
17 ilm 482
    }
483
 
484
    /**
485
     * The version of this package, <code>null</code> if it cannot be found (eg this package is
486
     * empty, or contains no xml).
487
     *
488
     * @return the version of this package, can be <code>null</code>.
489
     */
490
    public final XMLVersion getVersion() {
25 ilm 491
        return getVersion(this.version, this.type);
19 ilm 492
    }
493
 
494
    public final XMLFormatVersion getFormatVersion() {
25 ilm 495
        return this.version;
17 ilm 496
    }
497
 
498
    /**
499
     * The type of this package, <code>null</code> if it cannot be found (eg this package is empty).
500
     *
501
     * @return the type of this package, can be <code>null</code>.
502
     */
503
    public final ContentTypeVersioned getContentType() {
25 ilm 504
        return this.type;
505
    }
506
 
507
    public final void setContentType(final ContentTypeVersioned newType) {
508
        this.putFile(MIMETYPE_ENTRY, newType.getMimeType().getBytes(MIMETYPE_ENC));
509
    }
510
 
511
    private void updateTypeAndVersion(final String entry, ODXMLDocument xml) {
512
        this.setTypeAndVersion(entry.equals(CONTENT.getZipEntry()) ? ContentTypeVersioned.fromContent(xml) : null, xml.getFormatVersion(), entry);
513
    }
514
 
515
    private void updateTypeAndVersion(byte[] mimetype) {
516
        this.setTypeAndVersion(ContentTypeVersioned.fromMime(mimetype), null, MIMETYPE_ENTRY);
517
    }
518
 
519
    private final void setTypeAndVersion(final ContentTypeVersioned ct, final XMLFormatVersion fv, final String entry) {
520
        final Tuple3<XMLVersion, ContentTypeVersioned, XMLFormatVersion> requiredByPkg = this.getRequired(entry);
521
        if (requiredByPkg != null) {
73 ilm 522
            checkVersion(XMLVersion.class, "version", entry, getVersion(fv, ct), requiredByPkg.get0());
523
            checkVersion(ContentTypeVersioned.class, "type", entry, ct, requiredByPkg.get1());
524
            checkVersion(XMLFormatVersion.class, "format version", entry, fv, requiredByPkg.get2());
25 ilm 525
        }
526
 
527
        // since we're adding "entry" never set attributes to null
528
        if (fv != null && !fv.equals(this.version))
529
            this.version = fv;
530
        // don't let non-template from content overwrite the correct one
531
        if (ct != null && !ct.equals(this.type) && (this.type == null || entry.equals(MIMETYPE_ENTRY)))
532
            this.type = ct;
533
    }
534
 
535
    // find the versions required by the package without the passed entry
536
    private final Tuple3<XMLVersion, ContentTypeVersioned, XMLFormatVersion> getRequired(final String entryToIgnore) {
537
        if (this.files.size() == 0 || (this.files.size() == 1 && this.files.containsKey(entryToIgnore)))
538
            return null;
539
 
540
        final byte[] mimetype;
541
        if (this.files.containsKey(MIMETYPE_ENTRY) && !MIMETYPE_ENTRY.equals(entryToIgnore)) {
542
            mimetype = this.getBinaryFile(MIMETYPE_ENTRY);
543
        } else {
544
            mimetype = null;
545
        }
546
        XMLFormatVersion fv = null;
547
        final Map<String, Object> versionFiles = new HashMap<String, Object>();
548
        for (final String e : subdocNames) {
549
            if (this.files.containsKey(e) && !e.equals(entryToIgnore)) {
550
                final ODXMLDocument xmlFile = this.getXMLFile(e);
551
                versionFiles.put(e, xmlFile);
552
                if (fv == null)
553
                    fv = xmlFile.getFormatVersion();
554
                else
555
                    assert fv.equals(xmlFile.getFormatVersion()) : "Incoherence";
17 ilm 556
            }
557
        }
25 ilm 558
        final ODXMLDocument content = (ODXMLDocument) versionFiles.get(CONTENT.getZipEntry());
559
 
560
        final ContentTypeVersioned ct;
561
        if (mimetype != null)
562
            ct = ContentTypeVersioned.fromMime(mimetype);
563
        else if (content != null)
564
            ct = ContentTypeVersioned.fromContent(content);
565
        else
566
            ct = null;
567
 
568
        return Tuple3.create(getVersion(fv, ct), ct, fv);
17 ilm 569
    }
570
 
571
    public final String getMimeType() {
572
        return this.getContentType().getMimeType();
573
    }
574
 
25 ilm 575
    public final boolean isTemplate() {
576
        return this.getContentType().isTemplate();
577
    }
578
 
579
    public final void setTemplate(boolean b) {
580
        if (this.type == null)
581
            throw new IllegalStateException("No type");
582
        final ContentTypeVersioned newType = b ? this.type.getTemplate() : this.type.getNonTemplate();
583
        if (newType == null)
584
            throw new IllegalStateException("Missing " + (b ? "" : "non-") + "template for " + this.type);
585
        this.setContentType(newType);
586
    }
587
 
174 ilm 588
    public final void setLocale(Locale locale) {
589
        this.locale = locale;
590
    }
591
 
592
    // http://help.libreoffice.org/Common/Selecting_the_Document_Language
593
    // The document language is set by default-style/text-properties and is not used for formatting.
594
    // The formatting used to fill cell content is set by the editor application not read from the
595
    // document.
596
    public final Locale getLocale() {
597
        return this.locale == null ? Locale.getDefault() : this.locale;
598
    }
599
 
180 ilm 600
    public static final String formatNumber(Number n, final Locale locale, final CellStyle defaultStyle) {
601
        return formatNumber(NumberFormat.getNumberInstance(locale), n, defaultStyle);
174 ilm 602
    }
603
 
180 ilm 604
    public static final String formatPercent(Number n, final Locale locale, final CellStyle defaultStyle) {
605
        return formatNumber(NumberFormat.getPercentInstance(locale), n, defaultStyle);
174 ilm 606
    }
607
 
180 ilm 608
    public static final String formatCurrency(Number n, final Locale locale, final CellStyle defaultStyle) {
609
        return formatNumber(NumberFormat.getCurrencyInstance(locale), n, defaultStyle);
174 ilm 610
    }
611
 
180 ilm 612
    private static final String formatNumber(NumberFormat format, Number n, final CellStyle defaultStyle) {
174 ilm 613
        synchronized (format) {
614
            final int decPlaces = DataStyle.getDecimalPlaces(defaultStyle);
615
            format.setMinimumFractionDigits(0);
616
            format.setMaximumFractionDigits(decPlaces);
617
            return format.format(n);
618
        }
619
    }
620
 
17 ilm 621
    /**
19 ilm 622
     * Call {@link Validator#isValid()} on each XML subdocuments.
17 ilm 623
     *
80 ilm 624
     * @return all problems indexed by package entry names, i.e. empty if all OK, <code>null</code>
19 ilm 625
     *         if validation couldn't occur.
17 ilm 626
     */
627
    public final Map<String, String> validateSubDocuments() {
83 ilm 628
        return this.validateSubDocuments(true, true);
25 ilm 629
    }
630
 
83 ilm 631
    public final Map<String, String> validateSubDocuments(final boolean allowChangeToValidate, final boolean ignoreForeign) {
19 ilm 632
        final OOXML ooxml = this.getFormatVersion().getXML();
633
        if (!ooxml.canValidate())
634
            return null;
17 ilm 635
        final Map<String, String> res = new HashMap<String, String>();
636
        for (final String s : subdocNames) {
25 ilm 637
            final Document doc = this.getDocument(s);
638
            if (doc != null) {
639
                if (allowChangeToValidate) {
640
                    // OpenOffice do not generate DocType declaration
641
                    final DocType docType = RootElement.fromDocument(doc).createDocType(ooxml.getVersion());
642
                    if (docType != null && doc.getDocType() == null)
643
                        doc.setDocType(docType);
644
                }
83 ilm 645
                final String valid = ooxml.getValidator(doc, ignoreForeign).isValid();
17 ilm 646
                if (valid != null)
647
                    res.put(s, valid);
648
            }
649
        }
83 ilm 650
        final String valid = ooxml.getValidator(this.createManifest().getDocument(), ignoreForeign).isValid();
80 ilm 651
        if (valid != null)
652
            res.put(Manifest.ENTRY_NAME, valid);
17 ilm 653
        return res;
654
    }
655
 
25 ilm 656
    public final ODDocument getODDocument() {
657
        // cache ODDocument otherwise a second one can modify the XML (e.g. remove rows) without the
658
        // first one knowing
659
        if (this.doc == null) {
660
            final ContentType ct = this.getContentType().getType();
661
            if (ct.equals(ContentType.SPREADSHEET))
662
                this.doc = SpreadSheet.get(this);
663
            else if (ct.equals(ContentType.TEXT))
664
                this.doc = TextDocument.get(this);
665
        }
666
        return this.doc;
667
    }
668
 
669
    public final boolean hasODDocument() {
670
        return this.doc != null;
671
    }
672
 
673
    public final SpreadSheet getSpreadSheet() {
674
        return (SpreadSheet) this.getODDocument();
675
    }
676
 
677
    public final TextDocument getTextDocument() {
678
        return (TextDocument) this.getODDocument();
679
    }
680
 
17 ilm 681
    // *** getter on files
682
 
683
    public final Set<String> getEntries() {
684
        return this.files.keySet();
685
    }
686
 
687
    public final ODPackageEntry getEntry(String entry) {
688
        return this.files.get(entry);
689
    }
690
 
691
    protected final Object getData(String entry) {
692
        final ODPackageEntry e = this.getEntry(entry);
693
        return e == null ? null : e.getData();
694
    }
695
 
696
    public final byte[] getBinaryFile(String entry) {
697
        return (byte[]) this.getData(entry);
698
    }
699
 
700
    public final ODXMLDocument getXMLFile(String xmlEntry) {
701
        return (ODXMLDocument) this.getData(xmlEntry);
702
    }
703
 
704
    public final ODXMLDocument getXMLFile(final Document doc) {
705
        for (final String s : subdocNames) {
706
            final ODXMLDocument xmlFile = getXMLFile(s);
707
            if (xmlFile != null && xmlFile.getDocument() == doc) {
708
                return xmlFile;
709
            }
710
        }
711
        return null;
712
    }
713
 
20 ilm 714
    /**
715
     * The XML document where are located the common styles.
716
     *
717
     * @return the document where are located styles.
718
     */
719
    public final ODXMLDocument getStyles() {
720
        final ODXMLDocument res;
721
        if (this.isSingle())
722
            res = this.getContent();
723
        else {
724
            res = this.getXMLFile(STYLES.getZipEntry());
725
        }
726
        return res;
727
    }
728
 
17 ilm 729
    public final ODXMLDocument getContent() {
730
        return this.getXMLFile(CONTENT.getZipEntry());
731
    }
732
 
733
    public final ODMeta getMeta() {
61 ilm 734
        return this.getMeta(false);
17 ilm 735
    }
736
 
61 ilm 737
    public final ODMeta getMeta(final boolean create) {
738
        if (this.meta == null) {
739
            if (this.isSingle()) {
740
                this.meta = ODMeta.create(this.getContent(), create);
741
            } else {
742
                final String metaEntry = META.getZipEntry();
743
                ODXMLDocument xmlFile = this.getXMLFile(metaEntry);
744
                if (xmlFile == null && create) {
745
                    this.putFile(metaEntry, RootElement.META.createDocument(getFormatVersion()));
746
                    xmlFile = this.getXMLFile(metaEntry);
747
                }
748
                if (xmlFile != null) {
749
                    this.meta = ODMeta.create(xmlFile, create);
750
                }
751
            }
752
        }
753
        return this.meta;
754
    }
755
 
17 ilm 756
    /**
73 ilm 757
     * Parse BASIC libraries in this package.
758
     *
759
     * @return the BASIC libraries by name.
760
     */
761
    public final Map<String, Library> readBasicLibraries() {
762
        if (this.isSingle())
763
            return ((ODSingleXMLDocument) this.getContent()).readBasicLibraries();
764
 
765
        // TODO read DIALOG_LIBRARY_LIST_FILENAME (to support Library with only dialogs)
766
        final Document doc = (Document) this.getData(Library.DIR_NAME + "/" + Library.LIBRARY_LIST_FILENAME);
767
        if (doc == null)
768
            return Collections.emptyMap();
769
        @SuppressWarnings("unchecked")
770
        final List<Element> librariesElems = doc.getRootElement().getChildren();
771
        final Map<String, Library> res = new HashMap<String, Library>(librariesElems.size());
772
        for (final Element libraryElem : librariesElems) {
773
            final Library lib = Library.fromPackage(libraryElem, this);
774
            if (res.put(lib.getName(), lib) != null)
775
                throw new IllegalStateException("Duplicate library named " + lib.getName());
776
        }
777
        return res;
778
    }
779
 
780
    /**
781
     * Add the passed libraries to this package. Passed libraries with the same content as existing
782
     * ones are ignored.
783
     *
784
     * @param libraries what to add.
785
     * @return the actually added libraries.
786
     * @throws IllegalArgumentException if <code>libraries</code> contains duplicates or if it
787
     *         cannot be merged into this.
788
     * @see Library#canBeMerged(Library)
789
     */
790
    public final Set<String> addBasicLibraries(final Collection<? extends Library> libraries) {
791
        return this.addBasicLibraries(Library.toMap(libraries));
792
    }
793
 
794
    public final Set<String> addBasicLibraries(final ODPackage pkg) {
795
        if (pkg == this)
796
            return Collections.emptySet();
797
        return this.addBasicLibraries(pkg.readBasicLibraries());
798
    }
799
 
800
    private final Set<String> addBasicLibraries(final Map<String, Library> oLibraries) {
801
        if (oLibraries.size() == 0)
802
            return Collections.emptySet();
803
        if (this.isSingle())
804
            return ((ODSingleXMLDocument) this.getContent()).addBasicLibraries(oLibraries);
805
 
806
        final Map<String, Library> thisLibraries = this.readBasicLibraries();
807
        // check that the libraries to add which are already in us can be merged (no elements
808
        // conflict)
809
        Library.canBeMerged(thisLibraries, oLibraries);
810
 
811
        // merge
812
        for (final Library oLib : oLibraries.values()) {
813
            // can be null
814
            final Library thisLib = thisLibraries.get(oLib.getName());
815
            oLib.mergeModules(this, thisLib);
816
            oLib.mergeDialogs(this, thisLib);
817
        }
818
 
819
        final Set<String> newLibs = new HashSet<String>(oLibraries.keySet());
820
        newLibs.removeAll(thisLibraries.keySet());
821
        return newLibs;
822
    }
823
 
824
    /**
825
     * Remove the passed libraries.
826
     *
827
     * @param libraries which libraries to remove.
828
     * @return the actually removed libraries.
829
     */
830
    public final Set<String> removeBasicLibraries(final Collection<String> libraries) {
831
        if (libraries.size() == 0)
832
            return Collections.emptySet();
833
        if (this.isSingle())
834
            return ((ODSingleXMLDocument) this.getContent()).removeBasicLibraries(libraries);
835
 
836
        final Set<String> res = new HashSet<String>();
837
        for (final String libToRm : libraries) {
838
            if (Library.removeFromPackage(this, libToRm))
839
                res.add(libToRm);
840
        }
841
        return res;
842
    }
843
 
844
    /**
845
     * Parse events for the whole document.
846
     *
847
     * @return event listeners by event name.
848
     */
849
    public final Map<String, EventListener> readEventListeners() {
850
        final OOXML xml = getFormatVersion().getXML();
851
        final Element scriptsElem = this.getContent().getChild(xml.getOfficeScripts(), false);
852
        final Element eventListeners = scriptsElem == null ? null : scriptsElem.getChild(xml.getOfficeEventListeners(), getVersion().getOFFICE());
853
        if (eventListeners == null)
854
            return Collections.emptyMap();
855
 
856
        final Map<String, EventListener> res = new HashMap<String, EventListener>();
857
        final Namespace scriptNS = getVersion().getNS("script");
858
        @SuppressWarnings("unchecked")
859
        final List<Element> listeners = eventListeners.getChildren(xml.getEventListener(), scriptNS);
860
        for (final Element listener : listeners) {
861
            final EventListener l = new EventListener(listener);
862
            res.put(l.getName(), l);
863
        }
864
        return res;
865
    }
866
 
867
    /**
17 ilm 868
     * Return an XML document.
869
     *
870
     * @param xmlEntry the filename, eg "styles.xml".
871
     * @return the matching document, or <code>null</code> if there's none.
872
     * @throws JDOMException if error about the XML.
873
     * @throws IOException if an error occurs while reading the file.
874
     */
875
    public Document getDocument(String xmlEntry) {
876
        final ODXMLDocument xml = this.getXMLFile(xmlEntry);
877
        return xml == null ? null : xml.getDocument();
878
    }
879
 
880
    /**
881
     * Find the passed automatic or common style referenced from the content.
882
     *
65 ilm 883
     * @param desc the family, eg <code>StyleStyleDesc&lt;ParagraphStyle&gt;</code>.
17 ilm 884
     * @param name the name, eg "P1".
885
     * @return the corresponding XML element.
886
     */
887
    public final Element getStyle(final StyleDesc<?> desc, final String name) {
888
        return this.getStyle(this.getContent().getDocument(), desc, name);
889
    }
890
 
891
    /**
892
     * Find the passed automatic or common style. NOTE : <code>referent</code> is needed because
893
     * there can exist automatic styles with the same name in both "content.xml" and "styles.xml".
894
     *
895
     * @param referent the document referencing the style.
65 ilm 896
     * @param desc the family, eg <code>StyleStyleDesc&lt;ParagraphStyle&gt;</code>.
17 ilm 897
     * @param name the name, eg "P1".
898
     * @return the corresponding XML element.
73 ilm 899
     * @see ODXMLDocument#getStyle(StyleDesc, String, Document)
17 ilm 900
     */
901
    public final Element getStyle(final Document referent, final StyleDesc<?> desc, final String name) {
902
        // avoid searching in content then styles if it cannot be found
903
        if (name == null)
904
            return null;
905
 
906
        String refSubDoc = null;
73 ilm 907
        ODXMLDocument refXMLFile = null;
17 ilm 908
        final String[] stylesContainer = new String[] { CONTENT.getZipEntry(), STYLES.getZipEntry() };
73 ilm 909
        for (final String subDoc : stylesContainer) {
910
            final ODXMLDocument xmlFile = this.getXMLFile(subDoc);
911
            if (xmlFile != null && xmlFile.getDocument() == referent) {
17 ilm 912
                refSubDoc = subDoc;
73 ilm 913
                refXMLFile = xmlFile;
914
                break;
915
            }
916
        }
17 ilm 917
        if (refSubDoc == null)
918
            throw new IllegalArgumentException("neither in content nor styles : " + referent);
919
 
73 ilm 920
        Element res = refXMLFile.getStyle(desc, name, referent);
17 ilm 921
        // if it isn't in content.xml it might be in styles.xml
73 ilm 922
        if (res == null && refSubDoc.equals(stylesContainer[0])) {
923
            final ODXMLDocument stylesXMLFile = this.getXMLFile(stylesContainer[1]);
924
            if (stylesXMLFile != null)
925
                res = stylesXMLFile.getStyle(desc, name, referent);
926
        }
17 ilm 927
        return res;
928
    }
929
 
65 ilm 930
    public final Element getDefaultStyle(final StyleStyleDesc<?> desc, final boolean create) {
20 ilm 931
        // from 16.4 of OpenDocument-v1.2-cs01-part1, default-style only usable in office:styles
65 ilm 932
        return getStyles().getDefaultStyle(desc, create);
20 ilm 933
    }
934
 
25 ilm 935
    /**
936
     * Verify that styles referenced by this document are indeed defined. NOTE this method is not
937
     * perfect : not all problems are detected.
938
     *
939
     * @return <code>null</code> if no problem has been found, else a String describing it.
940
     */
941
    public final String checkStyles() {
942
        final ODXMLDocument stylesDoc = this.getStyles();
943
        final ODXMLDocument contentDoc = this.getContent();
944
        final Element styles;
945
        if (stylesDoc != null) {
946
            styles = stylesDoc.getChild("styles");
947
            // check styles.xml
948
            final String res = checkStyles(stylesDoc, styles);
949
            if (res != null)
950
                return res;
951
        } else {
952
            styles = contentDoc.getChild("styles");
953
        }
954
 
955
        // check content.xml
956
        return checkStyles(contentDoc, styles);
957
    }
958
 
959
    static private final String checkStyles(ODXMLDocument doc, Element styles) {
960
        try {
83 ilm 961
            final SetMap<String, String> stylesNames = getStylesNames(doc, styles, doc.getChild("automatic-styles"));
25 ilm 962
            // text:style-name : text:p, text:span
963
            // table:style-name : table:table, table:row, table:column, table:cell
964
            // draw:style-name : draw:text-box
965
            // style:data-style-name : <style:style style:family="table-cell">
966
            // TODO check by family
83 ilm 967
            final Set<String> names = new HashSet<String>(stylesNames.allValues());
25 ilm 968
            final Iterator attrs = doc.getXPath(".//@text:style-name | .//@table:style-name | .//@draw:style-name | .//@style:data-style-name | .//@style:list-style-name")
969
                    .selectNodes(doc.getDocument()).iterator();
970
            while (attrs.hasNext()) {
971
                final Attribute attr = (Attribute) attrs.next();
972
                if (!names.contains(attr.getValue()))
973
                    return "unknown style referenced by " + attr.getName() + " in " + JDOMUtils.output(attr.getParent());
974
            }
975
            // TODO check other references like page-*-name (§3 of #prefix())
976
        } catch (IllegalStateException e) {
977
            return ExceptionUtils.getStackTrace(e);
978
        } catch (JDOMException e) {
979
            return ExceptionUtils.getStackTrace(e);
980
        }
981
        return null;
982
    }
983
 
83 ilm 984
    static private final SetMap<String, String> getStylesNames(final ODXMLDocument doc, final Element styles, final Element autoStyles) throws IllegalStateException {
25 ilm 985
        // section 14.1 § Style Name : style:family + style:name is unique
83 ilm 986
        final SetMap<String, String> res = new SetMap<String, String>();
25 ilm 987
 
988
        final List<Element> nodes = new ArrayList<Element>();
989
        if (styles != null)
990
            nodes.add(styles);
991
        if (autoStyles != null)
992
            nodes.add(autoStyles);
993
 
994
        try {
995
            {
996
                final Iterator iter = doc.getXPath("./style:style/@style:name").selectNodes(nodes).iterator();
997
                while (iter.hasNext()) {
998
                    final Attribute attr = (Attribute) iter.next();
999
                    final String styleName = attr.getValue();
1000
                    final String family = attr.getParent().getAttributeValue("family", attr.getNamespace());
1001
                    if (res.getNonNull(family).contains(styleName))
1002
                        throw new IllegalStateException("duplicate style in " + family + " :  " + styleName);
83 ilm 1003
                    res.add(family, styleName);
25 ilm 1004
                }
1005
            }
1006
            {
1007
                final List<String> dataStyles = Arrays.asList("number-style", "currency-style", "percentage-style", "date-style", "time-style", "boolean-style", "text-style");
1008
                final String xpDataStyles = org.openconcerto.utils.CollectionUtils.join(dataStyles, " | ", new ITransformer<String, String>() {
1009
                    @Override
1010
                    public String transformChecked(String input) {
1011
                        return "./number:" + input;
1012
                    }
1013
                });
1014
                final Iterator listIter = doc.getXPath("./text:list-style | " + xpDataStyles).selectNodes(nodes).iterator();
1015
                while (listIter.hasNext()) {
1016
                    final Element elem = (Element) listIter.next();
83 ilm 1017
                    res.add(elem.getQualifiedName(), elem.getAttributeValue("name", doc.getVersion().getSTYLE()));
25 ilm 1018
                }
1019
            }
1020
        } catch (JDOMException e) {
1021
            throw new IllegalStateException(e);
1022
        }
1023
        return res;
1024
    }
1025
 
17 ilm 1026
    // *** setter
1027
 
1028
    public void putFile(String entry, Object data) {
1029
        this.putFile(entry, data, null);
1030
    }
1031
 
1032
    public void putFile(final String entry, final Object data, final String mediaType) {
1033
        this.putFile(entry, data, mediaType, true);
1034
    }
1035
 
1036
    public void putFile(final String entry, final Object data, final String mediaType, final boolean compress) {
1037
        if (entry == null)
1038
            throw new NullPointerException("null name");
25 ilm 1039
        if (data == null) {
1040
            this.rmFile(entry);
1041
            return;
1042
        }
17 ilm 1043
        final Object myData;
1044
        if (subdocNames.contains(entry)) {
1045
            final ODXMLDocument oodoc;
1046
            if (data instanceof Document)
25 ilm 1047
                oodoc = ODXMLDocument.create((Document) data);
17 ilm 1048
            else
1049
                oodoc = (ODXMLDocument) data;
25 ilm 1050
            checkEntryForDocument(entry);
1051
            this.updateTypeAndVersion(entry, oodoc);
17 ilm 1052
            myData = oodoc;
73 ilm 1053
        } else if (data instanceof Document) {
1054
            myData = data;
25 ilm 1055
        } else if (!(data instanceof byte[])) {
17 ilm 1056
            throw new IllegalArgumentException("should be byte[] for " + entry + ": " + data);
25 ilm 1057
        } else {
1058
            if (entry.equals(MIMETYPE_ENTRY))
1059
                this.updateTypeAndVersion((byte[]) data);
17 ilm 1060
            myData = data;
25 ilm 1061
        }
17 ilm 1062
        final String inferredType = mediaType != null ? mediaType : FileUtils.findMimeType(entry);
1063
        this.files.put(entry, new ODPackageEntry(entry, inferredType, myData, compress));
1064
    }
1065
 
25 ilm 1066
    // Perhaps add a clearODDocument() method to set doc to null and in ODDocument set pkg to null
1067
    // (after having verified !hasDocument()). For now just copy the package.
1068
    private void checkEntryForDocument(final String entry) {
1069
        if (this.hasODDocument() && (entry.equals(RootElement.CONTENT.getZipEntry()) || entry.equals(RootElement.STYLES.getZipEntry())))
1070
            throw new IllegalArgumentException("Cannot change content or styles with existing ODDocument");
1071
    }
1072
 
73 ilm 1073
    public final void putCopy(final ODPackageEntry entry) {
1074
        this.putCopy(entry, entry.getName());
1075
    }
1076
 
1077
    public final void putCopy(final ODPackageEntry entry, final String entryName) {
1078
        // ATTN this works because, all files are read upfront
1079
        final Object data = entry.getData();
1080
        final Object myData;
1081
        if (data instanceof byte[]) {
1082
            // assume byte[] are immutable
1083
            myData = data;
1084
        } else if (data instanceof ODSingleXMLDocument) {
1085
            myData = new ODSingleXMLDocument((ODSingleXMLDocument) data, this);
1086
        } else {
1087
            myData = CopyUtils.copy(data);
1088
        }
1089
        this.putFile(entryName, myData, entry.getType(), entry.isCompressed());
1090
    }
1091
 
17 ilm 1092
    public void rmFile(String entry) {
25 ilm 1093
        this.checkEntryForDocument(entry);
17 ilm 1094
        this.files.remove(entry);
25 ilm 1095
        if (entry.equals(MIMETYPE_ENTRY) || subdocNames.contains(entry)) {
1096
            final Tuple3<XMLVersion, ContentTypeVersioned, XMLFormatVersion> required = this.getRequired(entry);
1097
            this.type = required == null ? null : required.get1();
1098
            this.version = required == null ? null : required.get2();
1099
        }
17 ilm 1100
    }
1101
 
73 ilm 1102
    public final void rmFiles(Collection<String> entries) {
1103
        for (final String entry : entries)
1104
            this.rmFile(entry);
1105
    }
1106
 
25 ilm 1107
    public void clear() {
1108
        this.files.clear();
1109
        this.type = null;
1110
        this.version = null;
1111
    }
1112
 
17 ilm 1113
    /**
1114
     * Transform this to use a {@link ODSingleXMLDocument}. Ie after this method, only "content.xml"
1115
     * remains and it's an instance of ODSingleXMLDocument.
1116
     *
1117
     * @return the created ODSingleXMLDocument.
1118
     */
1119
    public ODSingleXMLDocument toSingle() {
1120
        if (!this.isSingle()) {
61 ilm 1121
            this.meta = null;
25 ilm 1122
            return ODSingleXMLDocument.create(this);
61 ilm 1123
        } else {
17 ilm 1124
            return (ODSingleXMLDocument) this.getContent();
61 ilm 1125
        }
17 ilm 1126
    }
1127
 
1128
    public final boolean isSingle() {
1129
        return this.getContent() instanceof ODSingleXMLDocument;
1130
    }
1131
 
1132
    /**
1133
     * Split the {@link RootElement#SINGLE_CONTENT}. If this was {@link #isSingle() single} the
1134
     * former {@link #getContent() content} won't be useable anymore, you can check it with
1135
     * {@link ODSingleXMLDocument#isDead()}.
1136
     *
1137
     * @return <code>true</code> if this was modified.
1138
     */
1139
    public final boolean split() {
1140
        final boolean res;
1141
        if (this.isSingle()) {
61 ilm 1142
            // store now, as split() empties us
1143
            final XMLFormatVersion version = getFormatVersion();
17 ilm 1144
            final Map<RootElement, Document> split = ((ODSingleXMLDocument) this.getContent()).split();
1145
            // from 22.2.1 (D1.1.2) of OpenDocument-v1.2-part1-cd04
1146
            assert (split.containsKey(RootElement.CONTENT) || split.containsKey(RootElement.STYLES)) && RootElement.getPackageElements().containsAll(split.keySet()) : "wrong elements " + split;
1147
            for (final Entry<RootElement, Document> e : split.entrySet()) {
1148
                this.putFile(e.getKey().getZipEntry(), new ODXMLDocument(e.getValue(), version));
1149
            }
61 ilm 1150
            this.meta = null;
17 ilm 1151
            res = true;
1152
        } else {
1153
            res = false;
1154
        }
1155
        assert !this.isSingle();
1156
        return res;
1157
    }
1158
 
1159
    // *** save
1160
 
80 ilm 1161
    private final Manifest createManifest() {
1162
        try {
1163
            return this.createManifest(null);
1164
        } catch (IOException e) {
1165
            // shouldn't happen since we're not writing
1166
            throw new IllegalStateException(e);
1167
        }
1168
    }
1169
 
1170
    private final Manifest createManifest(final Zip z) throws IOException {
1171
        final Manifest manifest = new Manifest(this.getFormatVersion(), this.getMimeType());
1172
        final XMLOutputter outputter = z == null ? null : createOutputter();
1173
        for (final String name : this.files.keySet()) {
1174
            // added at the end
1175
            if (name.equals(MIMETYPE_ENTRY) || name.equals(Manifest.ENTRY_NAME))
1176
                continue;
1177
 
1178
            final ODPackageEntry entry = this.files.get(name);
1179
            if (z != null) {
1180
                final Object val = entry.getData();
1181
                if (val != null) {
1182
                    if (val instanceof ODXMLDocument) {
180 ilm 1183
                        try (final OutputStream o = z.createEntryStream(name)) {
1184
                            outputter.output(((ODXMLDocument) val).getDocument(), o);
1185
                        }
80 ilm 1186
                    } else if (val instanceof Document) {
180 ilm 1187
                        try (final OutputStream o = z.createEntryStream(name)) {
1188
                            outputter.output((Document) val, o);
1189
                        }
80 ilm 1190
                    } else {
1191
                        z.zip(name, (byte[]) val, entry.isCompressed());
1192
                    }
1193
                }
1194
            }
1195
            final String mediaType = entry.getType();
1196
            manifest.addEntry(name, mediaType == null ? "" : mediaType);
1197
        }
1198
 
1199
        return manifest;
1200
    }
1201
 
83 ilm 1202
    /**
1203
     * Save this package to the passed stream.
1204
     *
1205
     * @param out the stream to write to, it will be closed.
1206
     * @throws IOException if an error occurs.
1207
     */
17 ilm 1208
    public final void save(OutputStream out) throws IOException {
1209
        // from 22.2.1 (D1.2)
1210
        if (this.isSingle()) {
1211
            // assert we can use this copy constructor (instead of the slower CopyUtils)
1212
            assert this.getClass() == ODPackage.class;
1213
            final ODPackage copy = new ODPackage(this);
1214
            copy.split();
1215
            copy.save(out);
1216
            return;
1217
        }
1218
 
61 ilm 1219
        // set the generator
1220
        ProductInfo productInfo = ProductInfo.getInstance();
1221
        if (productInfo == null) {
1222
            // do *not* use "/product.properties" as it might interfere with products using this
1223
            // framework
144 ilm 1224
            Properties props = PropertiesUtils.createFromResource(this.getClass(), "product.properties");
1225
            if (props == null) {
1226
                Log.get().warning("Neither ProductInfo singleton nor product.properties for " + this.getClass());
1227
                props = new Properties();
1228
            }
61 ilm 1229
            props.put(ProductInfo.NAME, this.getClass().getName());
1230
            productInfo = new ProductInfo(props);
1231
        }
1232
        final String generator;
1233
        if (productInfo.getVersion() == null)
1234
            generator = productInfo.getName();
1235
        else
1236
            generator = productInfo.getName() + "/" + productInfo.getVersion();
1237
        this.getMeta(true).setGenerator(generator);
73 ilm 1238
        // we could update almost all statistics (table count, paragraph count, ...) but the most
1239
        // important one for opening times is page count
1240
        this.getMeta().removeMetaChild("document-statistic");
1241
        final String pageCount = getPageCount();
1242
        if (pageCount != null && getContentType() != null && ContentType.TEXT.equals(getContentType().getType()))
1243
            this.getMeta().getMetaChild("document-statistic").setAttribute("page-count", pageCount, getVersion().getMETA());
61 ilm 1244
 
180 ilm 1245
        try (final Zip z = new Zip(out)) {
1246
            // magic number, see section 17.4
1247
            z.zipNonCompressed(MIMETYPE_ENTRY, this.getMimeType().getBytes(MIMETYPE_ENC));
17 ilm 1248
 
180 ilm 1249
            final Manifest manifest = createManifest(z);
17 ilm 1250
 
180 ilm 1251
            z.zip(Manifest.ENTRY_NAME, new StringInputStream(manifest.asString()));
1252
        }
17 ilm 1253
    }
1254
 
1255
    /**
1256
     * Save the content of this package to our file, overwriting it if it exists.
1257
     *
1258
     * @return the saved file.
1259
     * @throws IOException if an error occurs while saving.
1260
     */
1261
    public File save() throws IOException {
1262
        return this.saveAs(this.getFile());
1263
    }
1264
 
1265
    public File saveAs(final File fNoExt) throws IOException {
1266
        final File f = this.addExt(fNoExt);
1267
        if (f.getParentFile() != null)
1268
            f.getParentFile().mkdirs();
1269
        // ATTN at this point, we must have read all the content of this file
1270
        // otherwise we could save to File.createTempFile("oofd", null).deleteOnExit();
180 ilm 1271
        try (final BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(f), 512 * 1024);) {
17 ilm 1272
            this.save(bufferedOutputStream);
1273
        }
1274
        return f;
1275
    }
1276
}