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