OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 144 | Rev 180 | Go to most recent revision | Only display areas with differences | Regard whitespace | Details | Blame | Last modification | View Log | RSS feed

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