1 /*
2  * Copyright (c) 2012, 2018, 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 Charset _charset;
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 = System.lineSeparator().toCharArray();
79 
XMLStreamWriterImpl(OutputStream os)80     public XMLStreamWriterImpl(OutputStream os) throws XMLStreamException {
81         this(os, XMLStreamWriter.DEFAULT_CHARSET);
82     }
83 
XMLStreamWriterImpl(OutputStream os, Charset cs)84     public XMLStreamWriterImpl(OutputStream os, Charset cs)
85         throws XMLStreamException
86     {
87         if (cs == null) {
88             _charset = XMLStreamWriter.DEFAULT_CHARSET;
89         } else {
90             try {
91                 _charset = checkCharset(cs);
92             } catch (UnsupportedEncodingException e) {
93                 throw new XMLStreamException(e);
94             }
95         }
96 
97         _writer = new XMLWriter(os, null, _charset);
98     }
99 
100     /**
101      * Write the XML Declaration. Defaults the XML version to 1.0, and the
102      * encoding to utf-8.
103      *
104      * @throws XMLStreamException
105      */
writeStartDocument()106     public void writeStartDocument() throws XMLStreamException {
107         writeStartDocument(_charset.name(), XMLStreamWriter.DEFAULT_XML_VERSION);
108     }
109 
110     /**
111      * Write the XML Declaration. Defaults the encoding to utf-8
112      *
113      * @param version version of the xml document
114      * @throws XMLStreamException
115      */
writeStartDocument(String version)116     public void writeStartDocument(String version) throws XMLStreamException {
117         writeStartDocument(_charset.name(), version, null);
118     }
119 
120     /**
121      * Write the XML Declaration. Note that the encoding parameter does not set
122      * the actual encoding of the underlying output. That must be set when the
123      * instance of the XMLStreamWriter is created
124      *
125      * @param encoding encoding of the xml declaration
126      * @param version version of the xml document
127      * @throws XMLStreamException If given encoding does not match encoding of the
128      * underlying stream
129      */
writeStartDocument(String encoding, String version)130     public void writeStartDocument(String encoding, String version) throws XMLStreamException {
131         writeStartDocument(encoding, version, null);
132     }
133 
134     /**
135      * Write the XML Declaration. Note that the encoding parameter does not set
136      * the actual encoding of the underlying output. That must be set when the
137      * instance of the XMLStreamWriter is created
138      *
139      * @param encoding encoding of the xml declaration
140      * @param version version of the xml document
141      * @param standalone indicate if the xml document is standalone
142      * @throws XMLStreamException If given encoding does not match encoding of the
143      * underlying stream
144      */
writeStartDocument(String encoding, String version, String standalone)145     public void writeStartDocument(String encoding, String version, String standalone)
146         throws XMLStreamException
147     {
148         if (_state > 0) {
149             throw new XMLStreamException("XML declaration must be as the first line in the XML document.");
150         }
151         _state = STATE_XML_DECL;
152         String enc = encoding;
153         if (enc == null) {
154             enc = _charset.name();
155         } else {
156             //check if the encoding is supported
157             try {
158                 getCharset(encoding);
159             } catch (UnsupportedEncodingException e) {
160                 throw new XMLStreamException(e);
161             }
162         }
163 
164         if (version == null) {
165             version = XMLStreamWriter.DEFAULT_XML_VERSION;
166         }
167 
168         _writer.write("<?xml version=\"");
169         _writer.write(version);
170         _writer.write(DOUBLEQUOT);
171 
172         if (enc != null) {
173             _writer.write(" encoding=\"");
174             _writer.write(enc);
175             _writer.write(DOUBLEQUOT);
176         }
177 
178         if (standalone != null) {
179             _writer.write(" standalone=\"");
180             _writer.write(standalone);
181             _writer.write(DOUBLEQUOT);
182         }
183         _writer.write("?>");
184         writeLineSeparator();
185     }
186 
187     /**
188      * Write a DTD section.  This string represents the entire doctypedecl production
189      * from the XML 1.0 specification.
190      *
191      * @param dtd the DTD to be written
192      * @throws XMLStreamException
193      */
writeDTD(String dtd)194     public void writeDTD(String dtd) throws XMLStreamException {
195         if (_currentEle != null && _currentEle.getState() == ELEMENT_STARTTAG_OPEN) {
196             closeStartTag();
197         }
198         _writer.write(dtd);
199         writeLineSeparator();
200     }
201 
202     /**
203      * Writes a start tag to the output.
204      * @param localName local name of the tag, may not be null
205      * @throws XMLStreamException
206      */
writeStartElement(String localName)207     public void writeStartElement(String localName) throws XMLStreamException {
208         if (localName == null || localName.isEmpty()) {
209             throw new XMLStreamException("Local Name cannot be null or empty");
210         }
211 
212         _state = STATE_ELEMENT;
213         if (_currentEle != null && _currentEle.getState() == ELEMENT_STARTTAG_OPEN) {
214             closeStartTag();
215         }
216 
217         _currentEle = new Element(_currentEle, localName, false);
218         openStartTag();
219 
220         _writer.write(localName);
221     }
222 
223     /**
224      * Writes an empty element tag to the output
225      * @param localName local name of the tag, may not be null
226      * @throws XMLStreamException
227      */
writeEmptyElement(String localName)228     public void writeEmptyElement(String localName) throws XMLStreamException {
229         if (_currentEle != null && _currentEle.getState() == ELEMENT_STARTTAG_OPEN) {
230             closeStartTag();
231         }
232 
233         _currentEle = new Element(_currentEle, localName, true);
234 
235         openStartTag();
236         _writer.write(localName);
237     }
238 
239     /**
240      * Writes an attribute to the output stream without a prefix.
241      * @param localName the local name of the attribute
242      * @param value the value of the attribute
243      * @throws IllegalStateException if the current state does not allow Attribute writing
244      * @throws XMLStreamException
245      */
writeAttribute(String localName, String value)246     public void writeAttribute(String localName, String value) throws XMLStreamException {
247         if (_currentEle.getState() != ELEMENT_STARTTAG_OPEN) {
248             throw new XMLStreamException(
249                     "Attribute not associated with any element");
250         }
251 
252         _writer.write(SPACE);
253         _writer.write(localName);
254         _writer.write("=\"");
255         writeXMLContent(
256                 value,
257                 true, // true = escapeChars
258                 true);  // true = escapeDoubleQuotes
259         _writer.write(DOUBLEQUOT);
260     }
261 
writeEndDocument()262     public void writeEndDocument() throws XMLStreamException {
263         if (_currentEle != null && _currentEle.getState() == ELEMENT_STARTTAG_OPEN) {
264             closeStartTag();
265         }
266 
267         /**
268          * close unclosed elements if any
269          */
270         while (_currentEle != null) {
271 
272             if (!_currentEle.isEmpty()) {
273                 _writer.write(OPEN_END_TAG);
274                 _writer.write(_currentEle.getLocalName());
275                 _writer.write(CLOSE_END_TAG);
276             }
277 
278             _currentEle = _currentEle.getParent();
279         }
280     }
281 
writeEndElement()282     public void writeEndElement() throws XMLStreamException {
283         if (_currentEle != null && _currentEle.getState() == ELEMENT_STARTTAG_OPEN) {
284             closeStartTag();
285         }
286 
287         if (_currentEle == null) {
288             throw new XMLStreamException("No element was found to write");
289         }
290 
291         if (_currentEle.isEmpty()) {
292             return;
293         }
294 
295         _writer.write(OPEN_END_TAG);
296         _writer.write(_currentEle.getLocalName());
297         _writer.write(CLOSE_END_TAG);
298         writeLineSeparator();
299 
300         _currentEle = _currentEle.getParent();
301     }
302 
writeCData(String cdata)303     public void writeCData(String cdata) throws XMLStreamException {
304         if (cdata == null) {
305             throw new XMLStreamException("cdata cannot be null");
306         }
307 
308         if (_currentEle != null && _currentEle.getState() == ELEMENT_STARTTAG_OPEN) {
309             closeStartTag();
310         }
311 
312         _writer.write(START_CDATA);
313         _writer.write(cdata);
314         _writer.write(END_CDATA);
315     }
316 
writeCharacters(String data)317     public void writeCharacters(String data) throws XMLStreamException {
318         if (_currentEle != null && _currentEle.getState() == ELEMENT_STARTTAG_OPEN) {
319             closeStartTag();
320         }
321 
322         writeXMLContent(data);
323     }
324 
writeCharacters(char[] data, int start, int len)325     public void writeCharacters(char[] data, int start, int len)
326             throws XMLStreamException {
327         if (_currentEle != null && _currentEle.getState() == ELEMENT_STARTTAG_OPEN) {
328             closeStartTag();
329         }
330 
331         writeXMLContent(data, start, len, _escapeCharacters);
332     }
333 
334     /**
335      * Close this XMLStreamWriter by closing underlying writer.
336      */
close()337     public void close() throws XMLStreamException {
338         if (_writer != null) {
339             _writer.close();
340         }
341         _writer = null;
342         _currentEle = null;
343         _state = 0;
344     }
345 
346     /**
347      * Flush this XMLStreamWriter by flushing underlying writer.
348      */
flush()349     public void flush() throws XMLStreamException {
350         if (_writer != null) {
351             _writer.flush();
352         }
353     }
354 
355     /**
356      * Set the flag to indicate if the writer should add line separator
357      * @param doIndent
358      */
setDoIndent(boolean doIndent)359     public void setDoIndent(boolean doIndent) {
360         _doIndent = doIndent;
361     }
362 
363     /**
364      * Writes XML content to underlying writer. Escapes characters unless
365      * escaping character feature is turned off.
366      */
writeXMLContent(char[] content, int start, int length, boolean escapeChars)367     private void writeXMLContent(char[] content, int start, int length, boolean escapeChars)
368         throws XMLStreamException
369     {
370         if (!escapeChars) {
371             _writer.write(content, start, length);
372             return;
373         }
374 
375         // Index of the next char to be written
376         int startWritePos = start;
377 
378         final int end = start + length;
379 
380         for (int index = start; index < end; index++) {
381             char ch = content[index];
382 
383             if (!_writer.canEncode(ch)) {
384                 _writer.write(content, startWritePos, index - startWritePos);
385 
386                 // Escape this char as underlying encoder cannot handle it
387                 _writer.write(ENCODING_PREFIX);
388                 _writer.write(Integer.toHexString(ch));
389                 _writer.write(SEMICOLON);
390                 startWritePos = index + 1;
391                 continue;
392             }
393 
394             switch (ch) {
395                 case OPEN_START_TAG:
396                     _writer.write(content, startWritePos, index - startWritePos);
397                     _writer.write("&lt;");
398                     startWritePos = index + 1;
399 
400                     break;
401 
402                 case AMPERSAND:
403                     _writer.write(content, startWritePos, index - startWritePos);
404                     _writer.write("&amp;");
405                     startWritePos = index + 1;
406 
407                     break;
408 
409                 case CLOSE_START_TAG:
410                     _writer.write(content, startWritePos, index - startWritePos);
411                     _writer.write("&gt;");
412                     startWritePos = index + 1;
413 
414                     break;
415             }
416         }
417 
418         // Write any pending data
419         _writer.write(content, startWritePos, end - startWritePos);
420     }
421 
writeXMLContent(String content)422     private void writeXMLContent(String content) throws XMLStreamException {
423         if (content != null && !content.isEmpty()) {
424             writeXMLContent(content,
425                     _escapeCharacters, // boolean = escapeChars
426                     false);             // false = escapeDoubleQuotes
427         }
428     }
429 
430     /**
431      * Writes XML content to underlying writer. Escapes characters unless
432      * escaping character feature is turned off.
433      */
writeXMLContent( String content, boolean escapeChars, boolean escapeDoubleQuotes)434     private void writeXMLContent(
435             String content,
436             boolean escapeChars,
437             boolean escapeDoubleQuotes)
438         throws XMLStreamException
439     {
440 
441         if (!escapeChars) {
442             _writer.write(content);
443 
444             return;
445         }
446 
447         // Index of the next char to be written
448         int startWritePos = 0;
449 
450         final int end = content.length();
451 
452         for (int index = 0; index < end; index++) {
453             char ch = content.charAt(index);
454 
455             if (!_writer.canEncode(ch)) {
456                 _writer.write(content, startWritePos, index - startWritePos);
457 
458                 // Escape this char as underlying encoder cannot handle it
459                 _writer.write(ENCODING_PREFIX);
460                 _writer.write(Integer.toHexString(ch));
461                 _writer.write(SEMICOLON);
462                 startWritePos = index + 1;
463                 continue;
464             }
465 
466             switch (ch) {
467                 case OPEN_START_TAG:
468                     _writer.write(content, startWritePos, index - startWritePos);
469                     _writer.write("&lt;");
470                     startWritePos = index + 1;
471 
472                     break;
473 
474                 case AMPERSAND:
475                     _writer.write(content, startWritePos, index - startWritePos);
476                     _writer.write("&amp;");
477                     startWritePos = index + 1;
478 
479                     break;
480 
481                 case CLOSE_START_TAG:
482                     _writer.write(content, startWritePos, index - startWritePos);
483                     _writer.write("&gt;");
484                     startWritePos = index + 1;
485 
486                     break;
487 
488                 case DOUBLEQUOT:
489                     _writer.write(content, startWritePos, index - startWritePos);
490                     if (escapeDoubleQuotes) {
491                         _writer.write("&quot;");
492                     } else {
493                         _writer.write(DOUBLEQUOT);
494                     }
495                     startWritePos = index + 1;
496 
497                     break;
498             }
499         }
500 
501         // Write any pending data
502         _writer.write(content, startWritePos, end - startWritePos);
503     }
504 
505     /**
506      * marks open of start tag and writes the same into the writer.
507      */
openStartTag()508     private void openStartTag() throws XMLStreamException {
509         _currentEle.setState(ELEMENT_STARTTAG_OPEN);
510         _writer.write(OPEN_START_TAG);
511     }
512 
513     /**
514      * marks close of start tag and writes the same into the writer.
515      */
closeStartTag()516     private void closeStartTag() throws XMLStreamException {
517         if (_currentEle.isEmpty()) {
518             _writer.write(CLOSE_EMPTY_ELEMENT);
519         } else {
520             _writer.write(CLOSE_START_TAG);
521 
522         }
523 
524         if (_currentEle.getParent() == null) {
525             writeLineSeparator();
526         }
527 
528         _currentEle.setState(ELEMENT_STARTTAG_CLOSE);
529 
530     }
531 
532     /**
533      * Write a line separator
534      * @throws XMLStreamException
535      */
writeLineSeparator()536     private void writeLineSeparator() throws XMLStreamException {
537         if (_doIndent) {
538             _writer.write(_lineSep, 0, _lineSep.length);
539         }
540     }
541 
542     /**
543      * Returns a charset object for the specified encoding
544      * @param encoding
545      * @return a charset object
546      * @throws UnsupportedEncodingException if the encoding is not supported
547      */
getCharset(String encoding)548     private Charset getCharset(String encoding) throws UnsupportedEncodingException {
549         if (encoding.equalsIgnoreCase("UTF-32")) {
550             throw new UnsupportedEncodingException("The basic XMLWriter does "
551                     + "not support " + encoding);
552         }
553 
554         Charset cs;
555         try {
556             cs = Charset.forName(encoding);
557         } catch (IllegalCharsetNameException | UnsupportedCharsetException ex) {
558             throw new UnsupportedEncodingException(encoding);
559         }
560         return cs;
561     }
562 
563     /**
564      * Checks for charset support.
565      * @param charset the specified charset
566      * @return the charset
567      * @throws UnsupportedEncodingException if the charset is not supported
568      */
checkCharset(Charset charset)569     private Charset checkCharset(Charset charset) throws UnsupportedEncodingException {
570         if (charset.name().equalsIgnoreCase("UTF-32")) {
571             throw new UnsupportedEncodingException("The basic XMLWriter does "
572                     + "not support " + charset.name());
573         }
574         return charset;
575     }
576 
577     /*
578      * Start of Internal classes.
579      *
580      */
581     protected class Element {
582 
583         /**
584          * the parent element
585          */
586         protected Element _parent;
587         /**
588          * The size of the stack.
589          */
590         protected short _Depth;
591         /**
592          * indicate if an element is an empty one
593          */
594         boolean _isEmptyElement = false;
595         String _localpart;
596         int _state;
597 
598         /**
599          * Default constructor.
600          */
Element()601         public Element() {
602         }
603 
604         /**
605          * @param parent the parent of the element
606          * @param localpart name of the element
607          * @param isEmpty indicate if the element is an empty one
608          */
Element(Element parent, String localpart, boolean isEmpty)609         public Element(Element parent, String localpart, boolean isEmpty) {
610             _parent = parent;
611             _localpart = localpart;
612             _isEmptyElement = isEmpty;
613         }
614 
getParent()615         public Element getParent() {
616             return _parent;
617         }
618 
getLocalName()619         public String getLocalName() {
620             return _localpart;
621         }
622 
623         /**
624          * get the state of the element
625          */
getState()626         public int getState() {
627             return _state;
628         }
629 
630         /**
631          * Set the state of the element
632          *
633          * @param state the state of the element
634          */
setState(int state)635         public void setState(int state) {
636             _state = state;
637         }
638 
isEmpty()639         public boolean isEmpty() {
640             return _isEmptyElement;
641         }
642     }
643 }
644