1/*
2 * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package jdk.internal.util.xml.impl;
27
28import java.io.OutputStream;
29import java.io.UnsupportedEncodingException;
30import java.nio.charset.Charset;
31import java.nio.charset.IllegalCharsetNameException;
32import java.nio.charset.UnsupportedCharsetException;
33import jdk.internal.util.xml.XMLStreamException;
34import jdk.internal.util.xml.XMLStreamWriter;
35
36/**
37 * Implementation of a reduced version of XMLStreamWriter
38 *
39 * @author Joe Wang
40 */
41public class XMLStreamWriterImpl implements XMLStreamWriter {
42    //Document state
43
44    static final int STATE_XML_DECL = 1;
45    static final int STATE_PROLOG = 2;
46    static final int STATE_DTD_DECL = 3;
47    static final int STATE_ELEMENT = 4;
48    //Element state
49    static final int ELEMENT_STARTTAG_OPEN = 10;
50    static final int ELEMENT_STARTTAG_CLOSE = 11;
51    static final int ELEMENT_ENDTAG_OPEN = 12;
52    static final int ELEMENT_ENDTAG_CLOSE = 13;
53    public static final char CLOSE_START_TAG = '>';
54    public static final char OPEN_START_TAG = '<';
55    public static final String OPEN_END_TAG = "</";
56    public static final char CLOSE_END_TAG = '>';
57    public static final String START_CDATA = "<![CDATA[";
58    public static final String END_CDATA = "]]>";
59    public static final String CLOSE_EMPTY_ELEMENT = "/>";
60    public static final String ENCODING_PREFIX = "&#x";
61    public static final char SPACE = ' ';
62    public static final char AMPERSAND = '&';
63    public static final char DOUBLEQUOT = '"';
64    public static final char SEMICOLON = ';';
65    //current state
66    private int _state = 0;
67    private Element _currentEle;
68    private XMLWriter _writer;
69    private String _encoding;
70    /**
71     * This flag can be used to turn escaping off for content. It does
72     * not apply to attribute content.
73     */
74    boolean _escapeCharacters = true;
75    //pretty print by default
76    private boolean _doIndent = true;
77    //The system line separator for writing out line breaks.
78    private char[] _lineSep =
79            System.getProperty("line.separator").toCharArray();
80
81    public XMLStreamWriterImpl(OutputStream os) throws XMLStreamException {
82        this(os, XMLStreamWriter.DEFAULT_ENCODING);
83    }
84
85    public XMLStreamWriterImpl(OutputStream os, String encoding)
86        throws XMLStreamException
87    {
88        Charset cs = null;
89        if (encoding == null) {
90            _encoding = XMLStreamWriter.DEFAULT_ENCODING;
91        } else {
92            try {
93                cs = getCharset(encoding);
94            } catch (UnsupportedEncodingException e) {
95                throw new XMLStreamException(e);
96            }
97
98            this._encoding = encoding;
99        }
100
101        _writer = new XMLWriter(os, encoding, cs);
102    }
103
104    /**
105     * Write the XML Declaration. Defaults the XML version to 1.0, and the
106     * encoding to utf-8.
107     *
108     * @throws XMLStreamException
109     */
110    public void writeStartDocument() throws XMLStreamException {
111        writeStartDocument(_encoding, XMLStreamWriter.DEFAULT_XML_VERSION);
112    }
113
114    /**
115     * Write the XML Declaration. Defaults the encoding to utf-8
116     *
117     * @param version version of the xml document
118     * @throws XMLStreamException
119     */
120    public void writeStartDocument(String version) throws XMLStreamException {
121        writeStartDocument(_encoding, version, null);
122    }
123
124    /**
125     * Write the XML Declaration. Note that the encoding parameter does not set
126     * the actual encoding of the underlying output. That must be set when the
127     * instance of the XMLStreamWriter is created
128     *
129     * @param encoding encoding of the xml declaration
130     * @param version version of the xml document
131     * @throws XMLStreamException If given encoding does not match encoding of the
132     * underlying stream
133     */
134    public void writeStartDocument(String encoding, String version) throws XMLStreamException {
135        writeStartDocument(encoding, version, null);
136    }
137
138    /**
139     * Write the XML Declaration. Note that the encoding parameter does not set
140     * the actual encoding of the underlying output. That must be set when the
141     * instance of the XMLStreamWriter is created
142     *
143     * @param encoding encoding of the xml declaration
144     * @param version version of the xml document
145     * @param standalone indicate if the xml document is standalone
146     * @throws XMLStreamException If given encoding does not match encoding of the
147     * underlying stream
148     */
149    public void writeStartDocument(String encoding, String version, String standalone)
150        throws XMLStreamException
151    {
152        if (_state > 0) {
153            throw new XMLStreamException("XML declaration must be as the first line in the XML document.");
154        }
155        _state = STATE_XML_DECL;
156        String enc = encoding;
157        if (enc == null) {
158            enc = _encoding;
159        } else {
160            //check if the encoding is supported
161            try {
162                getCharset(encoding);
163            } catch (UnsupportedEncodingException e) {
164                throw new XMLStreamException(e);
165            }
166        }
167
168        if (version == null) {
169            version = XMLStreamWriter.DEFAULT_XML_VERSION;
170        }
171
172        _writer.write("<?xml version=\"");
173        _writer.write(version);
174        _writer.write(DOUBLEQUOT);
175
176        if (enc != null) {
177            _writer.write(" encoding=\"");
178            _writer.write(enc);
179            _writer.write(DOUBLEQUOT);
180        }
181
182        if (standalone != null) {
183            _writer.write(" standalone=\"");
184            _writer.write(standalone);
185            _writer.write(DOUBLEQUOT);
186        }
187        _writer.write("?>");
188        writeLineSeparator();
189    }
190
191    /**
192     * Write a DTD section.  This string represents the entire doctypedecl production
193     * from the XML 1.0 specification.
194     *
195     * @param dtd the DTD to be written
196     * @throws XMLStreamException
197     */
198    public void writeDTD(String dtd) throws XMLStreamException {
199        if (_currentEle != null && _currentEle.getState() == ELEMENT_STARTTAG_OPEN) {
200            closeStartTag();
201        }
202        _writer.write(dtd);
203        writeLineSeparator();
204    }
205
206    /**
207     * Writes a start tag to the output.
208     * @param localName local name of the tag, may not be null
209     * @throws XMLStreamException
210     */
211    public void writeStartElement(String localName) throws XMLStreamException {
212        if (localName == null || localName.length() == 0) {
213            throw new XMLStreamException("Local Name cannot be null or empty");
214        }
215
216        _state = STATE_ELEMENT;
217        if (_currentEle != null && _currentEle.getState() == ELEMENT_STARTTAG_OPEN) {
218            closeStartTag();
219        }
220
221        _currentEle = new Element(_currentEle, localName, false);
222        openStartTag();
223
224        _writer.write(localName);
225    }
226
227    /**
228     * Writes an empty element tag to the output
229     * @param localName local name of the tag, may not be null
230     * @throws XMLStreamException
231     */
232    public void writeEmptyElement(String localName) throws XMLStreamException {
233        if (_currentEle != null && _currentEle.getState() == ELEMENT_STARTTAG_OPEN) {
234            closeStartTag();
235        }
236
237        _currentEle = new Element(_currentEle, localName, true);
238
239        openStartTag();
240        _writer.write(localName);
241    }
242
243    /**
244     * Writes an attribute to the output stream without a prefix.
245     * @param localName the local name of the attribute
246     * @param value the value of the attribute
247     * @throws IllegalStateException if the current state does not allow Attribute writing
248     * @throws XMLStreamException
249     */
250    public void writeAttribute(String localName, String value) throws XMLStreamException {
251        if (_currentEle.getState() != ELEMENT_STARTTAG_OPEN) {
252            throw new XMLStreamException(
253                    "Attribute not associated with any element");
254        }
255
256        _writer.write(SPACE);
257        _writer.write(localName);
258        _writer.write("=\"");
259        writeXMLContent(
260                value,
261                true, // true = escapeChars
262                true);  // true = escapeDoubleQuotes
263        _writer.write(DOUBLEQUOT);
264    }
265
266    public void writeEndDocument() throws XMLStreamException {
267        if (_currentEle != null && _currentEle.getState() == ELEMENT_STARTTAG_OPEN) {
268            closeStartTag();
269        }
270
271        /**
272         * close unclosed elements if any
273         */
274        while (_currentEle != null) {
275
276            if (!_currentEle.isEmpty()) {
277                _writer.write(OPEN_END_TAG);
278                _writer.write(_currentEle.getLocalName());
279                _writer.write(CLOSE_END_TAG);
280            }
281
282            _currentEle = _currentEle.getParent();
283        }
284    }
285
286    public void writeEndElement() throws XMLStreamException {
287        if (_currentEle != null && _currentEle.getState() == ELEMENT_STARTTAG_OPEN) {
288            closeStartTag();
289        }
290
291        if (_currentEle == null) {
292            throw new XMLStreamException("No element was found to write");
293        }
294
295        if (_currentEle.isEmpty()) {
296            return;
297        }
298
299        _writer.write(OPEN_END_TAG);
300        _writer.write(_currentEle.getLocalName());
301        _writer.write(CLOSE_END_TAG);
302        writeLineSeparator();
303
304        _currentEle = _currentEle.getParent();
305    }
306
307    public void writeCData(String cdata) throws XMLStreamException {
308        if (cdata == null) {
309            throw new XMLStreamException("cdata cannot be null");
310        }
311
312        if (_currentEle != null && _currentEle.getState() == ELEMENT_STARTTAG_OPEN) {
313            closeStartTag();
314        }
315
316        _writer.write(START_CDATA);
317        _writer.write(cdata);
318        _writer.write(END_CDATA);
319    }
320
321    public void writeCharacters(String data) throws XMLStreamException {
322        if (_currentEle != null && _currentEle.getState() == ELEMENT_STARTTAG_OPEN) {
323            closeStartTag();
324        }
325
326        writeXMLContent(data);
327    }
328
329    public void writeCharacters(char[] data, int start, int len)
330            throws XMLStreamException {
331        if (_currentEle != null && _currentEle.getState() == ELEMENT_STARTTAG_OPEN) {
332            closeStartTag();
333        }
334
335        writeXMLContent(data, start, len, _escapeCharacters);
336    }
337
338    /**
339     * Close this XMLStreamWriter by closing underlying writer.
340     */
341    public void close() throws XMLStreamException {
342        if (_writer != null) {
343            _writer.close();
344        }
345        _writer = null;
346        _currentEle = null;
347        _state = 0;
348    }
349
350    /**
351     * Flush this XMLStreamWriter by flushing underlying writer.
352     */
353    public void flush() throws XMLStreamException {
354        if (_writer != null) {
355            _writer.flush();
356        }
357    }
358
359    /**
360     * Set the flag to indicate if the writer should add line separator
361     * @param doIndent
362     */
363    public void setDoIndent(boolean doIndent) {
364        _doIndent = doIndent;
365    }
366
367    /**
368     * Writes XML content to underlying writer. Escapes characters unless
369     * escaping character feature is turned off.
370     */
371    private void writeXMLContent(char[] content, int start, int length, boolean escapeChars)
372        throws XMLStreamException
373    {
374        if (!escapeChars) {
375            _writer.write(content, start, length);
376            return;
377        }
378
379        // Index of the next char to be written
380        int startWritePos = start;
381
382        final int end = start + length;
383
384        for (int index = start; index < end; index++) {
385            char ch = content[index];
386
387            if (!_writer.canEncode(ch)) {
388                _writer.write(content, startWritePos, index - startWritePos);
389
390                // Escape this char as underlying encoder cannot handle it
391                _writer.write(ENCODING_PREFIX);
392                _writer.write(Integer.toHexString(ch));
393                _writer.write(SEMICOLON);
394                startWritePos = index + 1;
395                continue;
396            }
397
398            switch (ch) {
399                case OPEN_START_TAG:
400                    _writer.write(content, startWritePos, index - startWritePos);
401                    _writer.write("&lt;");
402                    startWritePos = index + 1;
403
404                    break;
405
406                case AMPERSAND:
407                    _writer.write(content, startWritePos, index - startWritePos);
408                    _writer.write("&amp;");
409                    startWritePos = index + 1;
410
411                    break;
412
413                case CLOSE_START_TAG:
414                    _writer.write(content, startWritePos, index - startWritePos);
415                    _writer.write("&gt;");
416                    startWritePos = index + 1;
417
418                    break;
419            }
420        }
421
422        // Write any pending data
423        _writer.write(content, startWritePos, end - startWritePos);
424    }
425
426    private void writeXMLContent(String content) throws XMLStreamException {
427        if ((content != null) && (content.length() > 0)) {
428            writeXMLContent(content,
429                    _escapeCharacters, // boolean = escapeChars
430                    false);             // false = escapeDoubleQuotes
431        }
432    }
433
434    /**
435     * Writes XML content to underlying writer. Escapes characters unless
436     * escaping character feature is turned off.
437     */
438    private void writeXMLContent(
439            String content,
440            boolean escapeChars,
441            boolean escapeDoubleQuotes)
442        throws XMLStreamException
443    {
444
445        if (!escapeChars) {
446            _writer.write(content);
447
448            return;
449        }
450
451        // Index of the next char to be written
452        int startWritePos = 0;
453
454        final int end = content.length();
455
456        for (int index = 0; index < end; index++) {
457            char ch = content.charAt(index);
458
459            if (!_writer.canEncode(ch)) {
460                _writer.write(content, startWritePos, index - startWritePos);
461
462                // Escape this char as underlying encoder cannot handle it
463                _writer.write(ENCODING_PREFIX);
464                _writer.write(Integer.toHexString(ch));
465                _writer.write(SEMICOLON);
466                startWritePos = index + 1;
467                continue;
468            }
469
470            switch (ch) {
471                case OPEN_START_TAG:
472                    _writer.write(content, startWritePos, index - startWritePos);
473                    _writer.write("&lt;");
474                    startWritePos = index + 1;
475
476                    break;
477
478                case AMPERSAND:
479                    _writer.write(content, startWritePos, index - startWritePos);
480                    _writer.write("&amp;");
481                    startWritePos = index + 1;
482
483                    break;
484
485                case CLOSE_START_TAG:
486                    _writer.write(content, startWritePos, index - startWritePos);
487                    _writer.write("&gt;");
488                    startWritePos = index + 1;
489
490                    break;
491
492                case DOUBLEQUOT:
493                    _writer.write(content, startWritePos, index - startWritePos);
494                    if (escapeDoubleQuotes) {
495                        _writer.write("&quot;");
496                    } else {
497                        _writer.write(DOUBLEQUOT);
498                    }
499                    startWritePos = index + 1;
500
501                    break;
502            }
503        }
504
505        // Write any pending data
506        _writer.write(content, startWritePos, end - startWritePos);
507    }
508
509    /**
510     * marks open of start tag and writes the same into the writer.
511     */
512    private void openStartTag() throws XMLStreamException {
513        _currentEle.setState(ELEMENT_STARTTAG_OPEN);
514        _writer.write(OPEN_START_TAG);
515    }
516
517    /**
518     * marks close of start tag and writes the same into the writer.
519     */
520    private void closeStartTag() throws XMLStreamException {
521        if (_currentEle.isEmpty()) {
522            _writer.write(CLOSE_EMPTY_ELEMENT);
523        } else {
524            _writer.write(CLOSE_START_TAG);
525
526        }
527
528        if (_currentEle.getParent() == null) {
529            writeLineSeparator();
530        }
531
532        _currentEle.setState(ELEMENT_STARTTAG_CLOSE);
533
534    }
535
536    /**
537     * Write a line separator
538     * @throws XMLStreamException
539     */
540    private void writeLineSeparator() throws XMLStreamException {
541        if (_doIndent) {
542            _writer.write(_lineSep, 0, _lineSep.length);
543        }
544    }
545
546    /**
547     * Returns a charset object for the specified encoding
548     * @param encoding
549     * @return a charset object
550     * @throws UnsupportedEncodingException if the encoding is not supported
551     */
552    private Charset getCharset(String encoding) throws UnsupportedEncodingException {
553        if (encoding.equalsIgnoreCase("UTF-32")) {
554            throw new UnsupportedEncodingException("The basic XMLWriter does "
555                    + "not support " + encoding);
556        }
557
558        Charset cs;
559        try {
560            cs = Charset.forName(encoding);
561        } catch (IllegalCharsetNameException | UnsupportedCharsetException ex) {
562            throw new UnsupportedEncodingException(encoding);
563        }
564        return cs;
565    }
566
567    /*
568     * Start of Internal classes.
569     *
570     */
571    protected class Element {
572
573        /**
574         * the parent element
575         */
576        protected Element _parent;
577        /**
578         * The size of the stack.
579         */
580        protected short _Depth;
581        /**
582         * indicate if an element is an empty one
583         */
584        boolean _isEmptyElement = false;
585        String _localpart;
586        int _state;
587
588        /**
589         * Default constructor.
590         */
591        public Element() {
592        }
593
594        /**
595         * @param parent the parent of the element
596         * @param localpart name of the element
597         * @param isEmpty indicate if the element is an empty one
598         */
599        public Element(Element parent, String localpart, boolean isEmpty) {
600            _parent = parent;
601            _localpart = localpart;
602            _isEmptyElement = isEmpty;
603        }
604
605        public Element getParent() {
606            return _parent;
607        }
608
609        public String getLocalName() {
610            return _localpart;
611        }
612
613        /**
614         * get the state of the element
615         */
616        public int getState() {
617            return _state;
618        }
619
620        /**
621         * Set the state of the element
622         *
623         * @param state the state of the element
624         */
625        public void setState(int state) {
626            _state = state;
627        }
628
629        public boolean isEmpty() {
630            return _isEmptyElement;
631        }
632    }
633}
634