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 
  31 /**
  32  * Implementation of a reduced version of XMLStreamWriter 
  33  * 
  34  * @author Joe Wang
  35  */
  36 public class XMLStreamWriterImpl implements XMLStreamWriter {
  37     //Document state
  38     static final int STATE_XML_DECL = 1;
  39     static final int STATE_PROLOG = 2;
  40     static final int STATE_DTD_DECL = 3;
  41     static final int STATE_ELEMENT = 4;
  42     //Element state
  43     static final int ELEMENT_STARTTAG_OPEN = 10;
  44     static final int ELEMENT_STARTTAG_CLOSE = 11;
  45     static final int ELEMENT_ENDTAG_OPEN = 12;
  46     static final int ELEMENT_ENDTAG_CLOSE = 13;
  47     public static final char CLOSE_START_TAG = '>';
  48     public static final char OPEN_START_TAG = '<';
  49     public static final String OPEN_END_TAG = "</";
  50     public static final char CLOSE_END_TAG = '>';
  51     public static final String START_CDATA = "<![CDATA[";
  52     public static final String END_CDATA = "]]>";
  53     public static final String CLOSE_EMPTY_ELEMENT = "/>";
  54     public static final String ENCODING_PREFIX = "&#x";
  55     public static final char SPACE = ' ';
  56     public static final char AMPERSAND = '&';
  57     public static final char DOUBLEQUOT = '"';
  58     public static final char SEMICOLON = ';';
  59 
  60     //current state
  61     private int _state = 0;
  62     private Element _currentEle;
  63     private XMLWriter _writer;
  64     private String _encoding;
  65     /**
  66      * This flag can be used to turn escaping off for content. It does
  67      * not apply to attribute content.
  68      */
  69     boolean _escapeCharacters = true;
  70     //pretty print by default
  71     private boolean _doIndent = true;
  72     //The system line separator for writing out line breaks.
  73     private char[] _lineSep =
  74         System.getProperty("line.separator").toCharArray();
  75 
  76     public XMLStreamWriterImpl(OutputStream os)
  77             throws XMLStreamException {
  78         this(os, XMLStreamWriter.DEFAULT_ENCODING);
  79     }
  80 
  81     public XMLStreamWriterImpl(OutputStream os, String encoding)
  82             throws XMLStreamException {
  83         if (encoding == null) {
  84             _encoding = XMLStreamWriter.DEFAULT_ENCODING;
  85         } else {
  86             if (!isEncodingSupported(encoding)) {
  87                 throw new XMLStreamException(
  88                         new UnsupportedEncodingException("The basic XMLWriter does "
  89                         + "not support " + encoding));
  90             }
  91             this._encoding = encoding;
  92         }
  93 
  94         _writer = new XMLWriter(os, encoding);
  95     }
  96 
  97     /**
  98      * Write the XML Declaration. Defaults the XML version to 1.0, and the
  99      * encoding to utf-8.
 100      *
 101      * @throws XMLStreamException
 102      */
 103     public void writeStartDocument()
 104             throws XMLStreamException {
 105         writeStartDocument(_encoding, XMLStreamWriter.DEFAULT_XML_VERSION);
 106     }
 107 
 108     /**
 109      * Write the XML Declaration. Defaults the encoding to utf-8
 110      *
 111      * @param version version of the xml document
 112      * @throws XMLStreamException
 113      */
 114     public void writeStartDocument(String version)
 115             throws XMLStreamException {
 116         writeStartDocument(_encoding, version, null);
 117     }
 118 
 119     /**
 120      * Write the XML Declaration. Note that the encoding parameter does not set
 121      * the actual encoding of the underlying output. That must be set when the
 122      * instance of the XMLStreamWriter is created
 123      *
 124      * @param encoding encoding of the xml declaration
 125      * @param version version of the xml document
 126      * @throws XMLStreamException If given encoding does not match encoding of the
 127      * underlying stream
 128      */
 129     public void writeStartDocument(String encoding, String version)
 130             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         if (_state > 0) {
 148             throw new XMLStreamException("XML declaration must be as the first line in the XML document.");
 149         }
 150         _state = STATE_XML_DECL;
 151         String enc = encoding;
 152         if (enc == null) {
 153             enc = _encoding;
 154         } else {
 155             if (!isEncodingSupported(encoding)) {
 156                 throw new XMLStreamException(
 157                         new UnsupportedEncodingException("The basic XMLWriter does "
 158                         + "not support " + encoding));
 159             }
 160         }
 161 
 162         if (version == null) {
 163             version = XMLStreamWriter.DEFAULT_XML_VERSION;
 164         }
 165 
 166         _writer.write("<?xml version=\"");
 167         _writer.write(version);
 168         _writer.write(DOUBLEQUOT);
 169 
 170         if (enc != null) {
 171             _writer.write(" encoding=\"");
 172             _writer.write(enc);
 173             _writer.write(DOUBLEQUOT);
 174         }
 175 
 176         if (standalone != null) {
 177             _writer.write(" standalone=\"");
 178             _writer.write(standalone);
 179             _writer.write(DOUBLEQUOT);
 180         }
 181         _writer.write("?>");
 182         writeLineSeparator();
 183     }
 184 
 185     
 186   /**
 187    * Write a DTD section.  This string represents the entire doctypedecl production
 188    * from the XML 1.0 specification.
 189    *
 190    * @param dtd the DTD to be written
 191    * @throws XMLStreamException
 192    */
 193     public void writeDTD(String dtd) throws XMLStreamException {
 194         if (_currentEle != null && _currentEle.getState() == ELEMENT_STARTTAG_OPEN) {
 195             closeStartTag();
 196         }
 197         _writer.write(dtd);
 198         writeLineSeparator();
 199     }
 200 
 201   /**
 202    * Writes a start tag to the output.  
 203    * @param localName local name of the tag, may not be null
 204    * @throws XMLStreamException
 205    */
 206     public void writeStartElement(String localName) throws XMLStreamException {
 207         if (localName == null || localName.length() == 0) {
 208             throw new XMLStreamException("Local Name cannot be null or empty");
 209         }
 210 
 211         _state = STATE_ELEMENT;
 212         if (_currentEle != null && _currentEle.getState() == ELEMENT_STARTTAG_OPEN) {
 213             closeStartTag();
 214         }
 215 
 216         _currentEle = new Element(_currentEle, localName, false);
 217         openStartTag();
 218 
 219         _writer.write(localName);
 220     }
 221 
 222   /**
 223    * Writes an empty element tag to the output
 224    * @param localName local name of the tag, may not be null
 225    * @throws XMLStreamException
 226    */
 227     public void writeEmptyElement(String localName) throws XMLStreamException {
 228         if (_currentEle != null && _currentEle.getState() == ELEMENT_STARTTAG_OPEN) {
 229             closeStartTag();
 230         }
 231 
 232         _currentEle = new Element(_currentEle, localName, true);
 233 
 234         openStartTag();
 235         _writer.write(localName);
 236     }
 237 
 238   /**
 239    * Writes an attribute to the output stream without a prefix.
 240    * @param localName the local name of the attribute
 241    * @param value the value of the attribute
 242    * @throws IllegalStateException if the current state does not allow Attribute writing
 243    * @throws XMLStreamException
 244    */
 245     public void writeAttribute(String localName, String value)
 246         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      * Writes XML content to underlying writer. Escapes characters unless
 364      * escaping character feature is turned off.
 365      */
 366     private void writeXMLContent(char[] content, int start, int length,
 367         boolean escapeChars) throws XMLStreamException {
 368         if (!escapeChars) {
 369             _writer.write(content, start, length);
 370             return;
 371         }
 372 
 373         // Index of the next char to be written
 374         int startWritePos = start;
 375 
 376         final int end = start + length;
 377 
 378         for (int index = start; index < end; index++) {
 379             char ch = content[index];
 380 
 381             if (!_writer.canEncode(ch)){
 382                 _writer.write(content, startWritePos, index - startWritePos );
 383 
 384                 // Escape this char as underlying encoder cannot handle it
 385                 _writer.write( ENCODING_PREFIX );
 386                 _writer.write(Integer.toHexString(ch));
 387                 _writer.write( SEMICOLON );
 388                 startWritePos = index + 1;
 389                 continue;
 390             }
 391 
 392             switch (ch) {
 393             case OPEN_START_TAG:
 394                 _writer.write(content, startWritePos, index - startWritePos);
 395                 _writer.write("&lt;");
 396                 startWritePos = index + 1;
 397 
 398                 break;
 399 
 400             case AMPERSAND:
 401                 _writer.write(content, startWritePos, index - startWritePos);
 402                 _writer.write("&amp;");
 403                 startWritePos = index + 1;
 404 
 405                 break;
 406 
 407             case CLOSE_START_TAG:
 408                 _writer.write(content, startWritePos, index - startWritePos);
 409                 _writer.write("&gt;");
 410                 startWritePos = index + 1;
 411 
 412                 break;
 413             }
 414         }
 415 
 416         // Write any pending data
 417         _writer.write(content, startWritePos, end - startWritePos);
 418     }
 419 
 420     private void writeXMLContent(String content) throws XMLStreamException {
 421         if ((content != null) && (content.length() > 0)) {
 422             writeXMLContent(content,
 423                     _escapeCharacters,  // boolean = escapeChars
 424                     false);             // false = escapeDoubleQuotes
 425         }
 426     }
 427 
 428     /**
 429      * Writes XML content to underlying writer. Escapes characters unless
 430      * escaping character feature is turned off.
 431      */
 432     private void writeXMLContent(
 433             String content,
 434             boolean escapeChars,
 435             boolean escapeDoubleQuotes)
 436         throws XMLStreamException {
 437 
 438         if (!escapeChars) {
 439             _writer.write(content);
 440 
 441             return;
 442         }
 443 
 444         // Index of the next char to be written
 445         int startWritePos = 0;
 446 
 447         final int end = content.length();
 448 
 449         for (int index = 0; index < end; index++) {
 450             char ch = content.charAt(index);
 451 
 452             if (!_writer.canEncode(ch)){
 453                 _writer.write(content, startWritePos, index - startWritePos );
 454 
 455                 // Escape this char as underlying encoder cannot handle it
 456                 _writer.write( ENCODING_PREFIX );
 457                 _writer.write(Integer.toHexString(ch));
 458                 _writer.write( SEMICOLON );
 459                 startWritePos = index + 1;
 460                 continue;
 461             }
 462 
 463             switch (ch) {
 464             case OPEN_START_TAG:
 465                 _writer.write(content, startWritePos, index - startWritePos);
 466                 _writer.write("&lt;");
 467                 startWritePos = index + 1;
 468 
 469                 break;
 470 
 471             case AMPERSAND:
 472                 _writer.write(content, startWritePos, index - startWritePos);
 473                 _writer.write("&amp;");
 474                 startWritePos = index + 1;
 475 
 476                 break;
 477 
 478             case CLOSE_START_TAG:
 479                 _writer.write(content, startWritePos, index - startWritePos);
 480                 _writer.write("&gt;");
 481                 startWritePos = index + 1;
 482 
 483                 break;
 484 
 485             case DOUBLEQUOT:
 486                 _writer.write(content, startWritePos, index - startWritePos);
 487                 if (escapeDoubleQuotes) {
 488                     _writer.write("&quot;");
 489                 } else {
 490                     _writer.write(DOUBLEQUOT);
 491                 }
 492                 startWritePos = index + 1;
 493 
 494                 break;
 495             }
 496         }
 497 
 498         // Write any pending data
 499         _writer.write(content, startWritePos, end - startWritePos);
 500     }
 501 
 502 
 503     /**
 504      * marks open of start tag and writes the same into the writer.
 505      */
 506     private void openStartTag() throws XMLStreamException {
 507         _currentEle.setState(ELEMENT_STARTTAG_OPEN);
 508         _writer.write(OPEN_START_TAG);
 509     }
 510 
 511     /**
 512      * marks close of start tag and writes the same into the writer.
 513      */
 514     private void closeStartTag() throws XMLStreamException {
 515         if (_currentEle.isEmpty()) {
 516             _writer.write(CLOSE_EMPTY_ELEMENT);
 517         } else {
 518             _writer.write(CLOSE_START_TAG);
 519             
 520         }
 521 
 522         if (_currentEle.getParent() == null) {
 523             writeLineSeparator();
 524         }
 525         
 526         _currentEle.setState(ELEMENT_STARTTAG_CLOSE);
 527 
 528     }
 529 
 530     /**
 531      * Write a line separator
 532      * @throws XMLStreamException 
 533      */
 534     private void writeLineSeparator() throws XMLStreamException {
 535         if (_doIndent) {
 536             _writer.write(_lineSep, 0, _lineSep.length);        
 537         }
 538     }
 539     private boolean isEncodingSupported(String encoding) {
 540         if (encoding.equalsIgnoreCase("UTF-32")) {
 541             return false;
 542         }
 543         return true;
 544     }
 545     /*
 546      * Start of Internal classes.
 547      *
 548      */
 549     protected class Element {
 550 
 551         /**
 552          * the parent element
 553          */
 554         protected Element _parent;
 555         /**
 556          * The size of the stack.
 557          */
 558         protected short _Depth;
 559         /**
 560          * indicate if an element is an empty one
 561          */
 562         boolean _isEmptyElement = false;
 563         String _localpart;
 564         int _state;
 565 
 566         /**
 567          * Default constructor.
 568          */
 569         public Element() {
 570         }
 571 
 572         /**
 573          * @param parent the parent of the element
 574          * @param localpart name of the element
 575          * @param isEmpty indicate if the element is an empty one
 576          */
 577         public Element(Element parent, String localpart, boolean isEmpty) {
 578             _parent = parent;
 579             _localpart = localpart;
 580             _isEmptyElement = isEmpty;
 581         }
 582 
 583         public Element getParent() {
 584             return _parent;
 585         }
 586 
 587         public String getLocalName() {
 588             return _localpart;
 589         }
 590 
 591         /**
 592          * get the state of the element
 593          */
 594         public int getState() {
 595             return _state;
 596         }
 597 
 598         /**
 599          * Set the state of the element
 600          *
 601          * @param state the state of the element
 602          */
 603         public void setState(int state) {
 604             _state = state;
 605         }
 606 
 607         public boolean isEmpty() {
 608             return _isEmptyElement;
 609         }
 610     }
 611 }