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