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