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("<"); 399 startWritePos = index + 1; 400 401 break; 402 403 case AMPERSAND: 404 _writer.write(content, startWritePos, index - startWritePos); 405 _writer.write("&"); 406 startWritePos = index + 1; 407 408 break; 409 410 case CLOSE_START_TAG: 411 _writer.write(content, startWritePos, index - startWritePos); 412 _writer.write(">"); 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("<"); 471 startWritePos = index + 1; 472 473 break; 474 475 case AMPERSAND: 476 _writer.write(content, startWritePos, index - startWritePos); 477 _writer.write("&"); 478 startWritePos = index + 1; 479 480 break; 481 482 case CLOSE_START_TAG: 483 _writer.write(content, startWritePos, index - startWritePos); 484 _writer.write(">"); 485 startWritePos = index + 1; 486 487 break; 488 489 case DOUBLEQUOT: 490 _writer.write(content, startWritePos, index - startWritePos); 491 if (escapeDoubleQuotes) { 492 _writer.write("""); 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 }