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 
26 package jdk.internal.util.xml.impl;
27 
28 import java.io.OutputStream;
29 import java.io.UnsupportedEncodingException;
30 import java.nio.charset.Charset;
31 import java.nio.charset.IllegalCharsetNameException;
32 import java.nio.charset.UnsupportedCharsetException;
33 import jdk.internal.util.xml.XMLStreamException;
34 import jdk.internal.util.xml.XMLStreamWriter;
35 
36 /**
37  * Implementation of a reduced version of XMLStreamWriter
38  *
39  * @author Joe Wang
40  */
41 public 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 
XMLStreamWriterImpl(OutputStream os)81     public XMLStreamWriterImpl(OutputStream os) throws XMLStreamException {
82         this(os, XMLStreamWriter.DEFAULT_ENCODING);
83     }
84 
XMLStreamWriterImpl(OutputStream os, String encoding)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      */
writeStartDocument()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      */
writeStartDocument(String version)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      */
writeStartDocument(String encoding, String version)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      */
writeStartDocument(String encoding, String version, String standalone)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      */
writeDTD(String dtd)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      */
writeStartElement(String localName)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      */
writeEmptyElement(String localName)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      */
writeAttribute(String localName, String value)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 
writeEndDocument()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 
writeEndElement()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 
writeCData(String cdata)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 
writeCharacters(String data)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 
writeCharacters(char[] data, int start, int len)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      */
close()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      */
flush()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      */
setDoIndent(boolean doIndent)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      */
writeXMLContent(char[] content, int start, int length, boolean escapeChars)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 
writeXMLContent(String content)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      */
writeXMLContent( String content, boolean escapeChars, boolean escapeDoubleQuotes)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      */
openStartTag()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      */
closeStartTag()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      */
writeLineSeparator()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      */
getCharset(String encoding)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          */
Element()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          */
Element(Element parent, String localpart, boolean isEmpty)599         public Element(Element parent, String localpart, boolean isEmpty) {
600             _parent = parent;
601             _localpart = localpart;
602             _isEmptyElement = isEmpty;
603         }
604 
getParent()605         public Element getParent() {
606             return _parent;
607         }
608 
getLocalName()609         public String getLocalName() {
610             return _localpart;
611         }
612 
613         /**
614          * get the state of the element
615          */
getState()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          */
setState(int state)625         public void setState(int state) {
626             _state = state;
627         }
628 
isEmpty()629         public boolean isEmpty() {
630             return _isEmptyElement;
631         }
632     }
633 }
634