OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

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