OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

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