OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

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

Rev 83 Rev 180
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.xml.Step.createAttributeStep;
-
 
18
import static org.openconcerto.xml.Step.createAttributeStepFromQualifiedName;
-
 
19
import static org.openconcerto.xml.Step.createElementStep;
-
 
20
import static org.openconcerto.xml.Step.createElementStepFromQualifiedName;
-
 
21
 
17
import org.openconcerto.openoffice.ODPackage.RootElement;
22
import org.openconcerto.openoffice.ODPackage.RootElement;
18
import org.openconcerto.openoffice.style.data.DataStyle;
23
import org.openconcerto.openoffice.style.data.DataStyle;
19
import org.openconcerto.utils.Base64;
24
import org.openconcerto.utils.Base64;
20
import org.openconcerto.utils.CollectionUtils;
25
import org.openconcerto.utils.CollectionUtils;
21
import org.openconcerto.utils.FileUtils;
26
import org.openconcerto.utils.FileUtils;
22
import org.openconcerto.utils.Tuple2;
27
import org.openconcerto.utils.Tuple2;
23
import org.openconcerto.utils.cc.IPredicate;
28
import org.openconcerto.utils.cc.IPredicate;
24
import org.openconcerto.xml.JDOMUtils;
29
import org.openconcerto.xml.JDOMUtils;
25
import org.openconcerto.xml.SimpleXMLPath;
30
import org.openconcerto.xml.SimpleXMLPath;
26
import org.openconcerto.xml.Step;
31
import org.openconcerto.xml.Step;
27
import org.openconcerto.xml.Step.Axis;
32
import org.openconcerto.xml.Step.Axis;
28
 
33
 
29
import java.awt.Point;
34
import java.awt.Point;
30
import java.io.File;
35
import java.io.File;
-
 
36
import java.io.FileOutputStream;
31
import java.io.IOException;
37
import java.io.IOException;
32
import java.io.InputStream;
38
import java.io.InputStream;
33
import java.io.OutputStream;
39
import java.io.OutputStream;
34
import java.math.BigDecimal;
40
import java.math.BigDecimal;
35
import java.net.URI;
41
import java.net.URI;
36
import java.net.URISyntaxException;
42
import java.net.URISyntaxException;
37
import java.util.ArrayList;
43
import java.util.ArrayList;
38
import java.util.Collection;
44
import java.util.Collection;
39
import java.util.Collections;
45
import java.util.Collections;
40
import java.util.HashMap;
46
import java.util.HashMap;
41
import java.util.HashSet;
47
import java.util.HashSet;
42
import java.util.Iterator;
48
import java.util.Iterator;
43
import java.util.List;
49
import java.util.List;
44
import java.util.ListIterator;
50
import java.util.ListIterator;
45
import java.util.Map;
51
import java.util.Map;
46
import java.util.Map.Entry;
52
import java.util.Map.Entry;
47
import java.util.Set;
53
import java.util.Set;
48
 
54
 
49
import org.apache.commons.collections.Transformer;
-
 
50
import org.jdom.Attribute;
55
import org.jdom.Attribute;
51
import org.jdom.Content;
56
import org.jdom.Content;
52
import org.jdom.DocType;
57
import org.jdom.DocType;
53
import org.jdom.Document;
58
import org.jdom.Document;
54
import org.jdom.Element;
59
import org.jdom.Element;
55
import org.jdom.JDOMException;
60
import org.jdom.JDOMException;
56
import org.jdom.Namespace;
61
import org.jdom.Namespace;
57
import org.jdom.xpath.XPath;
62
import org.jdom.xpath.XPath;
58
 
63
 
59
/**
64
/**
60
 * An XML document containing all of an office document, see section 2.1 of OpenDocument 1.1.
65
 * An XML document containing all of an office document, see section 2.1 of OpenDocument 1.1.
61
 * 
66
 * 
62
 * @author Sylvain CUAZ 24 nov. 2004
67
 * @author Sylvain CUAZ 24 nov. 2004
63
 */
68
 */
64
public class ODSingleXMLDocument extends ODXMLDocument implements Cloneable {
69
public class ODSingleXMLDocument extends ODXMLDocument implements Cloneable {
65
 
70
 
66
    static private enum ContentPart {
71
    static private enum ContentPart {
67
        PROLOGUE, MAIN, EPILOGUE
72
        PROLOGUE, MAIN, EPILOGUE
68
    }
73
    }
69
 
74
 
70
    private static final String BASIC_LANG_NAME = "ooo:Basic";
75
    private static final String BASIC_LANG_NAME = "ooo:Basic";
71
    private static final SimpleXMLPath<Attribute> ALL_HREF_ATTRIBUTES = SimpleXMLPath.allAttributes("href", "xlink");
76
    private static final SimpleXMLPath<Attribute> ALL_HREF_ATTRIBUTES = SimpleXMLPath.allAttributes("href", "xlink");
72
    private static final SimpleXMLPath<Element> ALL_BINARY_DATA_ELEMENTS = SimpleXMLPath.allElements("binary-data", "office");
77
    private static final SimpleXMLPath<Element> ALL_BINARY_DATA_ELEMENTS = SimpleXMLPath.allElements("binary-data", "office");
73
    // see 10.4.5 <office:binary-data> of OpenDocument-v1.2-os
78
    // see 10.4.5 <office:binary-data> of OpenDocument-v1.2-os
74
    private static final Set<String> BINARY_DATA_PARENTS = CollectionUtils.createSet("draw:image", "draw:object-ole", "style:background-image", "text:list-level-style-image");
79
    private static final Set<String> BINARY_DATA_PARENTS = CollectionUtils.createSet("draw:image", "draw:object-ole", "style:background-image", "text:list-level-style-image");
75
 
80
 
76
    final static Set<String> DONT_PREFIX;
81
    final static Set<String> DONT_PREFIX;
77
    static private final Map<XMLVersion, List<Set<Element>>> ELEMS_ORDER;
82
    static private final Map<XMLVersion, List<Set<Element>>> ELEMS_ORDER;
78
    static private final Map<XMLVersion, Map<Tuple2<Namespace, String>, ContentPart>> ELEMS_PARTS;
83
    static private final Map<XMLVersion, Map<Tuple2<Namespace, String>, ContentPart>> ELEMS_PARTS;
79
    static {
84
    static {
80
        DONT_PREFIX = new HashSet<String>();
85
        DONT_PREFIX = new HashSet<String>();
81
        // don't touch to user fields and variables
86
        // don't touch to user fields and variables
82
        // we want them to be the same across the document
87
        // we want them to be the same across the document
83
        DONT_PREFIX.add("user-field-decl");
88
        DONT_PREFIX.add("user-field-decl");
84
        DONT_PREFIX.add("user-field-get");
89
        DONT_PREFIX.add("user-field-get");
85
        DONT_PREFIX.add("variable-get");
90
        DONT_PREFIX.add("variable-get");
86
        DONT_PREFIX.add("variable-decl");
91
        DONT_PREFIX.add("variable-decl");
87
        DONT_PREFIX.add("variable-set");
92
        DONT_PREFIX.add("variable-set");
88
 
93
 
89
        final XMLVersion[] versions = XMLVersion.values();
94
        final XMLVersion[] versions = XMLVersion.values();
90
        ELEMS_ORDER = new HashMap<XMLVersion, List<Set<Element>>>(versions.length);
95
        ELEMS_ORDER = new HashMap<XMLVersion, List<Set<Element>>>(versions.length);
91
        ELEMS_PARTS = new HashMap<XMLVersion, Map<Tuple2<Namespace, String>, ContentPart>>(versions.length);
96
        ELEMS_PARTS = new HashMap<XMLVersion, Map<Tuple2<Namespace, String>, ContentPart>>(versions.length);
92
        for (final XMLVersion v : versions) {
97
        for (final XMLVersion v : versions) {
93
            // some elements can only appear in some types but since we have the null wild card,
98
            // some elements can only appear in some types but since we have the null wild card,
94
            // they'd match that. So always include all elements.
99
            // they'd match that. So always include all elements.
95
            final List<Set<Element>> children = createChildren(v, null);
100
            final List<Set<Element>> children = createChildren(v, null);
96
 
101
 
97
            ELEMS_ORDER.put(v, children);
102
            ELEMS_ORDER.put(v, children);
98
 
103
 
99
            final Map<Tuple2<Namespace, String>, ContentPart> m = new HashMap<Tuple2<Namespace, String>, ODSingleXMLDocument.ContentPart>(ContentPart.values().length);
104
            final Map<Tuple2<Namespace, String>, ContentPart> m = new HashMap<Tuple2<Namespace, String>, ODSingleXMLDocument.ContentPart>(ContentPart.values().length);
100
            boolean beforeNull = true;
105
            boolean beforeNull = true;
101
            for (final Set<Element> s : children) {
106
            for (final Set<Element> s : children) {
102
                if (s == null) {
107
                if (s == null) {
103
                    m.put(null, ContentPart.MAIN);
108
                    m.put(null, ContentPart.MAIN);
104
                    assert beforeNull : "more than one null";
109
                    assert beforeNull : "more than one null";
105
                    beforeNull = false;
110
                    beforeNull = false;
106
                } else {
111
                } else {
107
                    for (final Element elem : s)
112
                    for (final Element elem : s)
108
                        m.put(Tuple2.create(elem.getNamespace(), elem.getName()), beforeNull ? ContentPart.PROLOGUE : ContentPart.EPILOGUE);
113
                        m.put(Tuple2.create(elem.getNamespace(), elem.getName()), beforeNull ? ContentPart.PROLOGUE : ContentPart.EPILOGUE);
109
                }
114
                }
110
            }
115
            }
111
            ELEMS_PARTS.put(v, m);
116
            ELEMS_PARTS.put(v, m);
112
        }
117
        }
113
    }
118
    }
114
 
119
 
115
    private static final List<Set<Element>> createChildren(XMLVersion v, ContentType t) {
120
    private static final List<Set<Element>> createChildren(XMLVersion v, ContentType t) {
116
        final Namespace textNS = v.getTEXT();
121
        final Namespace textNS = v.getTEXT();
117
        final Namespace tableNS = v.getTABLE();
122
        final Namespace tableNS = v.getTABLE();
118
        final List<Set<Element>> res = new ArrayList<Set<Element>>(24);
123
        final List<Set<Element>> res = new ArrayList<Set<Element>>(24);
119
 
124
 
120
        if (t == null || t == ContentType.TEXT)
125
        if (t == null || t == ContentType.TEXT)
121
            res.add(Collections.singleton(new Element("forms", v.getOFFICE())));
126
            res.add(Collections.singleton(new Element("forms", v.getOFFICE())));
122
        // first only for text, second only for spreadsheet
127
        // first only for text, second only for spreadsheet
123
        final Element textTrackedChanges = new Element("tracked-changes", textNS);
128
        final Element textTrackedChanges = new Element("tracked-changes", textNS);
124
        final Element tableTrackedChanges = new Element("tracked-changes", tableNS);
129
        final Element tableTrackedChanges = new Element("tracked-changes", tableNS);
125
        if (t == null)
130
        if (t == null)
126
            res.add(CollectionUtils.createSet(textTrackedChanges, tableTrackedChanges));
131
            res.add(CollectionUtils.createSet(textTrackedChanges, tableTrackedChanges));
127
        else if (t == ContentType.TEXT)
132
        else if (t == ContentType.TEXT)
128
            res.add(Collections.singleton(textTrackedChanges));
133
            res.add(Collections.singleton(textTrackedChanges));
129
        else if (t == ContentType.SPREADSHEET)
134
        else if (t == ContentType.SPREADSHEET)
130
            res.add(Collections.singleton(tableTrackedChanges));
135
            res.add(Collections.singleton(tableTrackedChanges));
131
 
136
 
132
        // text-decls
137
        // text-decls
133
        res.add(Collections.singleton(new Element("variable-decls", textNS)));
138
        res.add(Collections.singleton(new Element("variable-decls", textNS)));
134
        res.add(Collections.singleton(new Element("sequence-decls", textNS)));
139
        res.add(Collections.singleton(new Element("sequence-decls", textNS)));
135
        res.add(Collections.singleton(new Element("user-field-decls", textNS)));
140
        res.add(Collections.singleton(new Element("user-field-decls", textNS)));
136
        res.add(Collections.singleton(new Element("dde-connection-decls", textNS)));
141
        res.add(Collections.singleton(new Element("dde-connection-decls", textNS)));
137
        res.add(Collections.singleton(new Element("alphabetical-index-auto-mark-file", textNS)));
142
        res.add(Collections.singleton(new Element("alphabetical-index-auto-mark-file", textNS)));
138
 
143
 
139
        // table-decls
144
        // table-decls
140
        res.add(Collections.singleton(new Element("calculation-settings", tableNS)));
145
        res.add(Collections.singleton(new Element("calculation-settings", tableNS)));
141
        res.add(Collections.singleton(new Element("content-validations", tableNS)));
146
        res.add(Collections.singleton(new Element("content-validations", tableNS)));
142
        res.add(Collections.singleton(new Element("label-ranges", tableNS)));
147
        res.add(Collections.singleton(new Element("label-ranges", tableNS)));
143
 
148
 
144
        if (v == XMLVersion.OD && (t == null || t == ContentType.PRESENTATION)) {
149
        if (v == XMLVersion.OD && (t == null || t == ContentType.PRESENTATION)) {
145
            // perhaps add presentation-decls (needs new namespace in XMLVersion)
150
            // perhaps add presentation-decls (needs new namespace in XMLVersion)
146
        }
151
        }
147
 
152
 
148
        // main content
153
        // main content
149
        res.add(null);
154
        res.add(null);
150
 
155
 
151
        if (v == XMLVersion.OD && (t == null || t == ContentType.PRESENTATION)) {
156
        if (v == XMLVersion.OD && (t == null || t == ContentType.PRESENTATION)) {
152
            // perhaps add presentation:settings
157
            // perhaps add presentation:settings
153
        }
158
        }
154
 
159
 
155
        // table-functions
160
        // table-functions
156
        res.add(Collections.singleton(new Element("named-expressions", tableNS)));
161
        res.add(Collections.singleton(new Element("named-expressions", tableNS)));
157
        res.add(Collections.singleton(new Element("database-ranges", tableNS)));
162
        res.add(Collections.singleton(new Element("database-ranges", tableNS)));
158
        res.add(Collections.singleton(new Element("data-pilot-tables", tableNS)));
163
        res.add(Collections.singleton(new Element("data-pilot-tables", tableNS)));
159
        res.add(Collections.singleton(new Element("consolidation", tableNS)));
164
        res.add(Collections.singleton(new Element("consolidation", tableNS)));
160
        res.add(Collections.singleton(new Element("dde-links", tableNS)));
165
        res.add(Collections.singleton(new Element("dde-links", tableNS)));
161
 
166
 
162
        if (v == XMLVersion.OOo && (t == null || t == ContentType.PRESENTATION)) {
167
        if (v == XMLVersion.OOo && (t == null || t == ContentType.PRESENTATION)) {
163
            // perhaps add presentation:settings
168
            // perhaps add presentation:settings
164
        }
169
        }
165
 
170
 
166
        return res;
171
        return res;
167
    }
172
    }
168
 
173
 
169
    // return null if not an element
174
    // return null if not an element
170
    // return the part the element is in (assume MAIN for unknown elements)
175
    // return the part the element is in (assume MAIN for unknown elements)
171
    static private ContentPart getPart(final Map<Tuple2<Namespace, String>, ContentPart> parts, final Content bodyContent) {
176
    static private ContentPart getPart(final Map<Tuple2<Namespace, String>, ContentPart> parts, final Content bodyContent) {
172
        if (!(bodyContent instanceof Element))
177
        if (!(bodyContent instanceof Element))
173
            return null;
178
            return null;
174
        final Element elem = (Element) bodyContent;
179
        final Element elem = (Element) bodyContent;
175
        ContentPart res = parts.get(Tuple2.create(elem.getNamespace(), elem.getName()));
180
        ContentPart res = parts.get(Tuple2.create(elem.getNamespace(), elem.getName()));
176
        if (res == null)
181
        if (res == null)
177
            res = parts.get(null);
182
            res = parts.get(null);
178
        assert res != null;
183
        assert res != null;
179
        return res;
184
        return res;
180
    }
185
    }
181
 
186
 
182
    static private int[] getLastNulls(final Map<Tuple2<Namespace, String>, ContentPart> parts, final Element body) {
187
    static private int[] getLastNulls(final Map<Tuple2<Namespace, String>, ContentPart> parts, final Element body) {
183
        @SuppressWarnings("unchecked")
188
        @SuppressWarnings("unchecked")
184
        final List<Content> content = body.getContent();
189
        final List<Content> content = body.getContent();
185
        return getLastNulls(parts, content, content.size());
190
        return getLastNulls(parts, content, content.size());
186
    }
191
    }
187
 
192
 
188
    // return the start of the EPILOGUE, at 0 with non-elements (i.e. having a null ContentPart), at
193
    // return the start of the EPILOGUE, at 0 with non-elements (i.e. having a null ContentPart), at
189
    // 1 the first EPILOGUE element
194
    // 1 the first EPILOGUE element
190
    static private int[] getLastNulls(final Map<Tuple2<Namespace, String>, ContentPart> parts, final List<Content> content, final int contentSize) {
195
    static private int[] getLastNulls(final Map<Tuple2<Namespace, String>, ContentPart> parts, final List<Content> content, final int contentSize) {
191
        // start from the end until we leave the epilogue (quicker than traversing the main part as
196
        // start from the end until we leave the epilogue (quicker than traversing the main part as
192
        // prologue and epilogue sizes are bounded and small)
197
        // prologue and epilogue sizes are bounded and small)
193
        ContentPart contentPart = null;
198
        ContentPart contentPart = null;
194
        final ListIterator<Content> thisChildrenIter = content.listIterator(contentSize);
199
        final ListIterator<Content> thisChildrenIter = content.listIterator(contentSize);
195
        int nullsStartIndex = -1;
200
        int nullsStartIndex = -1;
196
        while ((contentPart == null || contentPart == ContentPart.EPILOGUE) && thisChildrenIter.hasPrevious()) {
201
        while ((contentPart == null || contentPart == ContentPart.EPILOGUE) && thisChildrenIter.hasPrevious()) {
197
            contentPart = getPart(parts, thisChildrenIter.previous());
202
            contentPart = getPart(parts, thisChildrenIter.previous());
198
            if (contentPart != null) {
203
            if (contentPart != null) {
199
                nullsStartIndex = -1;
204
                nullsStartIndex = -1;
200
            } else if (nullsStartIndex < 0) {
205
            } else if (nullsStartIndex < 0) {
201
                nullsStartIndex = thisChildrenIter.nextIndex();
206
                nullsStartIndex = thisChildrenIter.nextIndex();
202
            }
207
            }
203
        }
208
        }
204
        final int lastNullsStart = contentPart == null || contentPart == ContentPart.EPILOGUE ? thisChildrenIter.nextIndex() : thisChildrenIter.nextIndex() + 1;
209
        final int lastNullsStart = contentPart == null || contentPart == ContentPart.EPILOGUE ? thisChildrenIter.nextIndex() : thisChildrenIter.nextIndex() + 1;
205
        final int lastNullsEnd = nullsStartIndex < 0 ? lastNullsStart : nullsStartIndex + 1;
210
        final int lastNullsEnd = nullsStartIndex < 0 ? lastNullsStart : nullsStartIndex + 1;
206
        return new int[] { lastNullsStart, lastNullsEnd };
211
        return new int[] { lastNullsStart, lastNullsEnd };
207
    }
212
    }
208
 
213
 
209
    /**
214
    /**
210
     * Slice the body into parts. Since some content have no part (e.g. comment), they can be added
215
     * Slice the body into parts. Since some content have no part (e.g. comment), they can be added
211
     * to the previous or next range. If <code>overlapping</code> is <code>true</code> they will be
216
     * to the previous or next range. If <code>overlapping</code> is <code>true</code> they will be
212
     * added to both, else only to the next range.
217
     * added to both, else only to the next range.
213
     * 
218
     * 
214
     * @param parts parts definition.
219
     * @param parts parts definition.
215
     * @param body the element to slice.
220
     * @param body the element to slice.
216
     * @param overlapping <code>true</code> if ranges can overlap.
221
     * @param overlapping <code>true</code> if ranges can overlap.
217
     * @return the start (inclusive, {@link Point#x}) and end (exclusive, {@link Point#y}) for each
222
     * @return the start (inclusive, {@link Point#x}) and end (exclusive, {@link Point#y}) for each
218
     *         {@link ContentPart}.
223
     *         {@link ContentPart}.
219
     */
224
     */
220
    static private Point[] getBounds(final Map<Tuple2<Namespace, String>, ContentPart> parts, final Element body, final boolean overlapping) {
225
    static private Point[] getBounds(final Map<Tuple2<Namespace, String>, ContentPart> parts, final Element body, final boolean overlapping) {
221
        @SuppressWarnings("unchecked")
226
        @SuppressWarnings("unchecked")
222
        final List<Content> content = body.getContent();
227
        final List<Content> content = body.getContent();
223
        final int contentSize = content.size();
228
        final int contentSize = content.size();
224
        if (contentSize == 0)
229
        if (contentSize == 0)
225
            return new Point[] { new Point(0, 0), new Point(0, 0), new Point(0, 0) };
230
            return new Point[] { new Point(0, 0), new Point(0, 0), new Point(0, 0) };
226
 
231
 
227
        // start from the beginning until we leave the prologue
232
        // start from the beginning until we leave the prologue
228
        ContentPart contentPart = null;
233
        ContentPart contentPart = null;
229
        ListIterator<Content> thisChildrenIter = content.listIterator(0);
234
        ListIterator<Content> thisChildrenIter = content.listIterator(0);
230
        final int prologueStart = 0;
235
        final int prologueStart = 0;
231
        int nullsStartIndex = -1;
236
        int nullsStartIndex = -1;
232
        while ((contentPart == null || contentPart == ContentPart.PROLOGUE) && thisChildrenIter.hasNext()) {
237
        while ((contentPart == null || contentPart == ContentPart.PROLOGUE) && thisChildrenIter.hasNext()) {
233
            contentPart = getPart(parts, thisChildrenIter.next());
238
            contentPart = getPart(parts, thisChildrenIter.next());
234
            if (contentPart != null) {
239
            if (contentPart != null) {
235
                nullsStartIndex = -1;
240
                nullsStartIndex = -1;
236
            } else if (nullsStartIndex < 0) {
241
            } else if (nullsStartIndex < 0) {
237
                nullsStartIndex = thisChildrenIter.previousIndex();
242
                nullsStartIndex = thisChildrenIter.previousIndex();
238
            }
243
            }
239
        }
244
        }
240
        final int nullsEnd = contentPart == null || contentPart == ContentPart.PROLOGUE ? thisChildrenIter.nextIndex() : thisChildrenIter.previousIndex();
245
        final int nullsEnd = contentPart == null || contentPart == ContentPart.PROLOGUE ? thisChildrenIter.nextIndex() : thisChildrenIter.previousIndex();
241
        final int nullsStart = nullsStartIndex < 0 ? nullsEnd : nullsStartIndex;
246
        final int nullsStart = nullsStartIndex < 0 ? nullsEnd : nullsStartIndex;
242
        assert nullsStart >= 0 && nullsStart <= nullsEnd;
247
        assert nullsStart >= 0 && nullsStart <= nullsEnd;
243
        final int mainStart = nullsStart;
248
        final int mainStart = nullsStart;
244
        final int prologueStop = overlapping ? nullsEnd : nullsStart;
249
        final int prologueStop = overlapping ? nullsEnd : nullsStart;
245
 
250
 
246
        final int epilogueEnd = contentSize;
251
        final int epilogueEnd = contentSize;
247
        final int[] lastNulls = getLastNulls(parts, content, contentSize);
252
        final int[] lastNulls = getLastNulls(parts, content, contentSize);
248
        final int lastNullsStart = lastNulls[0];
253
        final int lastNullsStart = lastNulls[0];
249
        final int lastNullsEnd = lastNulls[1];
254
        final int lastNullsEnd = lastNulls[1];
250
        assert lastNullsStart >= mainStart && lastNullsStart <= lastNullsEnd;
255
        assert lastNullsStart >= mainStart && lastNullsStart <= lastNullsEnd;
251
        final int epilogueStart = lastNullsStart;
256
        final int epilogueStart = lastNullsStart;
252
        final int mainEnd = overlapping ? lastNullsEnd : lastNullsStart;
257
        final int mainEnd = overlapping ? lastNullsEnd : lastNullsStart;
253
 
258
 
254
        final Point[] res = new Point[] { new Point(prologueStart, prologueStop), new Point(mainStart, mainEnd), new Point(epilogueStart, epilogueEnd) };
259
        final Point[] res = new Point[] { new Point(prologueStart, prologueStop), new Point(mainStart, mainEnd), new Point(epilogueStart, epilogueEnd) };
255
        assert res.length == ContentPart.values().length;
260
        assert res.length == ContentPart.values().length;
256
        return res;
261
        return res;
257
    }
262
    }
258
 
263
 
259
    static private int getValidIndex(final Map<Tuple2<Namespace, String>, ContentPart> parts, final Element body, final int index) {
264
    static private int getValidIndex(final Map<Tuple2<Namespace, String>, ContentPart> parts, final Element body, final int index) {
260
        // overlapping ranges to have longest main part possible and thus avoid changing index
265
        // overlapping ranges to have longest main part possible and thus avoid changing index
261
        final Point[] bounds = getBounds(parts, body, true);
266
        final Point[] bounds = getBounds(parts, body, true);
262
        final Point mainBounds = bounds[ContentPart.MAIN.ordinal()];
267
        final Point mainBounds = bounds[ContentPart.MAIN.ordinal()];
263
 
268
 
264
        final int mainEnd = mainBounds.y;
269
        final int mainEnd = mainBounds.y;
265
        if (index < 0 || index > mainEnd)
270
        if (index < 0 || index > mainEnd)
266
            return mainEnd;
271
            return mainEnd;
267
 
272
 
268
        final int mainStart = mainBounds.x;
273
        final int mainStart = mainBounds.x;
269
        if (index < mainStart)
274
        if (index < mainStart)
270
            return mainStart;
275
            return mainStart;
271
 
276
 
272
        return index;
277
        return index;
273
    }
278
    }
274
 
279
 
275
    // Voir le TODO du ctor
280
    // Voir le TODO du ctor
276
    // public static OOSingleXMLDocument createEmpty() {
281
    // public static OOSingleXMLDocument createEmpty() {
277
    // }
282
    // }
278
 
283
 
279
    /**
284
    /**
280
     * Create a document from a collection of subdocuments.
285
     * Create a document from a collection of subdocuments.
281
     * 
286
     * 
282
     * @param content the content.
287
     * @param content the content.
283
     * @param style the styles, can be <code>null</code>.
288
     * @param style the styles, can be <code>null</code>.
284
     * @return the merged document.
289
     * @return the merged document.
285
     */
290
     */
286
    public static ODSingleXMLDocument createFromDocument(Document content, Document style) {
291
    public static ODSingleXMLDocument createFromDocument(Document content, Document style) {
287
        return ODPackage.createFromDocuments(content, style).toSingle();
292
        return ODPackage.createFromDocuments(content, style).toSingle();
288
    }
293
    }
289
 
294
 
290
    static ODSingleXMLDocument create(ODPackage files) {
295
    static ODSingleXMLDocument create(ODPackage files) {
291
        final Document content = files.getContent().getDocument();
296
        final Document content = files.getContent().getDocument();
292
        final Document style = files.getDocument(RootElement.STYLES.getZipEntry());
297
        final Document style = files.getDocument(RootElement.STYLES.getZipEntry());
293
        // signal that the xml is a complete document (was document-content)
298
        // signal that the xml is a complete document (was document-content)
294
        final Document singleContent = RootElement.createSingle(content);
299
        final Document singleContent = RootElement.createSingle(content);
295
        copyNS(content, singleContent);
300
        copyNS(content, singleContent);
296
        files.getContentType().setType(singleContent);
301
        files.getContentType().setType(singleContent);
297
        final Element root = singleContent.getRootElement();
302
        final Element root = singleContent.getRootElement();
298
        root.addContent(content.getRootElement().removeContent());
303
        root.addContent(content.getRootElement().removeContent());
299
        // see section 2.1.1 first meta, then settings, then the rest
304
        // see section 2.1.1 first meta, then settings, then the rest
300
        createScriptsElement(root, files);
305
        createScriptsElement(root, files);
301
        prependToRoot(files.getDocument(RootElement.SETTINGS.getZipEntry()), root);
306
        prependToRoot(files.getDocument(RootElement.SETTINGS.getZipEntry()), root);
302
        prependToRoot(files.getDocument(RootElement.META.getZipEntry()), root);
307
        prependToRoot(files.getDocument(RootElement.META.getZipEntry()), root);
303
        final ODSingleXMLDocument single = new ODSingleXMLDocument(singleContent, files);
308
        final ODSingleXMLDocument single = new ODSingleXMLDocument(singleContent, files);
304
        if (single.getChild("body") == null)
309
        if (single.getChild("body") == null)
305
            throw new IllegalArgumentException("no body in " + single);
310
            throw new IllegalArgumentException("no body in " + single);
306
        if (style != null) {
311
        if (style != null) {
307
            // section 2.1 : Styles used in the document content and automatic styles used in the
312
            // section 2.1 : Styles used in the document content and automatic styles used in the
308
            // styles themselves.
313
            // styles themselves.
309
            // more precisely in section 2.1.1 : office:document-styles contains style, master
314
            // more precisely in section 2.1.1 : office:document-styles contains style, master
310
            // style, auto style, font decls ; the last two being also in content.xml but are *not*
315
            // style, auto style, font decls ; the last two being also in content.xml but are *not*
311
            // related : eg P1 of styles.xml is *not* the P1 of content.xml
316
            // related : eg P1 of styles.xml is *not* the P1 of content.xml
312
            try {
317
            try {
313
                single.mergeAllStyles(new ODXMLDocument(style), true);
318
                single.mergeAllStyles(new ODXMLDocument(style), true);
314
            } catch (JDOMException e) {
319
            } catch (JDOMException e) {
315
                throw new IllegalArgumentException("style is not valid", e);
320
                throw new IllegalArgumentException("style is not valid", e);
316
            }
321
            }
317
        }
322
        }
318
        return single;
323
        return single;
319
    }
324
    }
320
 
325
 
321
    private static void createScriptsElement(final Element root, final ODPackage pkg) {
326
    private static void createScriptsElement(final Element root, final ODPackage pkg) {
322
        final Map<String, Library> basicLibraries = pkg.readBasicLibraries();
327
        final Map<String, Library> basicLibraries = pkg.readBasicLibraries();
323
        if (basicLibraries.size() > 0) {
328
        if (basicLibraries.size() > 0) {
324
            final XMLFormatVersion formatVersion = pkg.getFormatVersion();
329
            final XMLFormatVersion formatVersion = pkg.getFormatVersion();
325
            final XMLVersion version = formatVersion.getXMLVersion();
330
            final XMLVersion version = formatVersion.getXMLVersion();
326
            final Namespace officeNS = version.getOFFICE();
331
            final Namespace officeNS = version.getOFFICE();
327
 
332
 
328
            // scripts must be before the body and automatic styles
333
            // scripts must be before the body and automatic styles
329
            final Element scriptsElem = JDOMUtils.getOrCreateChild(root, formatVersion.getXML().getOfficeScripts(), officeNS, 0);
334
            final Element scriptsElem = JDOMUtils.getOrCreateChild(root, formatVersion.getXML().getOfficeScripts(), officeNS, 0);
330
            final Element scriptElem = new Element(formatVersion.getXML().getOfficeScript(), officeNS);
335
            final Element scriptElem = new Element(formatVersion.getXML().getOfficeScript(), officeNS);
331
            scriptElem.setAttribute("language", BASIC_LANG_NAME, version.getNS("script"));
336
            scriptElem.setAttribute("language", BASIC_LANG_NAME, version.getNS("script"));
332
            // script must be before events
337
            // script must be before events
333
            scriptsElem.addContent(0, scriptElem);
338
            scriptsElem.addContent(0, scriptElem);
334
 
339
 
335
            final Element libsElem = new Element("libraries", version.getLibrariesNS());
340
            final Element libsElem = new Element("libraries", version.getLibrariesNS());
336
            for (final Library lib : basicLibraries.values()) {
341
            for (final Library lib : basicLibraries.values()) {
337
                libsElem.addContent(lib.toFlatXML(formatVersion));
342
                libsElem.addContent(lib.toFlatXML(formatVersion));
338
            }
343
            }
339
            scriptElem.addContent(libsElem);
344
            scriptElem.addContent(libsElem);
340
        }
345
        }
341
    }
346
    }
342
 
347
 
343
    private static void prependToRoot(Document settings, final Element root) {
348
    private static void prependToRoot(Document settings, final Element root) {
344
        if (settings != null) {
349
        if (settings != null) {
345
            copyNS(settings, root.getDocument());
350
            copyNS(settings, root.getDocument());
346
            final Element officeSettings = (Element) settings.getRootElement().getChildren().get(0);
351
            final Element officeSettings = (Element) settings.getRootElement().getChildren().get(0);
347
            root.addContent(0, (Element) officeSettings.clone());
352
            root.addContent(0, (Element) officeSettings.clone());
348
        }
353
        }
349
    }
354
    }
350
 
355
 
351
    // some namespaces are needed even if not directly used, see § 18.3.19 namespacedToken
356
    // some namespaces are needed even if not directly used, see § 18.3.19 namespacedToken
352
    // of v1.2-part1-cd04 (e.g. 19.31 config:name or 19.260 form:control-implementation)
357
    // of v1.2-part1-cd04 (e.g. 19.31 config:name or 19.260 form:control-implementation)
353
    @SuppressWarnings("unchecked")
358
    @SuppressWarnings("unchecked")
354
    private static void copyNS(final Document src, final Document dest) {
359
    private static void copyNS(final Document src, final Document dest) {
355
        JDOMUtils.addNamespaces(dest.getRootElement(), src.getRootElement().getAdditionalNamespaces());
360
        JDOMUtils.addNamespaces(dest.getRootElement(), src.getRootElement().getAdditionalNamespaces());
356
    }
361
    }
357
 
362
 
358
    /**
363
    /**
359
     * Create a document from a package.
364
     * Create a document from a package.
360
     * 
365
     * 
361
     * @param f an OpenDocument package file.
366
     * @param f an OpenDocument package file.
362
     * @return the merged file.
367
     * @return the merged file.
363
     * @throws JDOMException if the file is not a valid OpenDocument file.
368
     * @throws JDOMException if the file is not a valid OpenDocument file.
364
     * @throws IOException if the file can't be read.
369
     * @throws IOException if the file can't be read.
365
     */
370
     */
366
    public static ODSingleXMLDocument createFromPackage(File f) throws JDOMException, IOException {
371
    public static ODSingleXMLDocument createFromPackage(File f) throws JDOMException, IOException {
367
        // this loads all linked files
372
        // this loads all linked files
368
        return new ODPackage(f).toSingle();
373
        return new ODPackage(f).toSingle();
369
    }
374
    }
370
 
375
 
371
    public static ODSingleXMLDocument createFromPackage(InputStream in) throws IOException {
376
    public static ODSingleXMLDocument createFromPackage(InputStream in) throws IOException {
372
        // this loads all linked files
377
        // this loads all linked files
373
        return new ODPackage(in).toSingle();
378
        return new ODPackage(in).toSingle();
374
    }
379
    }
375
 
380
 
376
    /**
381
    /**
377
     * Create a document from a flat XML.
382
     * Create a document from a flat XML.
378
     * 
383
     * 
379
     * @param f an OpenDocument XML file.
384
     * @param f an OpenDocument XML file.
380
     * @return the created file.
385
     * @return the created file.
381
     * @throws JDOMException if the file is not a valid OpenDocument file.
386
     * @throws JDOMException if the file is not a valid OpenDocument file.
382
     * @throws IOException if the file can't be read.
387
     * @throws IOException if the file can't be read.
383
     */
388
     */
384
    public static ODSingleXMLDocument createFromFile(File f) throws JDOMException, IOException {
389
    public static ODSingleXMLDocument createFromFile(File f) throws JDOMException, IOException {
385
        final ODSingleXMLDocument res = new ODSingleXMLDocument(OOUtils.getBuilder().build(f));
390
        final ODSingleXMLDocument res = new ODSingleXMLDocument(OOUtils.getBuilder().build(f));
386
        res.getPackage().setFile(f);
391
        res.getPackage().setFile(f);
387
        return res;
392
        return res;
388
    }
393
    }
389
 
394
 
390
    public static ODSingleXMLDocument createFromStream(InputStream ins) throws JDOMException, IOException {
395
    public static ODSingleXMLDocument createFromStream(InputStream ins) throws JDOMException, IOException {
391
        return new ODSingleXMLDocument(OOUtils.getBuilder().build(ins));
396
        return new ODSingleXMLDocument(OOUtils.getBuilder().build(ins));
392
    }
397
    }
393
 
398
 
394
    /**
399
    /**
395
     * fix bug when a SingleXMLDoc is used to create a document (for example with P2 and 1_P2), and
400
     * fix bug when a SingleXMLDoc is used to create a document (for example with P2 and 1_P2), and
396
     * then create another instance s2 with the previous document and add a second file (also with
401
     * then create another instance s2 with the previous document and add a second file (also with
397
     * P2 and 1_P2) => s2 will contain P2, 1_P2, 1_P2, 1_1_P2.
402
     * P2 and 1_P2) => s2 will contain P2, 1_P2, 1_P2, 1_1_P2.
398
     */
403
     */
399
    private static final String COUNT = "SingleXMLDocument_count";
404
    private static final String COUNT = "SingleXMLDocument_count";
400
 
405
 
401
    /** Le nombre de fichiers concat */
406
    /** Le nombre de fichiers concat */
402
    private int numero;
407
    private int numero;
403
    /** Les styles présent dans ce document */
408
    /** Les styles présent dans ce document */
404
    private final Set<String> stylesNames;
409
    private final Set<String> stylesNames;
405
    /** Les styles de liste présent dans ce document */
410
    /** Les styles de liste présent dans ce document */
406
    private final Set<String> listStylesNames;
411
    private final Set<String> listStylesNames;
407
    /** Les fichiers référencés par ce document */
412
    /** Les fichiers référencés par ce document */
408
    private ODPackage pkg;
413
    private ODPackage pkg;
409
    private final ODMeta meta;
414
    private final ODMeta meta;
410
    // the element between each page
415
    // the element between each page
411
    private Element pageBreak;
416
    private Element pageBreak;
412
 
417
 
413
    public ODSingleXMLDocument(Document content) {
418
    public ODSingleXMLDocument(Document content) {
414
        this(content, null);
419
        this(content, null);
415
    }
420
    }
416
 
421
 
417
    /**
422
    /**
418
     * A new single document. NOTE: this document will put himself in <code>pkg</code>, replacing
423
     * A new single document. NOTE: this document will put himself in <code>pkg</code>, replacing
419
     * any previous content.
424
     * any previous content.
420
     * 
425
     * 
421
     * @param content the XML.
426
     * @param content the XML.
422
     * @param pkg the package this document belongs to.
427
     * @param pkg the package this document belongs to.
423
     */
428
     */
424
    private ODSingleXMLDocument(Document content, final ODPackage pkg) {
429
    private ODSingleXMLDocument(Document content, final ODPackage pkg) {
425
        super(content);
430
        super(content);
426
 
431
 
427
        // inited in getPageBreak()
432
        // inited in getPageBreak()
428
        this.pageBreak = null;
433
        this.pageBreak = null;
429
 
434
 
430
        final boolean contentIsFlat = pkg == null;
435
        final boolean contentIsFlat = pkg == null;
431
        this.pkg = contentIsFlat ? new ODPackage() : pkg;
436
        this.pkg = contentIsFlat ? new ODPackage() : pkg;
432
        if (!contentIsFlat) {
437
        if (!contentIsFlat) {
433
            final Set<String> toRm = new HashSet<String>();
438
            final Set<String> toRm = new HashSet<String>();
434
            for (final RootElement e : RootElement.getPackageElements())
439
            for (final RootElement e : RootElement.getPackageElements())
435
                toRm.add(e.getZipEntry());
440
                toRm.add(e.getZipEntry());
436
            for (final String e : this.pkg.getEntries()) {
441
            for (final String e : this.pkg.getEntries()) {
437
                if (e.startsWith(Library.DIR_NAME))
442
                if (e.startsWith(Library.DIR_NAME))
438
                    toRm.add(e);
443
                    toRm.add(e);
439
            }
444
            }
440
            this.pkg.rmFiles(toRm);
445
            this.pkg.rmFiles(toRm);
441
        }
446
        }
442
        this.pkg.putFile(CONTENT.getZipEntry(), this, "text/xml");
447
        this.pkg.putFile(CONTENT.getZipEntry(), this, "text/xml");
443
 
448
 
444
        // update href
449
        // update href
445
        if (contentIsFlat) {
450
        if (contentIsFlat) {
446
            // OD thinks of the ZIP archive as an additional folder
451
            // OD thinks of the ZIP archive as an additional folder
447
            for (final Attribute hrefAttr : ALL_HREF_ATTRIBUTES.selectNodes(getDocument().getRootElement())) {
452
            for (final Attribute hrefAttr : ALL_HREF_ATTRIBUTES.selectNodes(getDocument().getRootElement())) {
448
                final String href = hrefAttr.getValue();
453
                final String href = hrefAttr.getValue();
449
                if (!URI.create(href).isAbsolute())
454
                if (!URI.create(href).isAbsolute())
450
                    hrefAttr.setValue("../" + href);
455
                    hrefAttr.setValue("../" + href);
451
            }
456
            }
452
        }
457
        }
453
        // decode Base64 binaries
458
        // decode Base64 binaries
454
        for (final Element binaryDataElem : ALL_BINARY_DATA_ELEMENTS.selectNodes(getDocument().getRootElement())) {
459
        for (final Element binaryDataElem : ALL_BINARY_DATA_ELEMENTS.selectNodes(getDocument().getRootElement())) {
455
            final String name;
460
            final String name;
456
            int i = 1;
461
            int i = 1;
457
            final Set<String> entries = getPackage().getEntries();
462
            final Set<String> entries = getPackage().getEntries();
458
            final Element binaryParentElement = binaryDataElem.getParentElement();
463
            final Element binaryParentElement = binaryDataElem.getParentElement();
459
            while (entries.contains(binaryParentElement.getName() + "/" + i))
464
            while (entries.contains(binaryParentElement.getName() + "/" + i))
460
                i++;
465
                i++;
461
            name = binaryParentElement.getName() + "/" + i;
466
            name = binaryParentElement.getName() + "/" + i;
462
            getPackage().putFile(name, Base64.decode(binaryDataElem.getText()));
467
            getPackage().putFile(name, Base64.decode(binaryDataElem.getText()));
463
            binaryParentElement.setAttribute("href", name, binaryDataElem.getNamespace("xlink"));
468
            binaryParentElement.setAttribute("href", name, binaryDataElem.getNamespace("xlink"));
464
            binaryDataElem.detach();
469
            binaryDataElem.detach();
465
        }
470
        }
466
 
471
 
467
        this.meta = this.getPackage().getMeta(true);
472
        this.meta = this.getPackage().getMeta(true);
468
 
473
 
469
        final ODUserDefinedMeta userMeta = this.meta.getUserMeta(COUNT);
474
        final ODUserDefinedMeta userMeta = this.meta.getUserMeta(COUNT);
470
        if (userMeta != null) {
475
        if (userMeta != null) {
471
            final Object countValue = userMeta.getValue();
476
            final Object countValue = userMeta.getValue();
472
            if (countValue instanceof Number) {
477
            if (countValue instanceof Number) {
473
                this.numero = ((Number) countValue).intValue();
478
                this.numero = ((Number) countValue).intValue();
474
            } else {
479
            } else {
475
                this.numero = new BigDecimal(countValue.toString()).intValue();
480
                this.numero = new BigDecimal(countValue.toString()).intValue();
476
            }
481
            }
477
        } else {
482
        } else {
478
            // if not hasCount(), it's not us that created content
483
            // if not hasCount(), it's not us that created content
479
            // so there should not be any 1_
484
            // so there should not be any 1_
480
            this.setNumero(0);
485
            this.setNumero(0);
481
        }
486
        }
482
 
487
 
483
        this.stylesNames = new HashSet<String>(64);
488
        this.stylesNames = new HashSet<String>(64);
484
        this.listStylesNames = new HashSet<String>(16);
489
        this.listStylesNames = new HashSet<String>(16);
485
 
490
 
486
        // little trick to find the common styles names (not to be prefixed so they remain
491
        // little trick to find the common styles names (not to be prefixed so they remain
487
        // consistent across the added documents)
492
        // consistent across the added documents)
488
        final Element styles = this.getChild("styles");
493
        final Element styles = this.getChild("styles");
489
        if (styles != null) {
494
        if (styles != null) {
490
            // create a second document with our styles to collect names
495
            // create a second document with our styles to collect names
491
            final Element root = this.getDocument().getRootElement();
496
            final Element root = this.getDocument().getRootElement();
492
            final Document clonedDoc = new Document(new Element(root.getName(), root.getNamespace()));
497
            final Document clonedDoc = new Document(new Element(root.getName(), root.getNamespace()));
493
            clonedDoc.getRootElement().addContent(styles.detach());
498
            clonedDoc.getRootElement().addContent(styles.detach());
494
            try {
499
            try {
495
                this.mergeStyles(new ODXMLDocument(clonedDoc), true);
500
                this.mergeStyles(new ODXMLDocument(clonedDoc), true);
496
            } catch (JDOMException e) {
501
            } catch (JDOMException e) {
497
                throw new IllegalArgumentException("can't find common styles names.");
502
                throw new IllegalArgumentException("can't find common styles names.");
498
            }
503
            }
499
            // reattach our styles
504
            // reattach our styles
500
            styles.detach();
505
            styles.detach();
501
            this.setChild(styles);
506
            this.setChild(styles);
502
        }
507
        }
503
    }
508
    }
504
 
509
 
505
    ODSingleXMLDocument(ODSingleXMLDocument doc, ODPackage p) {
510
    ODSingleXMLDocument(ODSingleXMLDocument doc, ODPackage p) {
506
        super(doc);
511
        super(doc);
507
        if (p == null)
512
        if (p == null)
508
            throw new NullPointerException("Null package");
513
            throw new NullPointerException("Null package");
509
        this.stylesNames = new HashSet<String>(doc.stylesNames);
514
        this.stylesNames = new HashSet<String>(doc.stylesNames);
510
        this.listStylesNames = new HashSet<String>(doc.listStylesNames);
515
        this.listStylesNames = new HashSet<String>(doc.listStylesNames);
511
        this.pkg = p;
516
        this.pkg = p;
512
        this.meta = ODMeta.create(this);
517
        this.meta = ODMeta.create(this);
513
        this.setNumero(doc.numero);
518
        this.setNumero(doc.numero);
514
    }
519
    }
515
 
520
 
516
    @Override
521
    @Override
517
    public ODSingleXMLDocument clone() {
522
    public ODSingleXMLDocument clone() {
518
        final ODPackage copy = new ODPackage(this.pkg);
523
        final ODPackage copy = new ODPackage(this.pkg);
519
        return (ODSingleXMLDocument) copy.getContent();
524
        return (ODSingleXMLDocument) copy.getContent();
520
    }
525
    }
521
 
526
 
522
    private void setNumero(int numero) {
527
    private void setNumero(int numero) {
523
        this.numero = numero;
528
        this.numero = numero;
524
        this.meta.getUserMeta(COUNT, true).setValue(this.numero);
529
        this.meta.getUserMeta(COUNT, true).setValue(this.numero);
525
    }
530
    }
526
 
531
 
527
    /**
532
    /**
528
     * The number of files concatenated with {@link #add(ODSingleXMLDocument)}.
533
     * The number of files concatenated with {@link #add(ODSingleXMLDocument)}.
529
     * 
534
     * 
530
     * @return number of files concatenated.
535
     * @return number of files concatenated.
531
     */
536
     */
532
    public final int getNumero() {
537
    public final int getNumero() {
533
        return this.numero;
538
        return this.numero;
534
    }
539
    }
535
 
540
 
536
    public ODPackage getPackage() {
541
    public ODPackage getPackage() {
537
        return this.pkg;
542
        return this.pkg;
538
    }
543
    }
539
 
544
 
540
    final Element getBasicScriptElem() {
545
    final Element getBasicScriptElem() {
541
        return this.getBasicScriptElem(false);
546
        return this.getBasicScriptElem(false);
542
    }
547
    }
543
 
548
 
544
    private final Element getBasicScriptElem(final boolean create) {
549
    private final Element getBasicScriptElem(final boolean create) {
545
        final OOXML xml = getXML();
550
        final OOXML xml = getXML();
546
        final String officeScripts = xml.getOfficeScripts();
551
        final String officeScripts = xml.getOfficeScripts();
547
        final Element scriptsElem = this.getChild(officeScripts, create);
552
        final Element scriptsElem = this.getChild(officeScripts, create);
548
        if (scriptsElem == null)
553
        if (scriptsElem == null)
549
            return null;
554
            return null;
550
        final Namespace scriptNS = this.getVersion().getNS("script");
555
        final Namespace scriptNS = this.getVersion().getNS("script");
551
        final Namespace officeNS = this.getVersion().getOFFICE();
556
        final Namespace officeNS = this.getVersion().getOFFICE();
552
        @SuppressWarnings("unchecked")
557
        @SuppressWarnings("unchecked")
553
        final List<Element> scriptElems = scriptsElem.getChildren(xml.getOfficeScript(), officeNS);
558
        final List<Element> scriptElems = scriptsElem.getChildren(xml.getOfficeScript(), officeNS);
554
        for (final Element scriptElem : scriptElems) {
559
        for (final Element scriptElem : scriptElems) {
555
            if (scriptElem.getAttributeValue("language", scriptNS).equals(BASIC_LANG_NAME))
560
            if (scriptElem.getAttributeValue("language", scriptNS).equals(BASIC_LANG_NAME))
556
                return scriptElem;
561
                return scriptElem;
557
        }
562
        }
558
        if (create) {
563
        if (create) {
559
            final Element res = new Element(xml.getOfficeScript(), officeNS);
564
            final Element res = new Element(xml.getOfficeScript(), officeNS);
560
            res.setAttribute("language", BASIC_LANG_NAME, scriptNS);
565
            res.setAttribute("language", BASIC_LANG_NAME, scriptNS);
561
            scriptsElem.addContent(res);
566
            scriptsElem.addContent(res);
562
            return res;
567
            return res;
563
        } else {
568
        } else {
564
            return null;
569
            return null;
565
        }
570
        }
566
    }
571
    }
567
 
572
 
568
    /**
573
    /**
569
     * Parse BASIC libraries in this flat XML.
574
     * Parse BASIC libraries in this flat XML.
570
     * 
575
     * 
571
     * @return the BASIC libraries by name.
576
     * @return the BASIC libraries by name.
572
     */
577
     */
573
    public final Map<String, Library> readBasicLibraries() {
578
    public final Map<String, Library> readBasicLibraries() {
574
        return this.readBasicLibraries(this.getBasicScriptElem()).get0();
579
        return this.readBasicLibraries(this.getBasicScriptElem()).get0();
575
    }
580
    }
576
 
581
 
577
    private final Tuple2<Map<String, Library>, Map<String, Element>> readBasicLibraries(final Element scriptElem) {
582
    private final Tuple2<Map<String, Library>, Map<String, Element>> readBasicLibraries(final Element scriptElem) {
578
        if (scriptElem == null)
583
        if (scriptElem == null)
579
            return Tuple2.create(Collections.<String, Library> emptyMap(), Collections.<String, Element> emptyMap());
584
            return Tuple2.create(Collections.<String, Library> emptyMap(), Collections.<String, Element> emptyMap());
580
 
585
 
581
        final Namespace libNS = this.getVersion().getLibrariesNS();
586
        final Namespace libNS = this.getVersion().getLibrariesNS();
582
        final Namespace linkNS = this.getVersion().getNS("xlink");
587
        final Namespace linkNS = this.getVersion().getNS("xlink");
583
        final Map<String, Library> res = new HashMap<String, Library>();
588
        final Map<String, Library> res = new HashMap<String, Library>();
584
        final Map<String, Element> resElems = new HashMap<String, Element>();
589
        final Map<String, Element> resElems = new HashMap<String, Element>();
585
        @SuppressWarnings("unchecked")
590
        @SuppressWarnings("unchecked")
586
        final List<Element> libsElems = scriptElem.getChildren("libraries", libNS);
591
        final List<Element> libsElems = scriptElem.getChildren("libraries", libNS);
587
        for (final Element libsElem : libsElems) {
592
        for (final Element libsElem : libsElems) {
588
            @SuppressWarnings("unchecked")
593
            @SuppressWarnings("unchecked")
589
            final List<Element> libElems = libsElem.getChildren();
594
            final List<Element> libElems = libsElem.getChildren();
590
            for (final Element libElem : libElems) {
595
            for (final Element libElem : libElems) {
591
                final Library library = Library.fromFlatXML(libElem, this.getPackage(), linkNS);
596
                final Library library = Library.fromFlatXML(libElem, this.getPackage(), linkNS);
592
                if (library != null) {
597
                if (library != null) {
593
                    if (res.put(library.getName(), library) != null)
598
                    if (res.put(library.getName(), library) != null)
594
                        throw new IllegalStateException("Duplicate library named " + library.getName());
599
                        throw new IllegalStateException("Duplicate library named " + library.getName());
595
                    resElems.put(library.getName(), libElem);
600
                    resElems.put(library.getName(), libElem);
596
                }
601
                }
597
            }
602
            }
598
        }
603
        }
599
 
604
 
600
        return Tuple2.create(res, resElems);
605
        return Tuple2.create(res, resElems);
601
    }
606
    }
602
 
607
 
603
    /**
608
    /**
604
     * Append a document.
609
     * Append a document.
605
     * 
610
     * 
606
     * @param doc the document to add.
611
     * @param doc the document to add.
607
     */
612
     */
608
    public synchronized void add(ODSingleXMLDocument doc) {
613
    public synchronized void add(ODSingleXMLDocument doc) {
609
        // ajoute un saut de page entre chaque document
614
        // ajoute un saut de page entre chaque document
610
        this.add(doc, true);
615
        this.add(doc, true);
611
    }
616
    }
612
 
617
 
613
    /**
618
    /**
614
     * Append a document.
619
     * Append a document.
615
     * 
620
     * 
616
     * @param doc the document to add, <code>null</code> means no-op.
621
     * @param doc the document to add, <code>null</code> means no-op.
617
     * @param pageBreak whether a page break should be inserted before <code>doc</code>.
622
     * @param pageBreak whether a page break should be inserted before <code>doc</code>.
618
     */
623
     */
619
    public synchronized void add(ODSingleXMLDocument doc, boolean pageBreak) {
624
    public synchronized void add(ODSingleXMLDocument doc, boolean pageBreak) {
620
        if (doc != null && pageBreak) {
625
        if (doc != null && pageBreak) {
621
            // only add a page break, if a page was really added
626
            // only add a page break, if a page was really added
622
            final Element thisBody = this.getBody();
627
            final Element thisBody = this.getBody();
623
            thisBody.addContent(getLastNulls(ELEMS_PARTS.get(getVersion()), thisBody)[0], this.getPageBreak());
628
            thisBody.addContent(getLastNulls(ELEMS_PARTS.get(getVersion()), thisBody)[0], this.getPageBreak());
624
        }
629
        }
625
        this.add(null, -1, doc);
630
        this.add(null, -1, doc);
626
    }
631
    }
627
 
632
 
628
    public synchronized void replace(Element elem, ODSingleXMLDocument doc) {
633
    public synchronized void replace(Element elem, ODSingleXMLDocument doc) {
629
        final Element parent = elem.getParentElement();
634
        final Element parent = elem.getParentElement();
630
        this.add(parent, parent.indexOf(elem), doc);
635
        this.add(parent, parent.indexOf(elem), doc);
631
        elem.detach();
636
        elem.detach();
632
    }
637
    }
633
 
638
 
634
    // use content index and not children (element) index, since it's more accurate (we can add
639
    // use content index and not children (element) index, since it's more accurate (we can add
635
    // after or before a comment) and faster (no filter and adjusted index)
640
    // after or before a comment) and faster (no filter and adjusted index)
636
    /**
641
    /**
637
     * Add the passed document at the specified place.
642
     * Add the passed document at the specified place.
638
     * 
643
     * 
639
     * @param where a descendant of the body, <code>null</code> meaning the body itself.
644
     * @param where a descendant of the body, <code>null</code> meaning the body itself.
640
     * @param index the content index inside <code>where</code>, -1 meaning the end.
645
     * @param index the content index inside <code>where</code>, -1 meaning the end.
641
     * @param doc the document to add, <code>null</code> means no-op.
646
     * @param doc the document to add, <code>null</code> means no-op.
642
     */
647
     */
643
    public synchronized void add(Element where, int index, ODSingleXMLDocument doc) {
648
    public synchronized void add(Element where, int index, ODSingleXMLDocument doc) {
644
        if (doc == null)
649
        if (doc == null)
645
            return;
650
            return;
646
        if (!this.getVersion().equals(doc.getVersion()))
651
        if (!this.getVersion().equals(doc.getVersion()))
647
            throw new IllegalArgumentException("version mismatch");
652
            throw new IllegalArgumentException("version mismatch");
648
 
653
 
649
        this.setNumero(this.numero + 1);
654
        this.setNumero(this.numero + 1);
650
        try {
655
        try {
651
            copyNS(doc.getDocument(), this.getDocument());
656
            copyNS(doc.getDocument(), this.getDocument());
652
            this.mergeEmbedded(doc);
657
            this.mergeEmbedded(doc);
653
            this.mergeSettings(doc);
658
            this.mergeSettings(doc);
654
            this.mergeScripts(doc);
659
            this.mergeScripts(doc);
655
            this.mergeAllStyles(doc, false);
660
            this.mergeAllStyles(doc, false);
656
            this.mergeBody(where, index, doc);
661
            this.mergeBody(where, index, doc);
657
        } catch (JDOMException exn) {
662
        } catch (JDOMException exn) {
658
            throw new IllegalArgumentException("XML error", exn);
663
            throw new IllegalArgumentException("XML error", exn);
659
        }
664
        }
660
    }
665
    }
661
 
666
 
662
    /**
667
    /**
663
     * Merge the four elements of style.
668
     * Merge the four elements of style.
664
     * 
669
     * 
665
     * @param doc the xml document to merge.
670
     * @param doc the xml document to merge.
666
     * @param sameDoc whether <code>doc</code> is the same OpenDocument than this, eg
671
     * @param sameDoc whether <code>doc</code> is the same OpenDocument than this, eg
667
     *        <code>true</code> when merging content.xml and styles.xml.
672
     *        <code>true</code> when merging content.xml and styles.xml.
668
     * @throws JDOMException if an error occurs.
673
     * @throws JDOMException if an error occurs.
669
     */
674
     */
670
    private void mergeAllStyles(ODXMLDocument doc, boolean sameDoc) throws JDOMException {
675
    private void mergeAllStyles(ODXMLDocument doc, boolean sameDoc) throws JDOMException {
671
        // no reference
676
        // no reference
672
        this.mergeFontDecls(doc);
677
        this.mergeFontDecls(doc);
673
        // section 14.1
678
        // section 14.1
674
        // § Parent Style only refer to other common styles
679
        // § Parent Style only refer to other common styles
675
        // § Next Style cannot refer to an autostyle (only available in common styles)
680
        // § Next Style cannot refer to an autostyle (only available in common styles)
676
        // § List Style can refer to an autostyle
681
        // § List Style can refer to an autostyle
677
        // § Master Page Name cannot (auto master pages does not exist)
682
        // § Master Page Name cannot (auto master pages does not exist)
678
        // § Data Style Name (for cells) can
683
        // § Data Style Name (for cells) can
679
        // but since the UI for common styles doesn't allow to customize List Style
684
        // but since the UI for common styles doesn't allow to customize List Style
680
        // and there is no common styles for tables : office:styles doesn't reference any automatic
685
        // and there is no common styles for tables : office:styles doesn't reference any automatic
681
        // styles
686
        // styles
682
        this.mergeStyles(doc, sameDoc);
687
        this.mergeStyles(doc, sameDoc);
683
        // on the contrary autostyles do refer to other autostyles :
688
        // on the contrary autostyles do refer to other autostyles :
684
        // choosing "activate bullets" will create an automatic paragraph style:style
689
        // choosing "activate bullets" will create an automatic paragraph style:style
685
        // referencing an automatic text:list-style.
690
        // referencing an automatic text:list-style.
686
        this.mergeAutoStyles(doc, !sameDoc);
691
        this.mergeAutoStyles(doc, !sameDoc);
687
        // section 14.4
692
        // section 14.4
688
        // § Page Layout can refer to an autostyle
693
        // § Page Layout can refer to an autostyle
689
        // § Next Style Name refer to another masterPage
694
        // § Next Style Name refer to another masterPage
690
        this.mergeMasterStyles(doc, !sameDoc);
695
        this.mergeMasterStyles(doc, !sameDoc);
691
    }
696
    }
692
 
697
 
693
    private void mergeEmbedded(ODSingleXMLDocument doc) {
698
    private void mergeEmbedded(ODSingleXMLDocument doc) {
694
        // since we are adding another document our existing thumbnail is obsolete
699
        // since we are adding another document our existing thumbnail is obsolete
695
        this.pkg.rmFile("Thumbnails/thumbnail.png");
700
        this.pkg.rmFile("Thumbnails/thumbnail.png");
696
        this.pkg.rmFile("layout-cache");
701
        this.pkg.rmFile("layout-cache");
697
        // copy the files (only non generated files, e.g. content.xml will be merged later)
702
        // copy the files (only non generated files, e.g. content.xml will be merged later)
698
        for (final String name : doc.pkg.getEntries()) {
703
        for (final String name : doc.pkg.getEntries()) {
699
            final ODPackageEntry e = doc.pkg.getEntry(name);
704
            final ODPackageEntry e = doc.pkg.getEntry(name);
700
            if (!ODPackage.isStandardFile(e.getName())) {
705
            if (!ODPackage.isStandardFile(e.getName())) {
701
                this.pkg.putCopy(e, this.prefix(e.getName()));
706
                this.pkg.putCopy(e, this.prefix(e.getName()));
702
            }
707
            }
703
        }
708
        }
704
    }
709
    }
705
 
710
 
706
    private void mergeSettings(ODSingleXMLDocument doc) throws JDOMException {
711
    private void mergeSettings(ODSingleXMLDocument doc) throws JDOMException {
707
        // used to call addIfNotPresent(), but it cannot create the element at the correct position
712
        // used to call addIfNotPresent(), but it cannot create the element at the correct position
708
        final String elemName = "settings";
713
        final String elemName = "settings";
709
        if (this.getChild(elemName, false) == null) {
714
        if (this.getChild(elemName, false) == null) {
710
            final Element other = doc.getChild(elemName, false);
715
            final Element other = doc.getChild(elemName, false);
711
            if (other != null) {
716
            if (other != null) {
712
                this.getChild(elemName, true).addContent(other.cloneContent());
717
                this.getChild(elemName, true).addContent(other.cloneContent());
713
            }
718
            }
714
        }
719
        }
715
    }
720
    }
716
 
721
 
717
    private void mergeScripts(ODSingleXMLDocument doc) {
722
    private void mergeScripts(ODSingleXMLDocument doc) {
718
        // <office:script>*
723
        // <office:script>*
719
        this.addBasicLibraries(doc.readBasicLibraries());
724
        this.addBasicLibraries(doc.readBasicLibraries());
720
 
725
 
721
        // <office:event-listeners>?
726
        // <office:event-listeners>?
722
        final Map<String, EventListener> oEvents = doc.getPackage().readEventListeners();
727
        final Map<String, EventListener> oEvents = doc.getPackage().readEventListeners();
723
        if (oEvents.size() > 0) {
728
        if (oEvents.size() > 0) {
724
            // check if they can be merged
729
            // check if they can be merged
725
            final Map<String, EventListener> thisEvents = this.getPackage().readEventListeners();
730
            final Map<String, EventListener> thisEvents = this.getPackage().readEventListeners();
726
            final Set<String> duplicateEvents = CollectionUtils.inter(thisEvents.keySet(), oEvents.keySet());
731
            final Set<String> duplicateEvents = CollectionUtils.inter(thisEvents.keySet(), oEvents.keySet());
727
            for (final String eventName : duplicateEvents) {
732
            for (final String eventName : duplicateEvents) {
728
                final Element thisEvent = thisEvents.get(eventName).getElement();
733
                final Element thisEvent = thisEvents.get(eventName).getElement();
729
                final Element oEvent = oEvents.get(eventName).getElement();
734
                final Element oEvent = oEvents.get(eventName).getElement();
730
                if (!JDOMUtils.equalsDeep(oEvent, thisEvent)) {
735
                if (!JDOMUtils.equalsDeep(oEvent, thisEvent)) {
731
                    throw new IllegalArgumentException("Incompatible elements for " + eventName);
736
                    throw new IllegalArgumentException("Incompatible elements for " + eventName);
732
                }
737
                }
733
            }
738
            }
734
 
739
 
735
            final OOXML xml = getXML();
740
            final OOXML xml = getXML();
736
            final Element thisScripts = this.getChild(xml.getOfficeScripts(), true);
741
            final Element thisScripts = this.getChild(xml.getOfficeScripts(), true);
737
            final Element thisEventListeners = JDOMUtils.getOrCreateChild(thisScripts, xml.getOfficeEventListeners(), this.getVersion().getOFFICE());
742
            final Element thisEventListeners = JDOMUtils.getOrCreateChild(thisScripts, xml.getOfficeEventListeners(), this.getVersion().getOFFICE());
738
            for (final Entry<String, EventListener> e : oEvents.entrySet()) {
743
            for (final Entry<String, EventListener> e : oEvents.entrySet()) {
739
                if (!thisEvents.containsKey(e.getKey())) {
744
                if (!thisEvents.containsKey(e.getKey())) {
740
                    // we can just clone since libraries aren't renamed when merged
745
                    // we can just clone since libraries aren't renamed when merged
741
                    thisEventListeners.addContent((Element) e.getValue().getElement().clone());
746
                    thisEventListeners.addContent((Element) e.getValue().getElement().clone());
742
                }
747
                }
743
            }
748
            }
744
        }
749
        }
745
    }
750
    }
746
 
751
 
747
    /**
752
    /**
748
     * Add the passed libraries to this document. Passed libraries with the same content as existing
753
     * Add the passed libraries to this document. Passed libraries with the same content as existing
749
     * ones are ignored.
754
     * ones are ignored.
750
     * 
755
     * 
751
     * @param libraries what to add.
756
     * @param libraries what to add.
752
     * @return the actually added libraries.
757
     * @return the actually added libraries.
753
     * @throws IllegalArgumentException if <code>libraries</code> contains duplicates or if it
758
     * @throws IllegalArgumentException if <code>libraries</code> contains duplicates or if it
754
     *         cannot be merged into this.
759
     *         cannot be merged into this.
755
     * @see Library#canBeMerged(Library)
760
     * @see Library#canBeMerged(Library)
756
     */
761
     */
757
    public final Set<String> addBasicLibraries(final Collection<? extends Library> libraries) {
762
    public final Set<String> addBasicLibraries(final Collection<? extends Library> libraries) {
758
        return this.addBasicLibraries(Library.toMap(libraries));
763
        return this.addBasicLibraries(Library.toMap(libraries));
759
    }
764
    }
760
 
765
 
761
    public final Set<String> addBasicLibraries(final ODPackage pkg) {
766
    public final Set<String> addBasicLibraries(final ODPackage pkg) {
762
        if (pkg == this.pkg)
767
        if (pkg == this.pkg)
763
            return Collections.emptySet();
768
            return Collections.emptySet();
764
        return this.addBasicLibraries(pkg.readBasicLibraries());
769
        return this.addBasicLibraries(pkg.readBasicLibraries());
765
    }
770
    }
766
 
771
 
767
    final Set<String> addBasicLibraries(final Map<String, Library> oLibraries) {
772
    final Set<String> addBasicLibraries(final Map<String, Library> oLibraries) {
768
        if (oLibraries.size() == 0)
773
        if (oLibraries.size() == 0)
769
            return Collections.emptySet();
774
            return Collections.emptySet();
770
 
775
 
771
        final Tuple2<Map<String, Library>, Map<String, Element>> thisLibrariesAndElements = this.readBasicLibraries(this.getBasicScriptElem(false));
776
        final Tuple2<Map<String, Library>, Map<String, Element>> thisLibrariesAndElements = this.readBasicLibraries(this.getBasicScriptElem(false));
772
        final Map<String, Library> thisLibraries = thisLibrariesAndElements.get0();
777
        final Map<String, Library> thisLibraries = thisLibrariesAndElements.get0();
773
        final Map<String, Element> thisLibrariesElements = thisLibrariesAndElements.get1();
778
        final Map<String, Element> thisLibrariesElements = thisLibrariesAndElements.get1();
774
        // check that the libraries to add which are already in us can be merged (no elements
779
        // check that the libraries to add which are already in us can be merged (no elements
775
        // conflict)
780
        // conflict)
776
        final Set<String> duplicateLibs = Library.canBeMerged(thisLibraries, oLibraries);
781
        final Set<String> duplicateLibs = Library.canBeMerged(thisLibraries, oLibraries);
777
        final Set<String> newLibs = new HashSet<String>(oLibraries.keySet());
782
        final Set<String> newLibs = new HashSet<String>(oLibraries.keySet());
778
        newLibs.removeAll(thisLibraries.keySet());
783
        newLibs.removeAll(thisLibraries.keySet());
779
 
784
 
780
        // merge modules
785
        // merge modules
781
        for (final String duplicateLib : duplicateLibs) {
786
        for (final String duplicateLib : duplicateLibs) {
782
            final Library thisLib = thisLibraries.get(duplicateLib);
787
            final Library thisLib = thisLibraries.get(duplicateLib);
783
            final Library oLib = oLibraries.get(duplicateLib);
788
            final Library oLib = oLibraries.get(duplicateLib);
784
            assert thisLib != null && oLib != null : "Not duplicate " + duplicateLib;
789
            assert thisLib != null && oLib != null : "Not duplicate " + duplicateLib;
785
            oLib.mergeToFlatXML(this.getFormatVersion(), thisLib, thisLibrariesElements.get(duplicateLib));
790
            oLib.mergeToFlatXML(this.getFormatVersion(), thisLib, thisLibrariesElements.get(duplicateLib));
786
        }
791
        }
787
        if (newLibs.size() > 0) {
792
        if (newLibs.size() > 0) {
788
            final Element thisScriptElem = this.getBasicScriptElem(true);
793
            final Element thisScriptElem = this.getBasicScriptElem(true);
789
            final Element librariesElem = JDOMUtils.getOrCreateChild(thisScriptElem, "libraries", this.getVersion().getLibrariesNS());
794
            final Element librariesElem = JDOMUtils.getOrCreateChild(thisScriptElem, "libraries", this.getVersion().getLibrariesNS());
790
            for (final String newLib : newLibs)
795
            for (final String newLib : newLibs)
791
                librariesElem.addContent(oLibraries.get(newLib).toFlatXML(this.getFormatVersion()));
796
                librariesElem.addContent(oLibraries.get(newLib).toFlatXML(this.getFormatVersion()));
792
        }
797
        }
793
 
798
 
794
        // merge dialogs
799
        // merge dialogs
795
        for (final Library oLib : oLibraries.values()) {
800
        for (final Library oLib : oLibraries.values()) {
796
            final String libName = oLib.getName();
801
            final String libName = oLib.getName();
797
            // can be null
802
            // can be null
798
            final Library thisLib = thisLibraries.get(libName);
803
            final Library thisLib = thisLibraries.get(libName);
799
            oLib.mergeDialogs(this.getPackage(), thisLib);
804
            oLib.mergeDialogs(this.getPackage(), thisLib);
800
        }
805
        }
801
 
806
 
802
        return newLibs;
807
        return newLibs;
803
    }
808
    }
804
 
809
 
805
    /**
810
    /**
806
     * Remove the passed libraries.
811
     * Remove the passed libraries.
807
     * 
812
     * 
808
     * @param libraries which libraries to remove.
813
     * @param libraries which libraries to remove.
809
     * @return the actually removed libraries.
814
     * @return the actually removed libraries.
810
     */
815
     */
811
    public final Set<String> removeBasicLibraries(final Collection<String> libraries) {
816
    public final Set<String> removeBasicLibraries(final Collection<String> libraries) {
812
        final Element basicScriptElem = this.getBasicScriptElem(false);
817
        final Element basicScriptElem = this.getBasicScriptElem(false);
813
        final Map<String, Element> thisLibrariesElements = this.readBasicLibraries(basicScriptElem).get1();
818
        final Map<String, Element> thisLibrariesElements = this.readBasicLibraries(basicScriptElem).get1();
814
        // don't return if thisLibrariesElements is empty as we also check the package content
819
        // don't return if thisLibrariesElements is empty as we also check the package content
815
 
820
 
816
        final Set<String> res = new HashSet<String>();
821
        final Set<String> res = new HashSet<String>();
817
        for (final String libToRm : libraries) {
822
        for (final String libToRm : libraries) {
818
            final Element elemToRm = thisLibrariesElements.get(libToRm);
823
            final Element elemToRm = thisLibrariesElements.get(libToRm);
819
            if (elemToRm != null) {
824
            if (elemToRm != null) {
820
                // also detach empty <libraries>
825
                // also detach empty <libraries>
821
                JDOMUtils.detachEmptyParent(elemToRm);
826
                JDOMUtils.detachEmptyParent(elemToRm);
822
                res.add(libToRm);
827
                res.add(libToRm);
823
            }
828
            }
824
            if (Library.removeFromPackage(this.getPackage(), libToRm))
829
            if (Library.removeFromPackage(this.getPackage(), libToRm))
825
                res.add(libToRm);
830
                res.add(libToRm);
826
        }
831
        }
827
        // Detach empty <script>, works because we already detached empty <libraries>
832
        // Detach empty <script>, works because we already detached empty <libraries>
828
        if (basicScriptElem != null && basicScriptElem.getChildren().isEmpty())
833
        if (basicScriptElem != null && basicScriptElem.getChildren().isEmpty())
829
            basicScriptElem.detach();
834
            basicScriptElem.detach();
830
 
835
 
831
        return res;
836
        return res;
832
    }
837
    }
833
 
838
 
834
    /**
839
    /**
835
     * Fusionne les office:font-decls/style:font-decl. On ne préfixe jamais, on ajoute seulement si
840
     * Fusionne les office:font-decls/style:font-decl. On ne préfixe jamais, on ajoute seulement si
836
     * l'attribut style:name est différent.
841
     * l'attribut style:name est différent.
837
     * 
842
     * 
838
     * @param doc le document à fusionner avec celui-ci.
843
     * @param doc le document à fusionner avec celui-ci.
839
     * @throws JDOMException
844
     * @throws JDOMException
840
     */
845
     */
841
    private void mergeFontDecls(ODXMLDocument doc) throws JDOMException {
846
    private void mergeFontDecls(ODXMLDocument doc) throws JDOMException {
842
        final String[] fontDecls = this.getFontDecls();
847
        final String[] fontDecls = this.getFontDecls();
843
        this.mergeUnique(doc, fontDecls[0], fontDecls[1]);
848
        this.mergeUnique(doc, fontDecls[0], fontDecls[1]);
844
    }
849
    }
845
 
850
 
846
    private String[] getFontDecls() {
851
    private String[] getFontDecls() {
847
        return getXML().getFontDecls();
852
        return getXML().getFontDecls();
848
    }
853
    }
849
 
854
 
850
    // merge everything under office:styles
855
    // merge everything under office:styles
851
    private void mergeStyles(ODXMLDocument doc, boolean sameDoc) throws JDOMException {
856
    private void mergeStyles(ODXMLDocument doc, boolean sameDoc) throws JDOMException {
852
        // les default-style (notamment tab-stop-distance)
857
        // les default-style (notamment tab-stop-distance)
853
        this.mergeUnique(doc, "styles", "style:default-style", "style:family", NOP_ElementTransformer);
858
        this.mergeUnique(doc, "styles", "style:default-style", "style:family", NOP_ElementTransformer);
854
        // data styles
859
        // data styles
855
        // we have to prefix data styles since they're automatically generated by LO
860
        // we have to prefix data styles since they're automatically generated by LO
856
        // (e.g. user created cellStyle1, LO generated dataStyleN0 in a document, then in another
861
        // (e.g. user created cellStyle1, LO generated dataStyleN0 in a document, then in another
857
        // document created cellStyle2, LO also generated dataStyleN0)
862
        // document created cellStyle2, LO also generated dataStyleN0)
858
        // MAYBE search for orphans that discarded (same name) styles might leave
863
        // MAYBE search for orphans that discarded (same name) styles might leave
859
        // Don't prefix if we're merging styles into content (content contains no styles, so there
864
        // Don't prefix if we're merging styles into content (content contains no styles, so there
860
        // can be no collision ; also we don't prefix the body)
865
        // can be no collision ; also we don't prefix the body)
861
        final String dsNS = "number";
866
        final String dsNS = "number";
862
        final boolean prefixDataStyles = !sameDoc;
867
        final boolean prefixDataStyles = !sameDoc;
863
        final List<Element> addedDataStyles = this.addStyles(doc, "styles", Step.createElementStep(null, dsNS, new IPredicate<Element>() {
868
        final List<Element> addedDataStyles = this.addStyles(doc, "styles", Step.createElementStep(null, dsNS, new IPredicate<Element>() {
864
            private final Set<String> names;
869
            private final Set<String> names;
865
            {
870
            {
866
                this.names = new HashSet<String>(DataStyle.DATA_STYLES.size());
871
                this.names = new HashSet<String>(DataStyle.DATA_STYLES.size());
867
                for (final Class<? extends DataStyle> cl : DataStyle.DATA_STYLES) {
872
                for (final Class<? extends DataStyle> cl : DataStyle.DATA_STYLES) {
868
                    final StyleDesc<? extends DataStyle> styleDesc = Style.getStyleDesc(cl, getVersion());
873
                    final StyleDesc<? extends DataStyle> styleDesc = Style.getStyleDesc(cl, getVersion());
869
                    this.names.add(styleDesc.getElementName());
874
                    this.names.add(styleDesc.getElementName());
870
                    assert styleDesc.getElementNS().getPrefix().equals(dsNS) : styleDesc;
875
                    assert styleDesc.getElementNS().getPrefix().equals(dsNS) : styleDesc;
871
                }
876
                }
872
            }
877
            }
873
 
878
 
874
            @Override
879
            @Override
875
            public boolean evaluateChecked(Element elem) {
880
            public boolean evaluateChecked(Element elem) {
876
                return this.names.contains(elem.getName());
881
                return this.names.contains(elem.getName());
877
            }
882
            }
878
        }), prefixDataStyles);
883
        }), prefixDataStyles);
879
        if (prefixDataStyles) {
884
        if (prefixDataStyles) {
880
            // data styles reference each other (e.g. style:map)
885
            // data styles reference each other (e.g. style:map)
881
            final SimpleXMLPath<Attribute> simplePath = SimpleXMLPath.allAttributes("apply-style-name", "style");
886
            final SimpleXMLPath<Attribute> simplePath = SimpleXMLPath.allAttributes("apply-style-name", "style");
882
            for (final Attribute attr : simplePath.selectNodes(addedDataStyles)) {
887
            for (final Attribute attr : simplePath.selectNodes(addedDataStyles)) {
883
                attr.setValue(prefix(attr.getValue()));
888
                attr.setValue(prefix(attr.getValue()));
884
            }
889
            }
885
        }
890
        }
886
        // les styles
891
        // les styles
887
        // if we prefixed data styles, we must prefix references
892
        // if we prefixed data styles, we must prefix references
888
        this.stylesNames.addAll(this.mergeUnique(doc, "styles", "style:style", !prefixDataStyles ? NOP_ElementTransformer : new ElementTransformer() {
893
        this.stylesNames.addAll(this.mergeUnique(doc, "styles", "style:style", !prefixDataStyles ? NOP_ElementTransformer : new ElementTransformer() {
889
            @Override
894
            @Override
890
            public Element transform(Element elem) throws JDOMException {
895
            public Element transform(Element elem) throws JDOMException {
891
                final Attribute attr = elem.getAttribute("data-style-name", elem.getNamespace());
896
                final Attribute attr = elem.getAttribute("data-style-name", elem.getNamespace());
892
                if (attr != null)
897
                if (attr != null)
893
                    attr.setValue(prefix(attr.getValue()));
898
                    attr.setValue(prefix(attr.getValue()));
894
                return elem;
899
                return elem;
895
            }
900
            }
896
        }));
901
        }));
897
        // on ajoute outline-style si non présent
902
        // on ajoute outline-style si non présent
898
        this.addStylesIfNotPresent(doc, "outline-style");
903
        this.addStylesIfNotPresent(doc, "outline-style");
899
        // les list-style
904
        // les list-style
900
        this.listStylesNames.addAll(this.mergeUnique(doc, "styles", "text:list-style"));
905
        this.listStylesNames.addAll(this.mergeUnique(doc, "styles", "text:list-style"));
901
        // les *notes-configuration
906
        // les *notes-configuration
902
        if (getVersion() == XMLVersion.OOo) {
907
        if (getVersion() == XMLVersion.OOo) {
903
            this.addStylesIfNotPresent(doc, "footnotes-configuration");
908
            this.addStylesIfNotPresent(doc, "footnotes-configuration");
904
            this.addStylesIfNotPresent(doc, "endnotes-configuration");
909
            this.addStylesIfNotPresent(doc, "endnotes-configuration");
905
        } else {
910
        } else {
906
            // 16.29.3 : specifies values for each note class used in a document
911
            // 16.29.3 : specifies values for each note class used in a document
907
            this.mergeUnique(doc, "styles", "text:notes-configuration", "text:note-class", NOP_ElementTransformer);
912
            this.mergeUnique(doc, "styles", "text:notes-configuration", "text:note-class", NOP_ElementTransformer);
908
        }
913
        }
909
        this.addStylesIfNotPresent(doc, "bibliography-configuration");
914
        this.addStylesIfNotPresent(doc, "bibliography-configuration");
910
        this.addStylesIfNotPresent(doc, "linenumbering-configuration");
915
        this.addStylesIfNotPresent(doc, "linenumbering-configuration");
911
    }
916
    }
912
 
917
 
913
    /**
918
    /**
914
     * Fusionne les office:automatic-styles, on préfixe tout.
919
     * Fusionne les office:automatic-styles, on préfixe tout.
915
     * 
920
     * 
916
     * @param doc le document à fusionner avec celui-ci.
921
     * @param doc le document à fusionner avec celui-ci.
917
     * @param ref whether to prefix hrefs.
922
     * @param ref whether to prefix hrefs.
918
     * @throws JDOMException
923
     * @throws JDOMException
919
     */
924
     */
920
    private void mergeAutoStyles(ODXMLDocument doc, boolean ref) throws JDOMException {
925
    private void mergeAutoStyles(ODXMLDocument doc, boolean ref) throws JDOMException {
921
        final List<Element> addedStyles = this.prefixAndAddAutoStyles(doc);
926
        final List<Element> addedStyles = this.prefixAndAddAutoStyles(doc);
922
        for (final Element addedStyle : addedStyles) {
927
        for (final Element addedStyle : addedStyles) {
923
            this.prefix(addedStyle, ref);
928
            this.prefix(addedStyle, ref);
924
        }
929
        }
925
    }
930
    }
926
 
931
 
927
    /**
932
    /**
928
     * Fusionne les office:master-styles. On ne préfixe jamais, on ajoute seulement si l'attribut
933
     * Fusionne les office:master-styles. On ne préfixe jamais, on ajoute seulement si l'attribut
929
     * style:name est différent.
934
     * style:name est différent.
930
     * 
935
     * 
931
     * @param doc le document à fusionner avec celui-ci.
936
     * @param doc le document à fusionner avec celui-ci.
932
     * @param ref whether to prefix hrefs.
937
     * @param ref whether to prefix hrefs.
933
     * @throws JDOMException if an error occurs.
938
     * @throws JDOMException if an error occurs.
934
     */
939
     */
935
    private void mergeMasterStyles(ODXMLDocument doc, boolean ref) throws JDOMException {
940
    private void mergeMasterStyles(ODXMLDocument doc, boolean ref) throws JDOMException {
936
        // est référencé dans les styles avec "style:master-page-name"
941
        // est référencé dans les styles avec "style:master-page-name"
937
        this.mergeUnique(doc, "master-styles", "style:master-page", ref ? this.prefixTransf : this.prefixTransfNoRef);
942
        this.mergeUnique(doc, "master-styles", "style:master-page", ref ? this.prefixTransf : this.prefixTransfNoRef);
938
    }
943
    }
939
 
944
 
940
    /**
945
    /**
941
     * Fusionne les corps.
946
     * Fusionne les corps.
942
     * 
947
     * 
943
     * @param where the element where to add the main content, <code>null</code> meaning at the root
948
     * @param where the element where to add the main content, <code>null</code> meaning at the root
944
     *        of the body.
949
     *        of the body.
945
     * @param index the content index inside <code>where</code>, -1 meaning at the end.
950
     * @param index the content index inside <code>where</code>, -1 meaning at the end.
946
     * @param doc le document à fusionner avec celui-ci.
951
     * @param doc le document à fusionner avec celui-ci.
947
     * @throws JDOMException
952
     * @throws JDOMException
948
     */
953
     */
949
    private void mergeBody(Element where, int index, ODSingleXMLDocument doc) throws JDOMException {
954
    private void mergeBody(Element where, int index, ODSingleXMLDocument doc) throws JDOMException {
950
        final Element thisBody = this.getBody();
955
        final Element thisBody = this.getBody();
951
        final Map<Tuple2<Namespace, String>, ContentPart> parts = ELEMS_PARTS.get(getVersion());
956
        final Map<Tuple2<Namespace, String>, ContentPart> parts = ELEMS_PARTS.get(getVersion());
952
 
957
 
953
        // find where to add
958
        // find where to add
954
        final Element nonNullWhere = where != null ? where : thisBody;
959
        final Element nonNullWhere = where != null ? where : thisBody;
955
        if (nonNullWhere == thisBody) {
960
        if (nonNullWhere == thisBody) {
956
            // don't add in prologue or epilogue (ATTN the caller passed the index in reference to
961
            // don't add in prologue or epilogue (ATTN the caller passed the index in reference to
957
            // the existing body but it might change and thus the index might need correction)
962
            // the existing body but it might change and thus the index might need correction)
958
            index = getValidIndex(parts, thisBody, index);
963
            index = getValidIndex(parts, thisBody, index);
959
        } else {
964
        } else {
960
            // check that the element is rooted in the main part
965
            // check that the element is rooted in the main part
961
            final Element movedChild = JDOMUtils.getAncestor(nonNullWhere, new IPredicate<Element>() {
966
            final Element movedChild = JDOMUtils.getAncestor(nonNullWhere, new IPredicate<Element>() {
962
                @Override
967
                @Override
963
                public boolean evaluateChecked(Element input) {
968
                public boolean evaluateChecked(Element input) {
964
                    return input.getParent() == thisBody;
969
                    return input.getParent() == thisBody;
965
                }
970
                }
966
            });
971
            });
967
            if (movedChild == null)
972
            if (movedChild == null)
968
                throw new IllegalStateException("not adding in body : " + nonNullWhere);
973
                throw new IllegalStateException("not adding in body : " + nonNullWhere);
969
            final ContentPart contentPart = getPart(parts, movedChild);
974
            final ContentPart contentPart = getPart(parts, movedChild);
970
            if (contentPart != ContentPart.MAIN)
975
            if (contentPart != ContentPart.MAIN)
971
                throw new IllegalStateException("not adding in main : " + contentPart + " ; " + nonNullWhere);
976
                throw new IllegalStateException("not adding in main : " + contentPart + " ; " + nonNullWhere);
972
        }
977
        }
973
 
978
 
974
        final ChildCreator childCreator = ChildCreator.createFromSets(thisBody, ELEMS_ORDER.get(getVersion()));
979
        final ChildCreator childCreator = ChildCreator.createFromSets(thisBody, ELEMS_ORDER.get(getVersion()));
975
        int prologueAddedCount = 0;
980
        int prologueAddedCount = 0;
976
        final Element otherBody = doc.getBody();
981
        final Element otherBody = doc.getBody();
977
        // split doc body in to three parts to keep non-elements
982
        // split doc body in to three parts to keep non-elements
978
        final Point[] bounds = getBounds(parts, otherBody, false);
983
        final Point[] bounds = getBounds(parts, otherBody, false);
979
        @SuppressWarnings("unchecked")
984
        @SuppressWarnings("unchecked")
980
        final List<Content> otherContent = otherBody.getContent();
985
        final List<Content> otherContent = otherBody.getContent();
981
        // prologue and epilogue have small and bounded size
986
        // prologue and epilogue have small and bounded size
982
        final List<Content> mainElements = new ArrayList<Content>(otherContent.size());
987
        final List<Content> mainElements = new ArrayList<Content>(otherContent.size());
983
        final ListIterator<Content> listIter = otherContent.listIterator();
988
        final ListIterator<Content> listIter = otherContent.listIterator();
984
        for (final ContentPart part : ContentPart.values()) {
989
        for (final ContentPart part : ContentPart.values()) {
985
            final Point partBounds = bounds[part.ordinal()];
990
            final Point partBounds = bounds[part.ordinal()];
986
            final int partEnd = partBounds.y;
991
            final int partEnd = partBounds.y;
987
            while (listIter.nextIndex() < partEnd) {
992
            while (listIter.nextIndex() < partEnd) {
988
                assert listIter.hasNext() : "wrong bounds";
993
                assert listIter.hasNext() : "wrong bounds";
989
                final Content c = listIter.next();
994
                final Content c = listIter.next();
990
                if (c instanceof Element) {
995
                if (c instanceof Element) {
991
                    final Element bodyChild = (Element) c;
996
                    final Element bodyChild = (Element) c;
992
                    if (part == ContentPart.PROLOGUE) {
997
                    if (part == ContentPart.PROLOGUE) {
993
                        final int preSize = thisBody.getContentSize();
998
                        final int preSize = thisBody.getContentSize();
994
                        final String childName = bodyChild.getName();
999
                        final String childName = bodyChild.getName();
995
                        if (childName.equals("forms")) {
1000
                        if (childName.equals("forms")) {
996
                            final Element elem = (Element) bodyChild.clone();
1001
                            final Element elem = (Element) bodyChild.clone();
997
                            // TODO prefix xml:id and their draw:control references
1002
                            // TODO prefix xml:id and their draw:control references
998
                            this.prefix(elem, true);
1003
                            this.prefix(elem, true);
999
                            childCreator.getChild(bodyChild, true).addContent(elem.removeContent());
1004
                            childCreator.getChild(bodyChild, true).addContent(elem.removeContent());
1000
                        } else if (childName.equals("variable-decls") || childName.equals("sequence-decls") || childName.equals("user-field-decls")) {
1005
                        } else if (childName.equals("variable-decls") || childName.equals("sequence-decls") || childName.equals("user-field-decls")) {
1001
                            final Element elem = (Element) bodyChild.clone();
1006
                            final Element elem = (Element) bodyChild.clone();
1002
                            // * user fields are global to a document, they do not vary across it.
1007
                            // * user fields are global to a document, they do not vary across it.
1003
                            // Hence they are initialized at declaration
1008
                            // Hence they are initialized at declaration
1004
                            // * variables are not initialized at declaration
1009
                            // * variables are not initialized at declaration
1005
                            detachDuplicate(elem);
1010
                            detachDuplicate(elem);
1006
                            this.prefix(elem, true);
1011
                            this.prefix(elem, true);
1007
                            childCreator.getChild(bodyChild, true).addContent(elem.removeContent());
1012
                            childCreator.getChild(bodyChild, true).addContent(elem.removeContent());
1008
                        } else {
1013
                        } else {
1009
                            Log.get().fine("Ignoring in " + part + " : " + bodyChild);
1014
                            Log.get().fine("Ignoring in " + part + " : " + bodyChild);
1010
                        }
1015
                        }
1011
                        final int postSize = thisBody.getContentSize();
1016
                        final int postSize = thisBody.getContentSize();
1012
                        prologueAddedCount += postSize - preSize;
1017
                        prologueAddedCount += postSize - preSize;
1013
                    } else if (part == ContentPart.MAIN) {
1018
                    } else if (part == ContentPart.MAIN) {
1014
                        mainElements.add(this.prefix((Element) bodyChild.clone(), true));
1019
                        mainElements.add(this.prefix((Element) bodyChild.clone(), true));
1015
                    } else if (part == ContentPart.EPILOGUE) {
1020
                    } else if (part == ContentPart.EPILOGUE) {
1016
                        Log.get().fine("Ignoring in " + part + " : " + bodyChild);
1021
                        Log.get().fine("Ignoring in " + part + " : " + bodyChild);
1017
                    }
1022
                    }
1018
                } else if (part == ContentPart.MAIN) {
1023
                } else if (part == ContentPart.MAIN) {
1019
                    mainElements.add((Content) c.clone());
1024
                    mainElements.add((Content) c.clone());
1020
                } else {
1025
                } else {
1021
                    Log.get().finer("Ignoring non-element in " + part);
1026
                    Log.get().finer("Ignoring non-element in " + part);
1022
                }
1027
                }
1023
            }
1028
            }
1024
        }
1029
        }
1025
 
1030
 
1026
        if (nonNullWhere == thisBody) {
1031
        if (nonNullWhere == thisBody) {
1027
            assert index >= 0;
1032
            assert index >= 0;
1028
            index += prologueAddedCount;
1033
            index += prologueAddedCount;
1029
            assert index >= 0 && index <= thisBody.getContentSize();
1034
            assert index >= 0 && index <= thisBody.getContentSize();
1030
        }
1035
        }
1031
        if (index < 0)
1036
        if (index < 0)
1032
            nonNullWhere.addContent(mainElements);
1037
            nonNullWhere.addContent(mainElements);
1033
        else
1038
        else
1034
            nonNullWhere.addContent(index, mainElements);
1039
            nonNullWhere.addContent(index, mainElements);
1035
    }
1040
    }
1036
 
1041
 
1037
    /**
1042
    /**
1038
     * Detach the children of elem whose names already exist in the body.
1043
     * Detach the children of elem whose names already exist in the body.
1039
     * 
1044
     * 
1040
     * @param elem the elem to be trimmed.
1045
     * @param elem the elem to be trimmed.
1041
     * @throws JDOMException if an error occurs.
1046
     * @throws JDOMException if an error occurs.
1042
     */
1047
     */
1043
    protected final void detachDuplicate(Element elem) throws JDOMException {
1048
    protected final void detachDuplicate(Element elem) throws JDOMException {
1044
        final String singularName = elem.getName().substring(0, elem.getName().length() - 1);
1049
        final String singularName = elem.getName().substring(0, elem.getName().length() - 1);
1045
        final List thisNames = getXPath("./text:" + singularName + "s/text:" + singularName + "/@text:name").selectNodes(getChild("body"));
1050
        final SimpleXMLPath<Attribute> simplePath = SimpleXMLPath.create(createElementStep(singularName + "s", "text"), createElementStep(singularName, "text"), createAttributeStep("name", "text"));
1046
        org.apache.commons.collections.CollectionUtils.transform(thisNames, new Transformer() {
-
 
1047
            public Object transform(Object obj) {
-
 
1048
                return ((Attribute) obj).getValue();
1051
        final List<String> thisNames = simplePath.selectValues(getChild("body"));
1049
            }
-
 
1050
        });
-
 
1051
 
1052
 
1052
        final Iterator iter = elem.getChildren().iterator();
1053
        final Iterator iter = elem.getChildren().iterator();
1053
        while (iter.hasNext()) {
1054
        while (iter.hasNext()) {
1054
            final Element decl = (Element) iter.next();
1055
            final Element decl = (Element) iter.next();
1055
            if (thisNames.contains(decl.getAttributeValue("name", getVersion().getTEXT()))) {
1056
            if (thisNames.contains(decl.getAttributeValue("name", getVersion().getTEXT()))) {
1056
                // on retire les déjà existant
1057
                // on retire les déjà existant
1057
                iter.remove();
1058
                iter.remove();
1058
            }
1059
            }
1059
        }
1060
        }
1060
    }
1061
    }
1061
 
1062
 
1062
    // *** Utils
1063
    // *** Utils
1063
 
1064
 
1064
    public final Element getBody() {
1065
    public final Element getBody() {
1065
        return this.getContentTypeVersioned().getBody(getDocument());
1066
        return this.getContentTypeVersioned().getBody(getDocument());
1066
    }
1067
    }
1067
 
1068
 
1068
    private ContentTypeVersioned getContentTypeVersioned() {
1069
    private ContentTypeVersioned getContentTypeVersioned() {
1069
        return getPackage().getContentType();
1070
        return getPackage().getContentType();
1070
    }
1071
    }
1071
 
1072
 
1072
    /**
1073
    /**
1073
     * Préfixe les attributs en ayant besoin.
1074
     * Préfixe les attributs en ayant besoin.
1074
     * 
1075
     * 
1075
     * @param elem l'élément à préfixer.
1076
     * @param elem l'élément à préfixer.
1076
     * @param references whether to prefix hrefs.
1077
     * @param references whether to prefix hrefs.
1077
     * @return <code>elem</code>.
1078
     * @return <code>elem</code>.
1078
     * @throws JDOMException if an error occurs.
1079
     * @throws JDOMException if an error occurs.
1079
     */
1080
     */
1080
    Element prefix(Element elem, boolean references) throws JDOMException {
1081
    Element prefix(Element elem, boolean references) throws JDOMException {
1081
        Iterator attrs = this.getXPath(".//@text:style-name | .//@table:style-name | .//@draw:style-name | .//@style:data-style-name | .//@style:apply-style-name").selectNodes(elem).iterator();
1082
        Iterator attrs = this.getXPath(".//@text:style-name | .//@table:style-name | .//@draw:style-name | .//@style:data-style-name | .//@style:apply-style-name").selectNodes(elem).iterator();
1082
        while (attrs.hasNext()) {
1083
        while (attrs.hasNext()) {
1083
            Attribute attr = (Attribute) attrs.next();
1084
            Attribute attr = (Attribute) attrs.next();
1084
            // text:list/@text:style-name references text:list-style
1085
            // text:list/@text:style-name references text:list-style
1085
            if (!this.listStylesNames.contains(attr.getValue()) && !this.stylesNames.contains(attr.getValue())) {
1086
            if (!this.listStylesNames.contains(attr.getValue()) && !this.stylesNames.contains(attr.getValue())) {
1086
                attr.setValue(this.prefix(attr.getValue()));
1087
                attr.setValue(this.prefix(attr.getValue()));
1087
            }
1088
            }
1088
        }
1089
        }
1089
 
1090
 
1090
        attrs = this.getXPath(".//@style:list-style-name").selectNodes(elem).iterator();
1091
        attrs = this.getXPath(".//@style:list-style-name").selectNodes(elem).iterator();
1091
        while (attrs.hasNext()) {
1092
        while (attrs.hasNext()) {
1092
            Attribute attr = (Attribute) attrs.next();
1093
            Attribute attr = (Attribute) attrs.next();
1093
            if (!this.listStylesNames.contains(attr.getValue())) {
1094
            if (!this.listStylesNames.contains(attr.getValue())) {
1094
                attr.setValue(this.prefix(attr.getValue()));
1095
                attr.setValue(this.prefix(attr.getValue()));
1095
            }
1096
            }
1096
        }
1097
        }
1097
 
1098
 
1098
        attrs = this.getXPath(".//@style:page-master-name | .//@style:page-layout-name | .//@text:name | .//@form:name | .//@form:property-name").selectNodes(elem).iterator();
1099
        attrs = this.getXPath(".//@style:page-master-name | .//@style:page-layout-name | .//@text:name | .//@form:name | .//@form:property-name").selectNodes(elem).iterator();
1099
        while (attrs.hasNext()) {
1100
        while (attrs.hasNext()) {
1100
            final Attribute attr = (Attribute) attrs.next();
1101
            final Attribute attr = (Attribute) attrs.next();
1101
            final String parentName = attr.getParent().getName();
1102
            final String parentName = attr.getParent().getName();
1102
            if (!DONT_PREFIX.contains(parentName))
1103
            if (!DONT_PREFIX.contains(parentName))
1103
                attr.setValue(this.prefix(attr.getValue()));
1104
                attr.setValue(this.prefix(attr.getValue()));
1104
        }
1105
        }
1105
 
1106
 
1106
        // prefix references
1107
        // prefix references
1107
        if (references) {
1108
        if (references) {
1108
            attrs = this.getXPath(".//@xlink:href[../@xlink:show='embed']").selectNodes(elem).iterator();
1109
            attrs = this.getXPath(".//@xlink:href[../@xlink:show='embed']").selectNodes(elem).iterator();
1109
            while (attrs.hasNext()) {
1110
            while (attrs.hasNext()) {
1110
                final Attribute attr = (Attribute) attrs.next();
1111
                final Attribute attr = (Attribute) attrs.next();
1111
                final String prefixedPath = this.prefixPath(attr.getValue());
1112
                final String prefixedPath = this.prefixPath(attr.getValue());
1112
                if (prefixedPath != null)
1113
                if (prefixedPath != null)
1113
                    attr.setValue(prefixedPath);
1114
                    attr.setValue(prefixedPath);
1114
            }
1115
            }
1115
        }
1116
        }
1116
        return elem;
1117
        return elem;
1117
    }
1118
    }
1118
 
1119
 
1119
    /**
1120
    /**
1120
     * Prefix a path.
1121
     * Prefix a path.
1121
     * 
1122
     * 
1122
     * @param href a path inside the pkg, eg "./Object 1/content.xml".
1123
     * @param href a path inside the pkg, eg "./Object 1/content.xml".
1123
     * @return the prefixed path or <code>null</code> if href is external, eg "./3_Object
1124
     * @return the prefixed path or <code>null</code> if href is external, eg "./3_Object
1124
     *         1/content.xml".
1125
     *         1/content.xml".
1125
     */
1126
     */
1126
    private String prefixPath(final String href) {
1127
    private String prefixPath(final String href) {
1127
        if (this.getVersion().equals(XMLVersion.OOo)) {
1128
        if (this.getVersion().equals(XMLVersion.OOo)) {
1128
            // in OOo 1.x inPKG is denoted by a #
1129
            // in OOo 1.x inPKG is denoted by a #
1129
            final boolean sharp = href.startsWith("#");
1130
            final boolean sharp = href.startsWith("#");
1130
            if (sharp)
1131
            if (sharp)
1131
                // eg #Pictures/100000000000006C000000ABCC02339E.png
1132
                // eg #Pictures/100000000000006C000000ABCC02339E.png
1132
                return "#" + this.prefix(href.substring(1));
1133
                return "#" + this.prefix(href.substring(1));
1133
            else
1134
            else
1134
                // eg ../../../../Program%20Files/OpenOffice.org1.1.5/share/gallery/apples.gif
1135
                // eg ../../../../Program%20Files/OpenOffice.org1.1.5/share/gallery/apples.gif
1135
                return null;
1136
                return null;
1136
        } else {
1137
        } else {
1137
            URI uri;
1138
            URI uri;
1138
            try {
1139
            try {
1139
                uri = new URI(href);
1140
                uri = new URI(href);
1140
            } catch (URISyntaxException e) {
1141
            } catch (URISyntaxException e) {
1141
                // OO doesn't escape characters for files
1142
                // OO doesn't escape characters for files
1142
                uri = null;
1143
                uri = null;
1143
            }
1144
            }
1144
            // section 17.5
1145
            // section 17.5
1145
            final boolean inPKGFile = uri == null || uri.getScheme() == null && uri.getAuthority() == null && uri.getPath().charAt(0) != '/';
1146
            final boolean inPKGFile = uri == null || uri.getScheme() == null && uri.getAuthority() == null && uri.getPath().charAt(0) != '/';
1146
            if (inPKGFile) {
1147
            if (inPKGFile) {
1147
                final String dotSlash = "./";
1148
                final String dotSlash = "./";
1148
                if (href.startsWith(dotSlash))
1149
                if (href.startsWith(dotSlash))
1149
                    return dotSlash + this.prefix(href.substring(dotSlash.length()));
1150
                    return dotSlash + this.prefix(href.substring(dotSlash.length()));
1150
                else
1151
                else
1151
                    return this.prefix(href);
1152
                    return this.prefix(href);
1152
            } else
1153
            } else
1153
                return null;
1154
                return null;
1154
        }
1155
        }
1155
    }
1156
    }
1156
 
1157
 
1157
    private String prefix(String value) {
1158
    private String prefix(String value) {
1158
        return "_" + this.numero + value;
1159
        return "_" + this.numero + value;
1159
    }
1160
    }
1160
 
1161
 
1161
    private final ElementTransformer prefixTransf = new ElementTransformer() {
1162
    private final ElementTransformer prefixTransf = new ElementTransformer() {
1162
        public Element transform(Element elem) throws JDOMException {
1163
        public Element transform(Element elem) throws JDOMException {
1163
            return ODSingleXMLDocument.this.prefix(elem, true);
1164
            return ODSingleXMLDocument.this.prefix(elem, true);
1164
        }
1165
        }
1165
    };
1166
    };
1166
 
1167
 
1167
    private final ElementTransformer prefixTransfNoRef = new ElementTransformer() {
1168
    private final ElementTransformer prefixTransfNoRef = new ElementTransformer() {
1168
        public Element transform(Element elem) throws JDOMException {
1169
        public Element transform(Element elem) throws JDOMException {
1169
            return ODSingleXMLDocument.this.prefix(elem, false);
1170
            return ODSingleXMLDocument.this.prefix(elem, false);
1170
        }
1171
        }
1171
    };
1172
    };
1172
 
1173
 
1173
    /**
1174
    /**
1174
     * Ajoute dans ce document seulement les éléments de doc correspondant au XPath spécifié et dont
1175
     * Ajoute dans ce document seulement les éléments de doc correspondant au XPath spécifié et dont
1175
     * la valeur de l'attribut style:name n'existe pas déjà.
1176
     * la valeur de l'attribut style:name n'existe pas déjà.
1176
     * 
1177
     * 
1177
     * @param doc le document à fusionner avec celui-ci.
1178
     * @param doc le document à fusionner avec celui-ci.
1178
     * @param topElem eg "office:font-decls".
1179
     * @param topElem eg "office:font-decls".
1179
     * @param elemToMerge les éléments à fusionner (par rapport à topElem), eg "style:font-decl".
1180
     * @param elemToMerge les éléments à fusionner (par rapport à topElem), eg "style:font-decl".
1180
     * @return les noms des éléments ajoutés.
1181
     * @return les noms des éléments ajoutés.
1181
     * @throws JDOMException
1182
     * @throws JDOMException
1182
     * @see #mergeUnique(ODSingleXMLDocument, String, String, ElementTransformer)
1183
     * @see #mergeUnique(ODSingleXMLDocument, String, String, ElementTransformer)
1183
     */
1184
     */
1184
    private List<String> mergeUnique(ODXMLDocument doc, String topElem, String elemToMerge) throws JDOMException {
1185
    private List<String> mergeUnique(ODXMLDocument doc, String topElem, String elemToMerge) throws JDOMException {
1185
        return this.mergeUnique(doc, topElem, elemToMerge, NOP_ElementTransformer);
1186
        return this.mergeUnique(doc, topElem, elemToMerge, NOP_ElementTransformer);
1186
    }
1187
    }
1187
 
1188
 
1188
    /**
1189
    /**
1189
     * Ajoute dans ce document seulement les éléments de doc correspondant au XPath spécifié et dont
1190
     * Ajoute dans ce document seulement les éléments de doc correspondant au XPath spécifié et dont
1190
     * la valeur de l'attribut style:name n'existe pas déjà. En conséquence n'ajoute que les
1191
     * la valeur de l'attribut style:name n'existe pas déjà. En conséquence n'ajoute que les
1191
     * éléments possédant un attribut style:name.
1192
     * éléments possédant un attribut style:name.
1192
     * 
1193
     * 
1193
     * @param doc le document à fusionner avec celui-ci.
1194
     * @param doc le document à fusionner avec celui-ci.
1194
     * @param topElem eg "office:font-decls".
1195
     * @param topElem eg "office:font-decls".
1195
     * @param elemToMerge les éléments à fusionner (par rapport à topElem), eg "style:font-decl".
1196
     * @param elemToMerge les éléments à fusionner (par rapport à topElem), eg "style:font-decl".
1196
     * @param addTransf la transformation à appliquer avant d'ajouter.
1197
     * @param addTransf la transformation à appliquer avant d'ajouter.
1197
     * @return les noms des éléments ajoutés.
1198
     * @return les noms des éléments ajoutés.
1198
     * @throws JDOMException
1199
     * @throws JDOMException
1199
     */
1200
     */
1200
    private List<String> mergeUnique(ODXMLDocument doc, String topElem, String elemToMerge, ElementTransformer addTransf) throws JDOMException {
1201
    private List<String> mergeUnique(ODXMLDocument doc, String topElem, String elemToMerge, ElementTransformer addTransf) throws JDOMException {
1201
        return this.mergeUnique(doc, topElem, elemToMerge, "style:name", addTransf);
1202
        return this.mergeUnique(doc, topElem, elemToMerge, "style:name", addTransf);
1202
    }
1203
    }
1203
 
1204
 
1204
    private List<String> mergeUnique(ODXMLDocument doc, String topElem, String elemToMerge, String attrFQName, ElementTransformer addTransf) throws JDOMException {
1205
    private List<String> mergeUnique(ODXMLDocument doc, String topElem, String elemToMerge, String attrFQName, ElementTransformer addTransf) throws JDOMException {
-
 
1206
        final Element other = doc.getChild(topElem);
-
 
1207
        if (other == null)
-
 
1208
            return Collections.emptyList();
1205
        List<String> added = new ArrayList<String>();
1209
        List<String> added = new ArrayList<String>();
1206
        Element thisParent = this.getChild(topElem, true);
1210
        Element thisParent = this.getChild(topElem, true);
1207
 
1211
 
1208
        XPath xp = this.getXPath("./" + elemToMerge + "/@" + attrFQName);
1212
        final SimpleXMLPath<Attribute> simplePath = SimpleXMLPath.create(createElementStepFromQualifiedName(elemToMerge), createAttributeStepFromQualifiedName(attrFQName));
1209
 
1213
 
1210
        // les styles de ce document
1214
        // les styles de ce document
1211
        List thisElemNames = xp.selectNodes(thisParent);
1215
        final List<String> thisElemNames = simplePath.selectValues(thisParent);
1212
        // on transforme la liste d'attributs en liste de String
-
 
1213
        org.apache.commons.collections.CollectionUtils.transform(thisElemNames, new Transformer() {
-
 
1214
            public Object transform(Object obj) {
-
 
1215
                return ((Attribute) obj).getValue();
-
 
1216
            }
-
 
1217
        });
-
 
1218
 
1216
 
1219
        // pour chaque style de l'autre document
1217
        // pour chaque style de l'autre document
1220
        Iterator otherElemNames = xp.selectNodes(doc.getChild(topElem)).iterator();
-
 
1221
        while (otherElemNames.hasNext()) {
-
 
1222
            Attribute attr = (Attribute) otherElemNames.next();
1218
        for (final Attribute attr : simplePath.selectNodes(other)) {
1223
            // on l'ajoute si non déjà dedans
1219
            // on l'ajoute si non déjà dedans
1224
            if (!thisElemNames.contains(attr.getValue())) {
1220
            if (!thisElemNames.contains(attr.getValue())) {
1225
                thisParent.addContent(addTransf.transform((Element) attr.getParent().clone()));
1221
                thisParent.addContent(addTransf.transform((Element) attr.getParent().clone()));
1226
                added.add(attr.getValue());
1222
                added.add(attr.getValue());
1227
            }
1223
            }
1228
        }
1224
        }
1229
 
1225
 
1230
        return added;
1226
        return added;
1231
    }
1227
    }
1232
 
1228
 
1233
    /**
1229
    /**
1234
     * Ajoute l'élément elemName de doc, s'il n'est pas dans ce document.
1230
     * Ajoute l'élément elemName de doc, s'il n'est pas dans ce document.
1235
     * 
1231
     * 
1236
     * @param doc le document à fusionner avec celui-ci.
1232
     * @param doc le document à fusionner avec celui-ci.
1237
     * @param elemName l'élément à ajouter, eg "outline-style".
1233
     * @param elemName l'élément à ajouter, eg "outline-style".
1238
     * @throws JDOMException if elemName is not valid.
1234
     * @throws JDOMException if elemName is not valid.
1239
     */
1235
     */
1240
    private void addStylesIfNotPresent(ODXMLDocument doc, String elemName) throws JDOMException {
1236
    private void addStylesIfNotPresent(ODXMLDocument doc, String elemName) throws JDOMException {
1241
        this.addIfNotPresent(doc, "./office:styles/text:" + elemName);
1237
        this.addIfNotPresent(doc, "./office:styles/text:" + elemName);
1242
    }
1238
    }
1243
 
1239
 
1244
    /**
1240
    /**
1245
     * Prefixe les fils de auto-styles possédant un attribut "name" avant de les ajouter.
1241
     * Prefixe les fils de auto-styles possédant un attribut "name" avant de les ajouter.
1246
     * 
1242
     * 
1247
     * @param doc le document à fusionner avec celui-ci.
1243
     * @param doc le document à fusionner avec celui-ci.
1248
     * @return les élément ayant été ajoutés.
1244
     * @return les élément ayant été ajoutés.
1249
     * @throws JDOMException
1245
     * @throws JDOMException
1250
     */
1246
     */
1251
    private List<Element> prefixAndAddAutoStyles(ODXMLDocument doc) throws JDOMException {
1247
    private List<Element> prefixAndAddAutoStyles(ODXMLDocument doc) throws JDOMException {
1252
        return addStyles(doc, "automatic-styles", Step.getAnyChildElementStep(), true);
1248
        return addStyles(doc, "automatic-styles", Step.getAnyChildElementStep(), true);
1253
    }
1249
    }
1254
 
1250
 
1255
    // add styles from doc/rootElem/styleElemStep/@style:name, optionally prefixing
1251
    // add styles from doc/rootElem/styleElemStep/@style:name, optionally prefixing
1256
    private List<Element> addStyles(ODXMLDocument doc, final String rootElem, final Step<Element> styleElemStep, boolean prefix) throws JDOMException {
1252
    private List<Element> addStyles(ODXMLDocument doc, final String rootElem, final Step<Element> styleElemStep, boolean prefix) throws JDOMException {
1257
        // needed since we add to us directly under rootElem
1253
        // needed since we add to us directly under rootElem
1258
        if (styleElemStep.getAxis() != Axis.child)
1254
        if (styleElemStep.getAxis() != Axis.child)
1259
            throw new IllegalArgumentException("Not child axis : " + styleElemStep.getAxis());
1255
            throw new IllegalArgumentException("Not child axis : " + styleElemStep.getAxis());
1260
        final List<Element> result = new ArrayList<Element>(128);
1256
        final List<Element> result = new ArrayList<Element>(128);
1261
        final Element thisChild = this.getChild(rootElem);
1257
        final Element thisChild = this.getChild(rootElem);
1262
        // find all elements with a style:name in doc
1258
        // find all elements with a style:name in doc
1263
        final SimpleXMLPath<Attribute> simplePath = SimpleXMLPath.create(styleElemStep, Step.createAttributeStep("name", "style"));
1259
        final SimpleXMLPath<Attribute> simplePath = SimpleXMLPath.create(styleElemStep, Step.createAttributeStep("name", "style"));
1264
        for (final Attribute attr : simplePath.selectNodes(doc.getChild(rootElem))) {
1260
        for (final Attribute attr : simplePath.selectNodes(doc.getChild(rootElem))) {
1265
            final Element parent = (Element) attr.getParent().clone();
1261
            final Element parent = (Element) attr.getParent().clone();
1266
            // prefix their name
1262
            // prefix their name
1267
            if (prefix)
1263
            if (prefix)
1268
                parent.setAttribute(attr.getName(), this.prefix(attr.getValue()), attr.getNamespace());
1264
                parent.setAttribute(attr.getName(), this.prefix(attr.getValue()), attr.getNamespace());
1269
            // and add to us
1265
            // and add to us
1270
            thisChild.addContent(parent);
1266
            thisChild.addContent(parent);
1271
            result.add(parent);
1267
            result.add(parent);
1272
        }
1268
        }
1273
        return result;
1269
        return result;
1274
    }
1270
    }
1275
 
1271
 
1276
    /**
1272
    /**
1277
     * Return <code>true</code> if this document was split.
1273
     * Return <code>true</code> if this document was split.
1278
     * 
1274
     * 
1279
     * @return <code>true</code> if this has no package anymore.
1275
     * @return <code>true</code> if this has no package anymore.
1280
     * @see ODPackage#split()
1276
     * @see ODPackage#split()
1281
     */
1277
     */
1282
    public final boolean isDead() {
1278
    public final boolean isDead() {
1283
        return this.getPackage() == null;
1279
        return this.getPackage() == null;
1284
    }
1280
    }
1285
 
1281
 
1286
    final Map<RootElement, Document> split() {
1282
    final Map<RootElement, Document> split() {
1287
        final Map<RootElement, Document> res = new HashMap<RootElement, Document>();
1283
        final Map<RootElement, Document> res = new HashMap<RootElement, Document>();
1288
        final XMLVersion version = getVersion();
1284
        final XMLVersion version = getVersion();
1289
        final Element root = this.getDocument().getRootElement();
1285
        final Element root = this.getDocument().getRootElement();
1290
        final XMLFormatVersion officeVersion = getFormatVersion();
1286
        final XMLFormatVersion officeVersion = getFormatVersion();
1291
 
1287
 
1292
        // meta
1288
        // meta
1293
        {
1289
        {
1294
            final Element thisMeta = root.getChild("meta", version.getOFFICE());
1290
            final Element thisMeta = root.getChild("meta", version.getOFFICE());
1295
            if (thisMeta != null) {
1291
            if (thisMeta != null) {
1296
                final Document meta = createDocument(res, RootElement.META, officeVersion);
1292
                final Document meta = createDocument(res, RootElement.META, officeVersion);
1297
                meta.getRootElement().addContent(thisMeta.detach());
1293
                meta.getRootElement().addContent(thisMeta.detach());
1298
            }
1294
            }
1299
        }
1295
        }
1300
        // settings
1296
        // settings
1301
        {
1297
        {
1302
            final Element thisSettings = root.getChild("settings", version.getOFFICE());
1298
            final Element thisSettings = root.getChild("settings", version.getOFFICE());
1303
            if (thisSettings != null) {
1299
            if (thisSettings != null) {
1304
                final Document settings = createDocument(res, RootElement.SETTINGS, officeVersion);
1300
                final Document settings = createDocument(res, RootElement.SETTINGS, officeVersion);
1305
                settings.getRootElement().addContent(thisSettings.detach());
1301
                settings.getRootElement().addContent(thisSettings.detach());
1306
            }
1302
            }
1307
        }
1303
        }
1308
        // scripts
1304
        // scripts
1309
        {
1305
        {
1310
            final Element thisScript = this.getBasicScriptElem();
1306
            final Element thisScript = this.getBasicScriptElem();
1311
            if (thisScript != null) {
1307
            if (thisScript != null) {
1312
                final Map<String, Library> basicLibraries = this.readBasicLibraries(thisScript).get0();
1308
                final Map<String, Library> basicLibraries = this.readBasicLibraries(thisScript).get0();
1313
                final Element lcRootElem = new Element("libraries", XMLVersion.LIBRARY_NS);
1309
                final Element lcRootElem = new Element("libraries", XMLVersion.LIBRARY_NS);
1314
                for (final Library lib : basicLibraries.values()) {
1310
                for (final Library lib : basicLibraries.values()) {
1315
                    lcRootElem.addContent(lib.toPackageLibrariesElement(officeVersion));
1311
                    lcRootElem.addContent(lib.toPackageLibrariesElement(officeVersion));
1316
                    for (final Entry<String, Document> e : lib.toPackageDocuments(officeVersion).entrySet()) {
1312
                    for (final Entry<String, Document> e : lib.toPackageDocuments(officeVersion).entrySet()) {
1317
                        this.pkg.putFile(Library.DIR_NAME + "/" + lib.getName() + "/" + e.getKey(), e.getValue(), FileUtils.XML_TYPE);
1313
                        this.pkg.putFile(Library.DIR_NAME + "/" + lib.getName() + "/" + e.getKey(), e.getValue(), FileUtils.XML_TYPE);
1318
                    }
1314
                    }
1319
                }
1315
                }
1320
                final Document lc = new Document(lcRootElem, new DocType("library:libraries", "-//OpenOffice.org//DTD OfficeDocument 1.0//EN", "libraries.dtd"));
1316
                final Document lc = new Document(lcRootElem, new DocType("library:libraries", "-//OpenOffice.org//DTD OfficeDocument 1.0//EN", "libraries.dtd"));
1321
                this.pkg.putFile(Library.DIR_NAME + "/" + Library.LIBRARY_LIST_FILENAME, lc, FileUtils.XML_TYPE);
1317
                this.pkg.putFile(Library.DIR_NAME + "/" + Library.LIBRARY_LIST_FILENAME, lc, FileUtils.XML_TYPE);
1322
                thisScript.detach();
1318
                thisScript.detach();
1323
                // nothing to do for dialogs, since they cannot be in our Document
1319
                // nothing to do for dialogs, since they cannot be in our Document
1324
            }
1320
            }
1325
        }
1321
        }
1326
        // styles
1322
        // styles
1327
        // we must move office:styles, office:master-styles and referenced office:automatic-styles
1323
        // we must move office:styles, office:master-styles and referenced office:automatic-styles
1328
        {
1324
        {
1329
            final Document styles = createDocument(res, RootElement.STYLES, officeVersion);
1325
            final Document styles = createDocument(res, RootElement.STYLES, officeVersion);
1330
            // don't bother finding out which font is used where since there isn't that many of them
1326
            // don't bother finding out which font is used where since there isn't that many of them
1331
            styles.getRootElement().addContent((Element) root.getChild(getFontDecls()[0], version.getOFFICE()).clone());
1327
            styles.getRootElement().addContent((Element) root.getChild(getFontDecls()[0], version.getOFFICE()).clone());
1332
            // extract common styles
1328
            // extract common styles
1333
            styles.getRootElement().addContent(root.getChild("styles", version.getOFFICE()).detach());
1329
            styles.getRootElement().addContent(root.getChild("styles", version.getOFFICE()).detach());
1334
            // only automatic styles used in the styles themselves.
1330
            // only automatic styles used in the styles themselves.
1335
            final Element contentAutoStyles = root.getChild("automatic-styles", version.getOFFICE());
1331
            final Element contentAutoStyles = root.getChild("automatic-styles", version.getOFFICE());
1336
            final Element stylesAutoStyles = new Element(contentAutoStyles.getName(), contentAutoStyles.getNamespace());
1332
            final Element stylesAutoStyles = new Element(contentAutoStyles.getName(), contentAutoStyles.getNamespace());
1337
            final Element masterStyles = root.getChild("master-styles", version.getOFFICE());
1333
            final Element masterStyles = root.getChild("master-styles", version.getOFFICE());
1338
 
1334
 
1339
            // style elements referenced, e.g. <style:page-layout style:name="pm1">
1335
            // style elements referenced, e.g. <style:page-layout style:name="pm1">
1340
            final Set<Element> referenced = new HashSet<Element>();
1336
            final Set<Element> referenced = new HashSet<Element>();
1341
 
1337
 
1342
            final SimpleXMLPath<Attribute> descAttrs = SimpleXMLPath.create(Step.createElementStep(Axis.descendantOrSelf, null), Step.createAttributeStep(null, null));
1338
            final SimpleXMLPath<Attribute> descAttrs = SimpleXMLPath.create(Step.createElementStep(Axis.descendantOrSelf, null), Step.createAttributeStep(null, null));
1343
            for (final Attribute attr : descAttrs.selectNodes(masterStyles)) {
1339
            for (final Attribute attr : descAttrs.selectNodes(masterStyles)) {
1344
                final Element referencedStyleElement = Style.getReferencedStyleElement(this.pkg, attr);
1340
                final Element referencedStyleElement = Style.getReferencedStyleElement(this.pkg, attr);
1345
                if (referencedStyleElement != null)
1341
                if (referencedStyleElement != null)
1346
                    referenced.add(referencedStyleElement);
1342
                    referenced.add(referencedStyleElement);
1347
            }
1343
            }
1348
            for (final Element r : referenced) {
1344
            for (final Element r : referenced) {
1349
                // since we already removed common styles
1345
                // since we already removed common styles
1350
                assert r.getParentElement() == contentAutoStyles;
1346
                assert r.getParentElement() == contentAutoStyles;
1351
                stylesAutoStyles.addContent(r.detach());
1347
                stylesAutoStyles.addContent(r.detach());
1352
            }
1348
            }
1353
 
1349
 
1354
            styles.getRootElement().addContent(stylesAutoStyles);
1350
            styles.getRootElement().addContent(stylesAutoStyles);
1355
            styles.getRootElement().addContent(masterStyles.detach());
1351
            styles.getRootElement().addContent(masterStyles.detach());
1356
        }
1352
        }
1357
        // content
1353
        // content
1358
        {
1354
        {
1359
            // store before emptying package
1355
            // store before emptying package
1360
            final ContentTypeVersioned contentTypeVersioned = getContentTypeVersioned();
1356
            final ContentTypeVersioned contentTypeVersioned = getContentTypeVersioned();
1361
            // needed since the content will be emptied (which can cause methods of ODPackage to
1357
            // needed since the content will be emptied (which can cause methods of ODPackage to
1362
            // fail, e.g. setTypeAndVersion())
1358
            // fail, e.g. setTypeAndVersion())
1363
            this.pkg.rmFile(RootElement.CONTENT.getZipEntry());
1359
            this.pkg.rmFile(RootElement.CONTENT.getZipEntry());
1364
            this.pkg = null;
1360
            this.pkg = null;
1365
            final Document content = createDocument(res, RootElement.CONTENT, officeVersion);
1361
            final Document content = createDocument(res, RootElement.CONTENT, officeVersion);
1366
            contentTypeVersioned.setType(content);
1362
            contentTypeVersioned.setType(content);
1367
            content.getRootElement().addContent(root.removeContent());
1363
            content.getRootElement().addContent(root.removeContent());
1368
        }
1364
        }
1369
        return res;
1365
        return res;
1370
    }
1366
    }
1371
 
1367
 
1372
    private Document createDocument(final Map<RootElement, Document> res, RootElement rootElement, final XMLFormatVersion version) {
1368
    private Document createDocument(final Map<RootElement, Document> res, RootElement rootElement, final XMLFormatVersion version) {
1373
        final Document doc = rootElement.createDocument(version);
1369
        final Document doc = rootElement.createDocument(version);
1374
        copyNS(this.getDocument(), doc);
1370
        copyNS(this.getDocument(), doc);
1375
        res.put(rootElement, doc);
1371
        res.put(rootElement, doc);
1376
        return doc;
1372
        return doc;
1377
    }
1373
    }
1378
 
1374
 
1379
    /**
1375
    /**
1380
     * Saves this OO document to a file.
1376
     * Saves this OO document to a file.
1381
     * 
1377
     * 
1382
     * @param f the file where this document will be saved, without extension, eg "dir/myfile".
1378
     * @param f the file where this document will be saved, without extension, eg "dir/myfile".
1383
     * @return the actual file where it has been saved (with extension), eg "dir/myfile.odt".
1379
     * @return the actual file where it has been saved (with extension), eg "dir/myfile.odt".
1384
     * @throws IOException if an error occurs.
1380
     * @throws IOException if an error occurs.
1385
     */
1381
     */
1386
    public File saveToPackageAs(File f) throws IOException {
1382
    public File saveToPackageAs(File f) throws IOException {
1387
        return this.pkg.saveAs(f);
1383
        return this.pkg.saveAs(f);
1388
    }
1384
    }
1389
 
1385
 
1390
    public void saveToPackage(OutputStream f) throws IOException {
1386
    public void saveToPackage(OutputStream f) throws IOException {
1391
        this.pkg.save(f);
1387
        this.pkg.save(f);
1392
    }
1388
    }
1393
 
1389
 
1394
    public File save() throws IOException {
1390
    public File save() throws IOException {
1395
        return this.saveAs(this.getPackage().getFile());
1391
        return this.saveAs(this.getPackage().getFile());
1396
    }
1392
    }
1397
 
1393
 
1398
    public File saveAs(File fNoExt) throws IOException {
1394
    public File saveAs(File fNoExt) throws IOException {
1399
        final Document doc = (Document) getDocument().clone();
1395
        final Document doc = (Document) getDocument().clone();
1400
        for (final Attribute hrefAttr : ALL_HREF_ATTRIBUTES.selectNodes(doc.getRootElement())) {
1396
        for (final Attribute hrefAttr : ALL_HREF_ATTRIBUTES.selectNodes(doc.getRootElement())) {
1401
            final String href = hrefAttr.getValue();
1397
            final String href = hrefAttr.getValue();
1402
            if (href.startsWith("../")) {
1398
            if (href.startsWith("../")) {
1403
                // update href
1399
                // update href
1404
                hrefAttr.setValue(href.substring(3));
1400
                hrefAttr.setValue(href.substring(3));
1405
            } else if (!URI.create(href).isAbsolute()) {
1401
            } else if (!URI.create(href).isAbsolute()) {
1406
                // encode binaries
1402
                // encode binaries
1407
                final Element hrefParent = hrefAttr.getParent();
1403
                final Element hrefParent = hrefAttr.getParent();
1408
                if (!BINARY_DATA_PARENTS.contains(hrefParent.getQualifiedName()))
1404
                if (!BINARY_DATA_PARENTS.contains(hrefParent.getQualifiedName()))
1409
                    throw new IllegalStateException("Cannot convert to binary data element : " + hrefParent);
1405
                    throw new IllegalStateException("Cannot convert to binary data element : " + hrefParent);
1410
                final Element binaryData = new Element("binary-data", getPackage().getVersion().getOFFICE());
1406
                final Element binaryData = new Element("binary-data", getPackage().getVersion().getOFFICE());
1411
 
1407
 
1412
                binaryData.setText(Base64.encodeBytes(getPackage().getBinaryFile(href)));
1408
                binaryData.setText(Base64.encodeBytesBreakLines(getPackage().getBinaryFile(href)));
1413
                hrefParent.addContent(binaryData);
1409
                hrefParent.addContent(binaryData);
1414
                // If this element is present, an xlink:href attribute in its parent element
1410
                // If this element is present, an xlink:href attribute in its parent element
1415
                // shall be ignored. But LO doesn't respect that
1411
                // shall be ignored. But LO doesn't respect that
1416
                hrefAttr.detach();
1412
                hrefAttr.detach();
1417
            }
1413
            }
1418
        }
1414
        }
1419
 
1415
 
1420
        final File f = this.getPackage().getContentType().addExt(fNoExt, true);
1416
        final File f = this.getPackage().getContentType().addExt(fNoExt, true);
-
 
1417
        try (final FileOutputStream outs = new FileOutputStream(f)) {
-
 
1418
            // Use OutputStream parameter so that the encoding used for the Writer matches the
-
 
1419
            // XML declaration.
1421
        FileUtils.write(ODPackage.createOutputter().outputString(doc), f);
1420
            ODPackage.createOutputter().output(doc, outs);
-
 
1421
        }
1422
        return f;
1422
        return f;
1423
    }
1423
    }
1424
 
1424
 
1425
    private Element getPageBreak() {
1425
    private Element getPageBreak() {
1426
        if (this.pageBreak == null) {
1426
        if (this.pageBreak == null) {
1427
            final String styleName = "PageBreak";
1427
            final String styleName = "PageBreak";
1428
            try {
1428
            try {
1429
                final XPath xp = this.getXPath("./style:style[@style:name='" + styleName + "']");
1429
                final XPath xp = this.getXPath("./style:style[@style:name='" + styleName + "']");
1430
                final Element styles = this.getChild("styles", true);
1430
                final Element styles = this.getChild("styles", true);
1431
                if (xp.selectSingleNode(styles) == null) {
1431
                if (xp.selectSingleNode(styles) == null) {
1432
                    final Element pageBreakStyle = new Element("style", this.getVersion().getSTYLE());
1432
                    final Element pageBreakStyle = new Element("style", this.getVersion().getSTYLE());
1433
                    pageBreakStyle.setAttribute("name", styleName, this.getVersion().getSTYLE());
1433
                    pageBreakStyle.setAttribute("name", styleName, this.getVersion().getSTYLE());
1434
                    pageBreakStyle.setAttribute("family", "paragraph", this.getVersion().getSTYLE());
1434
                    pageBreakStyle.setAttribute("family", "paragraph", this.getVersion().getSTYLE());
1435
                    pageBreakStyle.setContent(getPProps().setAttribute("break-after", "page", this.getVersion().getNS("fo")));
1435
                    pageBreakStyle.setContent(getPProps().setAttribute("break-after", "page", this.getVersion().getNS("fo")));
1436
                    // <element name="office:styles"> <interleave>...
1436
                    // <element name="office:styles"> <interleave>...
1437
                    // so just append the new style
1437
                    // so just append the new style
1438
                    styles.addContent(pageBreakStyle);
1438
                    styles.addContent(pageBreakStyle);
1439
                    this.stylesNames.add(styleName);
1439
                    this.stylesNames.add(styleName);
1440
                }
1440
                }
1441
            } catch (JDOMException e) {
1441
            } catch (JDOMException e) {
1442
                // static path, shouldn't happen
1442
                // static path, shouldn't happen
1443
                throw new IllegalStateException("pb while searching for " + styleName, e);
1443
                throw new IllegalStateException("pb while searching for " + styleName, e);
1444
            }
1444
            }
1445
            this.pageBreak = new Element("p", this.getVersion().getTEXT()).setAttribute("style-name", styleName, this.getVersion().getTEXT());
1445
            this.pageBreak = new Element("p", this.getVersion().getTEXT()).setAttribute("style-name", styleName, this.getVersion().getTEXT());
1446
        }
1446
        }
1447
        return (Element) this.pageBreak.clone();
1447
        return (Element) this.pageBreak.clone();
1448
    }
1448
    }
1449
 
1449
 
1450
    private final Element getPProps() {
1450
    private final Element getPProps() {
1451
        return this.getXML().createFormattingProperties("paragraph");
1451
        return this.getXML().createFormattingProperties("paragraph");
1452
    }
1452
    }
1453
}
1453
}