OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 21 | Rev 73 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
17 ilm 1
/*
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3
 *
4
 * Copyright 2011 OpenConcerto, by ILM Informatique. All rights reserved.
5
 *
6
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
7
 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
8
 * copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
9
 * language governing permissions and limitations under the License.
10
 *
11
 * When distributing the software, include this License Header Notice in each file.
12
 */
13
 
14
 /*
15
 * Créé le 28 oct. 2004
16
 */
17
package org.openconcerto.openoffice;
18
 
19 ilm 19
import org.openconcerto.utils.CollectionUtils;
17 ilm 20
import org.openconcerto.xml.JDOMUtils;
21
import org.openconcerto.xml.Validator;
22
 
21 ilm 23
import java.math.BigDecimal;
17 ilm 24
import java.util.ArrayList;
25
import java.util.HashMap;
26
import java.util.Iterator;
27
import java.util.List;
28
import java.util.Map;
29
import java.util.Map.Entry;
19 ilm 30
import java.util.SortedMap;
31
import java.util.TreeMap;
17 ilm 32
import java.util.regex.Matcher;
33
import java.util.regex.Pattern;
34
 
35
import javax.xml.XMLConstants;
36
import javax.xml.validation.Schema;
37
import javax.xml.validation.SchemaFactory;
38
 
39
import org.jdom.Content;
40
import org.jdom.Document;
41
import org.jdom.Element;
42
import org.jdom.JDOMException;
43
import org.jdom.Namespace;
44
import org.jdom.Parent;
45
import org.jdom.Text;
46
import org.jdom.xpath.XPath;
47
import org.xml.sax.SAXException;
48
 
49
/**
50
 * Various bits of OpenDocument XML.
51
 *
52
 * @author Sylvain CUAZ
19 ilm 53
 * @see #get(XMLFormatVersion)
17 ilm 54
 */
19 ilm 55
public abstract class OOXML implements Comparable<OOXML> {
17 ilm 56
 
19 ilm 57
    /**
58
     * If this system property is set to <code>true</code> then {@link #get(XMLFormatVersion)} will
59
     * never return <code>null</code>, allowing to support unknown versions.
60
     */
61
    public static final String LAST_FOR_UNKNOWN_PROP = OOXML.class.getPackage().getName() + ".lastOOXMLForUnknownVersion";
62
    private static final XML_OO instanceOO = new XML_OO();
63
    private static final SortedMap<String, XML_OD> instancesODByDate = new TreeMap<String, XML_OD>();
64
    private static final Map<String, XML_OD> instancesODByVersion = new HashMap<String, XML_OD>();
65
    private static final List<OOXML> values;
66
    private static OOXML defaultInstance;
17 ilm 67
 
19 ilm 68
    static {
69
        register(new XML_OD_1_0());
70
        register(new XML_OD_1_1());
71
        register(new XML_OD_1_2());
72
        values = new ArrayList<OOXML>(instancesODByDate.size() + 1);
73
        values.add(instanceOO);
74
        values.addAll(instancesODByDate.values());
75
 
76
        setDefault(getLast());
77
    }
78
 
79
    private static void register(XML_OD xml) {
80
        assert xml.getVersion() == XMLVersion.OD;
81
        instancesODByDate.put(xml.getDateString(), xml);
82
        instancesODByVersion.put(xml.getFormatVersion().getOfficeVersion(), xml);
83
    }
84
 
17 ilm 85
    /**
86
     * Returns the instance that match the requested version.
87
     *
88
     * @param version the version.
19 ilm 89
     * @return the corresponding instance, <code>null</code> for unsupported versions.
90
     * @see #LAST_FOR_UNKNOWN_PROP
17 ilm 91
     */
19 ilm 92
    public static OOXML get(XMLFormatVersion version) {
93
        return get(version, Boolean.getBoolean(LAST_FOR_UNKNOWN_PROP));
17 ilm 94
    }
95
 
19 ilm 96
    public static OOXML get(XMLFormatVersion version, final boolean lastForUnknown) {
97
        if (version.getXMLVersion() == XMLVersion.OOo) {
98
            return instanceOO;
99
        } else {
100
            final XML_OD res = instancesODByVersion.get(version.getOfficeVersion());
101
            if (res == null && lastForUnknown)
102
                return getLast(version.getXMLVersion());
103
            else
104
                return res;
105
        }
17 ilm 106
    }
107
 
19 ilm 108
    public static OOXML get(Element root) {
109
        return XMLFormatVersion.get(root).getXML();
17 ilm 110
    }
111
 
112
    /**
19 ilm 113
     * Return all known instances in the order they were published.
17 ilm 114
     *
19 ilm 115
     * @return all known instances ordered.
116
     * @see #compareTo(OOXML)
17 ilm 117
     */
19 ilm 118
    static public final List<OOXML> values() {
119
        return values;
120
    }
17 ilm 121
 
19 ilm 122
    static public final OOXML getLast() {
123
        return CollectionUtils.getLast(values);
124
    }
17 ilm 125
 
19 ilm 126
    static public final OOXML getLast(XMLVersion version) {
127
        if (version == XMLVersion.OOo)
128
            return instanceOO;
129
        else
130
            return instancesODByDate.get(instancesODByDate.lastKey());
17 ilm 131
    }
132
 
19 ilm 133
    public static void setDefault(OOXML ns) {
134
        defaultInstance = ns;
135
    }
136
 
137
    public static OOXML getDefault() {
138
        return defaultInstance;
139
    }
140
 
141
    static private final String rt2oo(String content, String tagName, String styleName) {
142
        return content.replaceAll("\\[" + tagName + "\\]", "<text:span text:style-name=\"" + styleName + "\">").replaceAll("\\[/" + tagName + "\\]", "</text:span>");
143
    }
144
 
28 ilm 145
    // from OpenDocument-v1.2-schema.rng : a coordinate is a length
146
    static private final BigDecimal parseCoordinate(final Element elem, final String attrName, final Namespace ns, LengthUnit unit) {
147
        return parseLength(elem, attrName, ns, unit);
148
    }
149
 
21 ilm 150
    static private final BigDecimal parseLength(final Element elem, final String attrName, final Namespace ns, LengthUnit unit) {
151
        final String attr = elem.getAttributeValue(attrName, ns);
152
        if (attr == null)
153
            return null;
28 ilm 154
        return LengthUnit.parseLength(attr, unit);
21 ilm 155
    }
156
 
17 ilm 157
    // *** instances
158
 
19 ilm 159
    private final XMLFormatVersion version;
160
    private final String dateString;
17 ilm 161
 
19 ilm 162
    private OOXML(XMLFormatVersion version, final String dateString) {
17 ilm 163
        this.version = version;
19 ilm 164
        this.dateString = dateString;
17 ilm 165
    }
166
 
19 ilm 167
    /**
168
     * The date the specification was published.
169
     *
170
     * @return the date in "yyyyMMdd" format.
171
     */
172
    public final String getDateString() {
173
        return this.dateString;
174
    }
175
 
176
    /**
177
     * Compare the date the specification was published.
178
     *
179
     * @param o the object to be compared.
180
     * @see #getDateString()
181
     */
182
    @Override
183
    public int compareTo(OOXML o) {
184
        return this.dateString.compareTo(o.dateString);
185
    }
186
 
17 ilm 187
    public final XMLVersion getVersion() {
19 ilm 188
        return this.getFormatVersion().getXMLVersion();
17 ilm 189
    }
190
 
19 ilm 191
    public final XMLFormatVersion getFormatVersion() {
192
        return this.version;
17 ilm 193
    }
194
 
19 ilm 195
    public abstract boolean canValidate();
196
 
17 ilm 197
    /**
198
     * Verify that the passed document is a valid OpenOffice.org 1 or ODF document.
199
     *
200
     * @param doc the xml to test.
201
     * @return a validator on <code>doc</code>.
202
     */
19 ilm 203
    public abstract Validator getValidator(Document doc);
17 ilm 204
 
205
    /**
206
     * Return the names of font face declarations.
207
     *
208
     * @return at index 0 the name of the container element, at 1 the qualified name of its
209
     *         children.
210
     */
19 ilm 211
    public abstract String[] getFontDecls();
17 ilm 212
 
213
    public final Element getLineBreak() {
214
        return new Element("line-break", getVersion().getTEXT());
215
    }
216
 
19 ilm 217
    public abstract Element getTab();
17 ilm 218
 
19 ilm 219
    public abstract String getFrameQName();
17 ilm 220
 
19 ilm 221
    public abstract Element createFormattingProperties(final String family);
222
 
223
    protected final List encodeRT_L(String content, Map<String, String> styles) {
17 ilm 224
        String res = JDOMUtils.OUTPUTTER.escapeElementEntities(content);
19 ilm 225
        for (final Entry<String, String> e : styles.entrySet()) {
226
            res = rt2oo(res, e.getKey(), e.getValue());
17 ilm 227
        }
228
        try {
229
            return JDOMUtils.parseString(res, getVersion().getALL());
230
        } catch (JDOMException e) {
231
            // should not happpen as we did escapeElementEntities which gives valid xml and then
232
            // rt2oo which introduce only static xml
233
            throw new IllegalStateException("could not parse " + res, e);
234
        }
235
    }
236
 
237
    /**
238
     * Convert rich text (with [] tags) into XML.
239
     *
240
     * @param content the string to convert, eg "texte [b]gras[/b]".
241
     * @param styles the mapping from tagname (eg "b") to the name of the character style (eg
242
     *        "Gras").
243
     * @return the corresponding element.
244
     */
19 ilm 245
    public final Element encodeRT(String content, Map<String, String> styles) {
17 ilm 246
        return new Element("span", getVersion().getTEXT()).addContent(encodeRT_L(content, styles));
247
    }
248
 
249
    // create the necessary <text:s c="n"/>
250
    private Element createSpaces(String spacesS) {
251
        return new Element("s", getVersion().getTEXT()).setAttribute("c", spacesS.length() + "", getVersion().getTEXT());
252
    }
253
 
254
    /**
255
     * Encode a String to OO XML. Handles substition of whitespaces to their OO equivalent.
256
     *
257
     * @param s a plain ole String, eg "term\tdefinition".
258
     * @return an Element suitable to be inserted in an OO XML document, eg
259
     *
260
     *         <pre>
261
     *     &lt;text:span&gt;term&lt;text:tab-stop/&gt;definition&lt;/text:span&gt;
262
     * </pre>
263
     *
264
     *         .
265
     */
266
    public final Element encodeWS(final String s) {
267
        return new Element("span", getVersion().getTEXT()).setContent(encodeWSasList(s));
268
    }
269
 
20 ilm 270
    public final List<Content> encodeWSasList(final String s) {
17 ilm 271
        final List<Content> res = new ArrayList<Content>();
272
        final Matcher m = Pattern.compile("\n|\t| {2,}").matcher(s);
273
        int last = 0;
274
        while (m.find()) {
275
            res.add(new Text(s.substring(last, m.start())));
276
            switch (m.group().charAt(0)) {
277
            case '\n':
278
                res.add(getLineBreak());
279
                break;
280
            case '\t':
281
                res.add(getTab());
282
                break;
283
            case ' ':
284
                res.add(createSpaces(m.group()));
285
                break;
286
 
287
            default:
288
                throw new IllegalStateException("unknown item: " + m.group());
289
            }
290
            last = m.end();
291
        }
292
        res.add(new Text(s.substring(last)));
293
        return res;
294
    }
295
 
296
    @SuppressWarnings("unchecked")
297
    public final void encodeWS(final Text t) {
298
        final Parent parent = t.getParent();
299
        final int ind = parent.indexOf(t);
300
        t.detach();
301
        parent.getContent().addAll(ind, encodeWSasList(t.getText()));
302
    }
303
 
304
    @SuppressWarnings("unchecked")
305
    public final Element encodeWS(final Element elem) {
306
        final XPath path;
307
        try {
19 ilm 308
            path = OOUtils.getXPath(".//text()", getVersion());
17 ilm 309
        } catch (JDOMException e) {
310
            // static path, hence always valid
311
            throw new IllegalStateException("cannot create XPath", e);
312
        }
313
        try {
314
            final Iterator iter = new ArrayList(path.selectNodes(elem)).iterator();
315
            while (iter.hasNext()) {
316
                final Text t = (Text) iter.next();
317
                encodeWS(t);
318
            }
319
        } catch (JDOMException e) {
320
            throw new IllegalArgumentException("cannot find text nodes of " + elem, e);
321
        }
322
        return elem;
323
    }
324
 
21 ilm 325
    /**
326
     * Return the coordinates of the top-left and bottom-right of the passed shape.
327
     *
328
     * @param elem an XML element.
329
     * @param unit the unit of the returned numbers.
330
     * @return an array of 4 numbers, <code>null</code> if <code>elem</code> is not a shape, numbers
331
     *         themselves are never <code>null</code>.
332
     */
333
    public final BigDecimal[] getCoordinates(Element elem, LengthUnit unit) {
334
        return this.getCoordinates(elem, unit, true, true);
335
    }
336
 
337
    /**
338
     * Return the coordinates of the top-left and bottom-right of the passed shape.
339
     *
340
     * @param elem an XML element.
341
     * @param unit the unit of the returned numbers.
342
     * @param horizontal <code>true</code> if the x coordinates should be computed,
343
     *        <code>false</code> meaning items 0 and 2 of the result are <code>null</code>.
344
     * @param vertical <code>true</code> if the y coordinates should be computed, <code>false</code>
345
     *        meaning items 1 and 3 of the result are <code>null</code>.
346
     * @return an array of 4 numbers, <code>null</code> if <code>elem</code> is not a shape, numbers
347
     *         themselves are only <code>null</code> if requested with <code>horizontal</code> or
348
     *         <code>vertical</code>.
349
     */
350
    public final BigDecimal[] getCoordinates(Element elem, LengthUnit unit, final boolean horizontal, final boolean vertical) {
351
        return getCoordinates(elem, getVersion().getNS("svg"), unit, horizontal, vertical);
352
    }
353
 
354
    static private final BigDecimal[] getCoordinates(Element elem, final Namespace svgNS, LengthUnit unit, final boolean horizontal, final boolean vertical) {
355
        if (elem.getName().equals("g") && elem.getNamespacePrefix().equals("draw")) {
356
            // put below if to allow null to be returned by getLocalCoordinates() if elem isn't a
357
            // shape
358
            if (!horizontal && !vertical)
359
                return new BigDecimal[] { null, null, null, null };
360
 
361
            // an OpenDocument group (of shapes) doesn't have any coordinates nor any width and
362
            // height so iterate through its components to find its coordinates
363
            BigDecimal minX = null, minY = null;
364
            BigDecimal maxX = null, maxY = null;
365
            for (final Object c : elem.getChildren()) {
366
                final Element child = (Element) c;
367
                final BigDecimal[] childCoord = getCoordinates(child, svgNS, unit, horizontal, vertical);
368
                // e.g. <office:event-listeners>, <svg:desc>, <svg:title>
369
                if (childCoord != null) {
370
                    {
371
                        final BigDecimal x = childCoord[0];
372
                        final BigDecimal x2 = childCoord[2];
373
                        if (x != null) {
374
                            assert x2 != null;
375
                            if (minX == null || x.compareTo(minX) < 0)
376
                                minX = x;
377
                            if (maxX == null || x2.compareTo(maxX) > 0)
378
                                maxX = x2;
379
                        }
380
                    }
381
                    {
382
                        final BigDecimal y = childCoord[1];
383
                        final BigDecimal y2 = childCoord[3];
384
                        if (y != null) {
385
                            assert y2 != null;
386
                            if (minY == null || y.compareTo(minY) < 0)
387
                                minY = y;
388
                            if (maxY == null || y2.compareTo(maxY) > 0)
389
                                maxY = y2;
390
                        }
391
                    }
392
                }
393
            }
394
            // works because we check above if both horizontal and vertical are false
395
            if (minX == null && minY == null)
396
                throw new IllegalArgumentException("Empty group : " + JDOMUtils.output(elem));
397
            return new BigDecimal[] { minX, minY, maxX, maxY };
398
        } else {
399
            return getLocalCoordinates(elem, svgNS, unit, horizontal, vertical);
400
        }
401
    }
402
 
403
    // return null if elem isn't a shape (no x/y or no width/height)
404
    // BigDecimal null if and only if horizontal/vertical is false
405
    static private final BigDecimal[] getLocalCoordinates(Element elem, final Namespace svgNS, LengthUnit unit, final boolean horizontal, final boolean vertical) {
28 ilm 406
        final BigDecimal x = parseCoordinate(elem, "x", svgNS, unit);
407
        final BigDecimal x1 = parseCoordinate(elem, "x1", svgNS, unit);
21 ilm 408
        if (x == null && x1 == null)
409
            return null;
410
 
28 ilm 411
        final BigDecimal y = parseCoordinate(elem, "y", svgNS, unit);
412
        final BigDecimal y1 = parseCoordinate(elem, "y1", svgNS, unit);
21 ilm 413
        if (y == null && y1 == null)
414
            throw new IllegalArgumentException("Have x but missing y in " + JDOMUtils.output(elem));
415
 
416
        final BigDecimal startX;
417
        final BigDecimal endX;
418
        if (horizontal) {
419
            if (x == null) {
420
                startX = x1;
28 ilm 421
                endX = parseCoordinate(elem, "x2", svgNS, unit);
21 ilm 422
            } else {
423
                startX = x;
424
                final BigDecimal width = parseLength(elem, "width", svgNS, unit);
425
                endX = width == null ? null : startX.add(width);
426
            }
427
            // return null if there's no second coordinate (it's a point)
428
            if (endX == null)
429
                return null;
430
        } else {
431
            startX = null;
432
            endX = null;
433
        }
434
 
435
        final BigDecimal startY;
436
        final BigDecimal endY;
437
        if (vertical) {
438
            if (y == null) {
439
                startY = y1;
28 ilm 440
                endY = parseCoordinate(elem, "y2", svgNS, unit);
21 ilm 441
            } else {
442
                startY = y;
443
                final BigDecimal height = parseLength(elem, "height", svgNS, unit);
444
                endY = height == null ? null : startY.add(height);
445
            }
446
            // return null if there's no second coordinate (it's a point)
447
            if (endY == null)
448
                return null;
449
        } else {
450
            startY = null;
451
            endY = null;
452
        }
453
 
454
        return new BigDecimal[] { startX, startY, endX, endY };
455
    }
456
 
19 ilm 457
    private static final class XML_OO extends OOXML {
458
        public XML_OO() {
459
            super(XMLFormatVersion.getOOo(), "20020501");
460
        }
461
 
462
        @Override
463
        public boolean canValidate() {
464
            return true;
465
        }
466
 
467
        @Override
468
        public Validator getValidator(Document doc) {
469
            // DTDs are stubborn, xmlns have to be exactly where they want
470
            // in this case the root element
471
            for (final Namespace n : getVersion().getALL())
472
                doc.getRootElement().addNamespaceDeclaration(n);
473
            return new Validator.DTDValidator(doc, OOUtils.getBuilderLoadDTD());
474
        }
475
 
476
        @Override
477
        public String[] getFontDecls() {
478
            return new String[] { "font-decls", "style:font-decl" };
479
        }
480
 
481
        @Override
482
        public final Element getTab() {
483
            return new Element("tab-stop", getVersion().getTEXT());
484
        }
485
 
486
        @Override
487
        public String getFrameQName() {
488
            return "draw:text-box";
489
        }
490
 
491
        @Override
492
        public Element createFormattingProperties(String family) {
493
            return new Element("properties", this.getVersion().getSTYLE());
494
        }
495
    }
496
 
497
    private static class XML_OD extends OOXML {
498
        private final String schemaFile;
499
        private Schema schema = null;
500
 
501
        public XML_OD(final String dateString, final String versionString, final String schemaFile) {
502
            super(XMLFormatVersion.get(XMLVersion.OD, versionString), dateString);
503
            this.schemaFile = schemaFile;
504
        }
505
 
506
        @Override
507
        public boolean canValidate() {
508
            return this.schemaFile != null;
509
        }
510
 
511
        private Schema getSchema() throws SAXException {
512
            if (this.schema == null && this.schemaFile != null) {
513
                this.schema = SchemaFactory.newInstance(XMLConstants.RELAXNG_NS_URI).newSchema(getClass().getResource("oofficeDTDs/" + this.schemaFile));
514
            }
515
            return this.schema;
516
        }
517
 
518
        @Override
519
        public Validator getValidator(Document doc) {
520
            final Schema schema;
521
            try {
522
                schema = this.getSchema();
523
            } catch (SAXException e) {
524
                throw new IllegalStateException("relaxNG schemas pb", e);
525
            }
526
            return schema == null ? null : new Validator.JAXPValidator(doc, schema);
527
        }
528
 
529
        @Override
530
        public final String[] getFontDecls() {
531
            return new String[] { "font-face-decls", "style:font-face" };
532
        }
533
 
534
        @Override
535
        public final Element getTab() {
536
            return new Element("tab", getVersion().getTEXT());
537
        }
538
 
539
        @Override
540
        public String getFrameQName() {
541
            return "draw:frame";
542
        }
543
 
544
        @Override
545
        public Element createFormattingProperties(String family) {
546
            return new Element(family + "-properties", this.getVersion().getSTYLE());
547
        }
548
    }
549
 
550
    private static final class XML_OD_1_0 extends XML_OD {
551
        public XML_OD_1_0() {
552
            super("20061130", "1.0", null);
553
        }
554
    }
555
 
556
    private static final class XML_OD_1_1 extends XML_OD {
557
        public XML_OD_1_1() {
558
            super("20070201", "1.1", "OpenDocument-strict-schema-v1.1.rng");
559
        }
560
    }
561
 
562
    private static final class XML_OD_1_2 extends XML_OD {
563
        public XML_OD_1_2() {
28 ilm 564
            super("20110317", "1.2", "OpenDocument-v1.2-schema.rng");
19 ilm 565
        }
566
    }
17 ilm 567
}