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 
  80     public XMLStreamWriterImpl(OutputStream os) throws XMLStreamException {
  81         this(os, XMLStreamWriter.DEFAULT_CHARSET);
  82     }
  83 
  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      */
 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      */
 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      */
 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      */
 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      */
 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      */
 207     public void writeStartElement(String localName) throws XMLStreamException {
 208         if (localName == null || localName.length() == 0) {
 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      */
 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      */
 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 
 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 
 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 
 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 
 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 
 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      */
 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      */
 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      */
 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      */
 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 
 422     private void writeXMLContent(String content) throws XMLStreamException {
 423         if ((content != null) && (content.length() > 0)) {
 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      */
 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      */
 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      */
 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      */
 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      */
 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      */
 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          */
 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          */
 609         public Element(Element parent, String localpart, boolean isEmpty) {
 610             _parent = parent;
 611             _localpart = localpart;
 612             _isEmptyElement = isEmpty;
 613         }
 614 
 615         public Element getParent() {
 616             return _parent;
 617         }
 618 
 619         public String getLocalName() {
 620             return _localpart;
 621         }
 622 
 623         /**
 624          * get the state of the element
 625          */
 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          */
 635         public void setState(int state) {
 636             _state = state;
 637         }
 638 
 639         public boolean isEmpty() {
 640             return _isEmptyElement;
 641         }
 642     }
 643 }