MXSerializer.java
1 /* 2 * Copyright (C) 2010 Ryszard Wiśniewski <brut.alll@gmail.com> 3 * Copyright (C) 2010 Connor Tumbleson <connor.tumbleson@gmail.com> 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * https://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 package brut.xmlpull; 18 19 import org.xmlpull.v1.XmlSerializer; 20 21 import java.io.IOException; 22 import java.io.OutputStream; 23 import java.io.OutputStreamWriter; 24 import java.io.Writer; 25 import java.util.HashSet; 26 import java.util.Objects; 27 import java.util.Set; 28 29 /** 30 * Implementation of XmlSerializer interface from XmlPull V1 API. This 31 * implementation is optimized for performance and low memory footprint. 32 * 33 * <p> 34 * Implemented features: 35 * <ul> 36 * <li>FEATURE_ATTR_VALUE_NO_ESCAPE 37 * <li>FEATURE_NAMES_INTERNED - when enabled all returned names (namespaces, 38 * prefixes) will be interned and it is required that all names passed as 39 * arguments MUST be interned 40 * </ul> 41 * <p> 42 * Implemented properties: 43 * <ul> 44 * <li>PROPERTY_DEFAULT_ENCODING 45 * <li>PROPERTY_INDENTATION 46 * <li>PROPERTY_LINE_SEPARATOR 47 * <li>PROPERTY_LOCATION 48 * </ul> 49 * 50 */ 51 public class MXSerializer implements XmlSerializer { 52 public static final String FEATURE_ATTR_VALUE_NO_ESCAPE = "http://xmlpull.org/v1/doc/features.html#attr-value-no-escape"; 53 public static final String FEATURE_NAMES_INTERNED = "http://xmlpull.org/v1/doc/features.html#names-interned"; 54 public static final String PROPERTY_DEFAULT_ENCODING = "http://xmlpull.org/v1/doc/properties.html#default-encoding"; 55 public static final String PROPERTY_INDENTATION = "http://xmlpull.org/v1/doc/properties.html#indentation"; 56 public static final String PROPERTY_LINE_SEPARATOR = "http://xmlpull.org/v1/doc/properties.html#line-separator"; 57 public static final String PROPERTY_LOCATION = "http://xmlpull.org/v1/doc/properties.html#location"; 58 59 private static final boolean TRACE_SIZING = false; 60 private static final boolean TRACE_ESCAPING = false; 61 private static final String XML_URI = "http://www.w3.org/XML/1998/namespace"; 62 private static final String XMLNS_URI = "http://www.w3.org/2000/xmlns/"; 63 64 // properties/features 65 private boolean namesInterned; 66 private boolean attrValueNoEscape; 67 private String defaultEncoding; 68 private String indentationString; 69 private String lineSeparator; 70 71 private String location; 72 private Writer writer; 73 74 private int autoDeclaredPrefixes; 75 76 private int depth = 0; 77 78 // element stack 79 private String[] elNamespace = new String[2]; 80 private String[] elName = new String[elNamespace.length]; 81 private String[] elPrefix = new String[elNamespace.length]; 82 private int[] elNamespaceCount = new int[elNamespace.length]; 83 84 // namespace stack 85 private int namespaceEnd = 0; 86 private String[] namespacePrefix = new String[8]; 87 private String[] namespaceUri = new String[namespacePrefix.length]; 88 89 private boolean finished; 90 private boolean pastRoot; 91 private boolean setPrefixCalled; 92 private boolean startTagIncomplete; 93 94 private boolean doIndent; 95 private boolean seenTag; 96 97 private boolean seenBracket; 98 private boolean seenBracketBracket; 99 100 private static final String[] precomputedPrefixes; 101 static { 102 precomputedPrefixes = new String[32]; // arbitrary number ... 103 for (int i = 0; i < precomputedPrefixes.length; i++) { 104 precomputedPrefixes[i] = ("n" + i).intern(); 105 } 106 } 107 108 private final boolean checkNamesInterned = false; 109 110 private void checkInterning(String name) { 111 if (namesInterned && !Objects.equals(name, name.intern())) { 112 throw new IllegalArgumentException("all names passed as arguments must be interned" 113 + "when NAMES INTERNED feature is enabled"); 114 } 115 } 116 117 private String getLocation() { 118 return location != null ? " @" + location : ""; 119 } 120 121 private void ensureElementsCapacity() { 122 int elStackSize = elName.length; 123 int newSize = (depth >= 7 ? 2 * depth : 8) + 2; 124 125 if (TRACE_SIZING) { 126 System.err.println(getClass().getName() + " elStackSize " 127 + elStackSize + " ==> " + newSize); 128 } 129 boolean needsCopying = elStackSize > 0; 130 String[] arr; 131 // reuse arr local variable slot 132 arr = new String[newSize]; 133 if (needsCopying) { 134 System.arraycopy(elName, 0, arr, 0, elStackSize); 135 } 136 elName = arr; 137 138 arr = new String[newSize]; 139 if (needsCopying) { 140 System.arraycopy(elPrefix, 0, arr, 0, elStackSize); 141 } 142 elPrefix = arr; 143 144 arr = new String[newSize]; 145 if (needsCopying) { 146 System.arraycopy(elNamespace, 0, arr, 0, elStackSize); 147 } 148 elNamespace = arr; 149 150 int[] iarr = new int[newSize]; 151 if (needsCopying) { 152 System.arraycopy(elNamespaceCount, 0, iarr, 0, elStackSize); 153 } else { 154 // special initialization 155 iarr[0] = 0; 156 } 157 elNamespaceCount = iarr; 158 } 159 160 private void ensureNamespacesCapacity() { 161 int newSize = namespaceEnd > 7 ? 2 * namespaceEnd : 8; 162 if (TRACE_SIZING) { 163 System.err.println(getClass().getName() + " namespaceSize " + namespacePrefix.length + " ==> " + newSize); 164 } 165 String[] newNamespacePrefix = new String[newSize]; 166 String[] newNamespaceUri = new String[newSize]; 167 if (namespacePrefix != null) { 168 System.arraycopy(namespacePrefix, 0, newNamespacePrefix, 0, namespaceEnd); 169 System.arraycopy(namespaceUri, 0, newNamespaceUri, 0, namespaceEnd); 170 } 171 namespacePrefix = newNamespacePrefix; 172 namespaceUri = newNamespaceUri; 173 } 174 175 // use buffer to optimize writing 176 private static final int BUFFER_LEN = 8192; 177 private final char[] buffer = new char[BUFFER_LEN]; 178 private int bufidx; 179 180 private void flushBuffer() throws IOException { 181 if (bufidx > 0) { 182 writer.write(buffer, 0, bufidx); 183 writer.flush(); 184 bufidx = 0; 185 } 186 } 187 188 private void write(char ch) throws IOException { 189 if (bufidx >= BUFFER_LEN) { 190 flushBuffer(); 191 } 192 buffer[bufidx++] = ch; 193 } 194 195 private void write(char[] buf, int i, int length) throws IOException { 196 while (length > 0) { 197 if (bufidx == BUFFER_LEN) { 198 flushBuffer(); 199 } 200 int batch = BUFFER_LEN - bufidx; 201 if (batch > length) { 202 batch = length; 203 } 204 System.arraycopy(buf, i, buffer, bufidx, batch); 205 i += batch; 206 length -= batch; 207 bufidx += batch; 208 } 209 } 210 211 private void write(String str) throws IOException { 212 if (str == null) { 213 str = ""; 214 } 215 write(str, 0, str.length()); 216 } 217 218 private void write(String str, int i, int length) throws IOException { 219 while (length > 0) { 220 if (bufidx == BUFFER_LEN) { 221 flushBuffer(); 222 } 223 int batch = BUFFER_LEN - bufidx; 224 if (batch > length) { 225 batch = length; 226 } 227 str.getChars(i, i + batch, buffer, bufidx); 228 i += batch; 229 length -= batch; 230 bufidx += batch; 231 } 232 } 233 234 // precomputed variables to simplify writing indentation 235 private static final int MAX_INDENT = 65; 236 private int offsetNewLine; 237 private int indentationJump; 238 private char[] indentationBuf; 239 private int maxIndentLevel; 240 private boolean writeLineSeparator; // should end-of-line be written 241 private boolean writeIndentation; // is indentation used? 242 243 /** 244 * For maximum efficiency when writing indents the required output is 245 * pre-computed This is internal function that recomputes buffer after user 246 * requested changes. 247 */ 248 private void rebuildIndentationBuf() { 249 if (!doIndent) { 250 return; 251 } 252 int bufSize = 0; 253 offsetNewLine = 0; 254 if (writeLineSeparator) { 255 offsetNewLine = lineSeparator.length(); 256 bufSize += offsetNewLine; 257 } 258 maxIndentLevel = 0; 259 if (writeIndentation) { 260 indentationJump = indentationString.length(); 261 maxIndentLevel = MAX_INDENT / indentationJump; 262 bufSize += maxIndentLevel * indentationJump; 263 } 264 if (indentationBuf == null || indentationBuf.length < bufSize) { 265 indentationBuf = new char[bufSize + 8]; 266 } 267 int bufPos = 0; 268 if (writeLineSeparator) { 269 for (int i = 0; i < lineSeparator.length(); i++) { 270 indentationBuf[bufPos++] = lineSeparator.charAt(i); 271 } 272 } 273 if (writeIndentation) { 274 for (int i = 0; i < maxIndentLevel; i++) { 275 for (int j = 0; j < indentationString.length(); j++) { 276 indentationBuf[bufPos++] = indentationString.charAt(j); 277 } 278 } 279 } 280 } 281 282 private void writeIndent() throws IOException { 283 int start = writeLineSeparator ? 0 : offsetNewLine; 284 int level = Math.min(depth, maxIndentLevel); 285 286 write(indentationBuf, start, ((level - 1) * indentationJump) + offsetNewLine); 287 } 288 289 // --- public API methods 290 291 @Override 292 public void setFeature(String name, boolean state) 293 throws IllegalArgumentException, IllegalStateException { 294 if (name == null) { 295 throw new IllegalArgumentException("feature name can not be null"); 296 } 297 switch (name) { 298 case FEATURE_ATTR_VALUE_NO_ESCAPE: 299 attrValueNoEscape = state; 300 break; 301 case FEATURE_NAMES_INTERNED: 302 namesInterned = state; 303 break; 304 default: 305 throw new IllegalStateException("unsupported feature: " + name); 306 } 307 } 308 309 @Override 310 public boolean getFeature(String name) throws IllegalArgumentException { 311 if (name == null) { 312 throw new IllegalArgumentException("feature name can not be null"); 313 } 314 switch (name) { 315 case FEATURE_ATTR_VALUE_NO_ESCAPE: 316 return attrValueNoEscape; 317 case FEATURE_NAMES_INTERNED: 318 return namesInterned; 319 default: 320 return false; 321 } 322 } 323 324 @Override 325 public void setProperty(String name, Object value) 326 throws IllegalArgumentException, IllegalStateException { 327 if (name == null) { 328 throw new IllegalArgumentException("property name can not be null"); 329 } 330 switch (name) { 331 case PROPERTY_DEFAULT_ENCODING: 332 defaultEncoding = (String) value; 333 break; 334 case PROPERTY_INDENTATION: 335 indentationString = (String) value; 336 break; 337 case PROPERTY_LINE_SEPARATOR: 338 lineSeparator = (String) value; 339 break; 340 case PROPERTY_LOCATION: 341 location = (String) value; 342 break; 343 default: 344 throw new IllegalStateException("unsupported property: " + name); 345 } 346 writeLineSeparator = lineSeparator != null && !lineSeparator.isEmpty(); 347 writeIndentation = indentationString != null && !indentationString.isEmpty(); 348 // optimize - do not write when nothing to write ... 349 doIndent = indentationString != null && (writeLineSeparator || writeIndentation); 350 // NOTE: when indentationString == null there is no indentation 351 // (even though writeLineSeparator may be true ...) 352 rebuildIndentationBuf(); 353 seenTag = false; // for consistency 354 } 355 356 @Override 357 public Object getProperty(String name) throws IllegalArgumentException { 358 if (name == null) { 359 throw new IllegalArgumentException("property name can not be null"); 360 } 361 switch (name) { 362 case PROPERTY_DEFAULT_ENCODING: 363 return defaultEncoding; 364 case PROPERTY_INDENTATION: 365 return indentationString; 366 case PROPERTY_LINE_SEPARATOR: 367 return lineSeparator; 368 case PROPERTY_LOCATION: 369 return location; 370 default: 371 return null; 372 } 373 } 374 375 @Override 376 public void setOutput(Writer writer) { 377 this.writer = writer; 378 379 // reset state 380 location = null; 381 autoDeclaredPrefixes = 0; 382 depth = 0; 383 384 // nullify references on all levels to allow it to be GCed 385 for (int i = 0; i < elNamespaceCount.length; i++) { 386 elName[i] = null; 387 elPrefix[i] = null; 388 elNamespace[i] = null; 389 elNamespaceCount[i] = 2; 390 } 391 392 namespaceEnd = 0; 393 394 // TODO: how to prevent from reporting this namespace? 395 // this is special namespace declared for consistency with XML infoset 396 namespacePrefix[namespaceEnd] = "xmlns"; 397 namespaceUri[namespaceEnd] = XMLNS_URI; 398 ++namespaceEnd; 399 400 namespacePrefix[namespaceEnd] = "xml"; 401 namespaceUri[namespaceEnd] = XML_URI; 402 ++namespaceEnd; 403 404 finished = false; 405 pastRoot = false; 406 setPrefixCalled = false; 407 startTagIncomplete = false; 408 seenTag = false; 409 410 seenBracket = false; 411 seenBracketBracket = false; 412 } 413 414 @Override 415 public void setOutput(OutputStream os, String encoding) throws IOException { 416 if (os == null) { 417 throw new IllegalArgumentException("output stream can not be null"); 418 } 419 if (encoding == null) { 420 encoding = defaultEncoding; 421 } 422 setOutput(encoding != null 423 ? new OutputStreamWriter(os, encoding) 424 : new OutputStreamWriter(os)); 425 } 426 427 @Override 428 public void startDocument(String encoding, Boolean standalone) throws IOException { 429 write("<?xml version=\"1.0\""); 430 if (encoding == null) { 431 encoding = defaultEncoding; 432 } 433 if (encoding != null) { 434 write(" encoding=\""); 435 write(encoding); 436 write('"'); 437 } 438 if (standalone != null) { 439 write(" standalone=\""); 440 if (standalone.booleanValue()) { 441 write("yes"); 442 } else { 443 write("no"); 444 } 445 write('"'); 446 } 447 write("?>"); 448 if (writeLineSeparator) { 449 write(lineSeparator); 450 } 451 } 452 453 @Override 454 public void endDocument() throws IOException { 455 // close all unclosed tag; 456 while (depth > 0) { 457 endTag(elNamespace[depth], elName[depth]); 458 } 459 if (writeLineSeparator) { 460 write(lineSeparator); 461 } 462 flushBuffer(); 463 finished = pastRoot = startTagIncomplete = true; 464 } 465 466 @Override 467 public void setPrefix(String prefix, String namespace) throws IOException { 468 if (startTagIncomplete) { 469 closeStartTag(); 470 } 471 if (prefix == null) { 472 prefix = ""; 473 } 474 if (!namesInterned) { 475 prefix = prefix.intern(); // will throw NPE if prefix==null 476 } else if (checkNamesInterned) { 477 checkInterning(prefix); 478 } else if (prefix == null) { 479 throw new IllegalArgumentException("prefix must be not null" + getLocation()); 480 } 481 482 if (!namesInterned) { 483 namespace = namespace.intern(); 484 } else if (checkNamesInterned) { 485 checkInterning(namespace); 486 } else if (namespace == null) { 487 throw new IllegalArgumentException("namespace must be not null" + getLocation()); 488 } 489 490 if (namespaceEnd >= namespacePrefix.length) { 491 ensureNamespacesCapacity(); 492 } 493 namespacePrefix[namespaceEnd] = prefix; 494 namespaceUri[namespaceEnd] = namespace; 495 ++namespaceEnd; 496 setPrefixCalled = true; 497 } 498 499 @Override 500 public String getPrefix(String namespace, boolean generatePrefix) { 501 return getPrefix(namespace, generatePrefix, false); 502 } 503 504 private String getPrefix(String namespace, boolean generatePrefix, boolean nonEmpty) { 505 if (!namesInterned) { 506 // when String is interned we can do much faster namespace stack lookups ... 507 namespace = namespace.intern(); 508 } else if (checkNamesInterned) { 509 checkInterning(namespace); 510 } 511 if (namespace == null) { 512 throw new IllegalArgumentException("namespace must be not null" + getLocation()); 513 } else if (namespace.isEmpty()) { 514 throw new IllegalArgumentException("default namespace cannot have prefix" + getLocation()); 515 } 516 517 // first check if namespace is already in scope 518 for (int i = namespaceEnd - 1; i >= 0; --i) { 519 if (namespace.equals(namespaceUri[i])) { 520 String prefix = namespacePrefix[i]; 521 if (nonEmpty && prefix.isEmpty()) { 522 continue; 523 } 524 525 return prefix; 526 } 527 } 528 529 // so not found it ... 530 if (!generatePrefix) { 531 return null; 532 } 533 return generatePrefix(namespace); 534 } 535 536 @Override 537 public int getDepth() { 538 return depth; 539 } 540 541 @Override 542 public String getNamespace() { 543 return elNamespace[depth]; 544 } 545 546 @Override 547 public String getName() { 548 return elName[depth]; 549 } 550 551 @Override 552 public XmlSerializer startTag(String namespace, String name) throws IOException { 553 if (startTagIncomplete) { 554 closeStartTag(); 555 } 556 seenBracket = seenBracketBracket = false; 557 ++depth; 558 if (doIndent && depth > 0 && seenTag) { 559 writeIndent(); 560 } 561 seenTag = true; 562 setPrefixCalled = false; 563 startTagIncomplete = true; 564 if ((depth + 1) >= elName.length) { 565 ensureElementsCapacity(); 566 } 567 568 if (checkNamesInterned && namesInterned) { 569 checkInterning(namespace); 570 } 571 572 elNamespace[depth] = (namesInterned || namespace == null) ? namespace : namespace.intern(); 573 if (checkNamesInterned && namesInterned) { 574 checkInterning(name); 575 } 576 577 elName[depth] = (namesInterned || name == null) ? name : name.intern(); 578 if (writer == null) { 579 throw new IllegalStateException("setOutput() must called set before serialization can start"); 580 } 581 write('<'); 582 if (namespace != null) { 583 if (!namespace.isEmpty()) { 584 // in future make this algo a feature on serializer 585 String prefix = null; 586 if (depth > 0 && (namespaceEnd - elNamespaceCount[depth - 1]) == 1) { 587 // if only one prefix was declared un-declare it if the 588 // prefix is already declared on parent el with the same URI 589 String uri = namespaceUri[namespaceEnd - 1]; 590 if (uri == namespace || uri.equals(namespace)) { 591 String elPfx = namespacePrefix[namespaceEnd - 1]; 592 for (int pos = elNamespaceCount[depth - 1] - 1; pos >= 2; --pos) { 593 String pf = namespacePrefix[pos]; 594 if (pf == elPfx || pf.equals(elPfx)) { 595 String n = namespaceUri[pos]; 596 if (n == uri || n.equals(uri)) { 597 --namespaceEnd; // un-declare namespace: this is kludge! 598 prefix = elPfx; 599 } 600 break; 601 } 602 } 603 } 604 } 605 if (prefix == null) { 606 prefix = getPrefix(namespace, true, false); 607 } 608 // make sure that default ("") namespace to not print ":" 609 if (!prefix.isEmpty()) { 610 elPrefix[depth] = prefix; 611 write(prefix); 612 write(':'); 613 } else { 614 elPrefix[depth] = ""; 615 } 616 } else { 617 // make sure that default namespace can be declared 618 for (int i = namespaceEnd - 1; i >= 0; --i) { 619 if (namespacePrefix[i] == "") { 620 String uri = namespaceUri[i]; 621 if (uri == null) { 622 setPrefix("", ""); 623 } else if (!uri.isEmpty()) { 624 throw new IllegalStateException("start tag can not be written in empty default namespace " 625 + "as default namespace is currently bound to '" 626 + uri + "'" + getLocation()); 627 } 628 break; 629 } 630 } 631 elPrefix[depth] = ""; 632 } 633 } else { 634 elPrefix[depth] = ""; 635 } 636 write(name); 637 return this; 638 } 639 640 private void closeStartTag() throws IOException { 641 if (finished) { 642 throw new IllegalArgumentException("trying to write past already finished output" + getLocation()); 643 } 644 if (seenBracket) { 645 seenBracket = seenBracketBracket = false; 646 } 647 if (startTagIncomplete || setPrefixCalled) { 648 if (setPrefixCalled) { 649 throw new IllegalArgumentException("startTag() must be called immediately after setPrefix()" + getLocation()); 650 } 651 if (!startTagIncomplete) { 652 throw new IllegalArgumentException("trying to close start tag that is not opened" + getLocation()); 653 } 654 655 // write all namespace declarations! 656 writeNamespaceDeclarations(); 657 write('>'); 658 elNamespaceCount[depth] = namespaceEnd; 659 startTagIncomplete = false; 660 } 661 } 662 663 @Override 664 public XmlSerializer attribute(String namespace, String name, String value) throws IOException { 665 if (!startTagIncomplete) { 666 throw new IllegalArgumentException("startTag() must be called before attribute()" + getLocation()); 667 } 668 write(' '); 669 if (namespace != null && !namespace.isEmpty()) { 670 if (!namesInterned) { 671 namespace = namespace.intern(); 672 } else if (checkNamesInterned) { 673 checkInterning(namespace); 674 } 675 String prefix = getPrefix(namespace, false, true); 676 if (prefix == null) { 677 // needs to declare prefix to hold default namespace 678 // NOTE: attributes such as a='b' are in NO namespace 679 prefix = generatePrefix(namespace); 680 } 681 write(prefix); 682 write(':'); 683 } 684 write(name); 685 write("=\""); 686 writeAttributeValue(value); 687 write('"'); 688 return this; 689 } 690 691 @Override 692 public XmlSerializer endTag(String namespace, String name) throws IOException { 693 seenBracket = seenBracketBracket = false; 694 if (namespace != null) { 695 if (!namesInterned) { 696 namespace = namespace.intern(); 697 } else if (checkNamesInterned) { 698 checkInterning(namespace); 699 } 700 } 701 702 if (name == null) { 703 throw new IllegalArgumentException("end tag name can not be null" + getLocation()); 704 } 705 if (checkNamesInterned && namesInterned) { 706 checkInterning(name); 707 } 708 if (startTagIncomplete) { 709 writeNamespaceDeclarations(); 710 write(" />"); // space is added to make it easier to work in XHTML!!! 711 } else { 712 if (doIndent && seenTag) { 713 writeIndent(); 714 } 715 write("</"); 716 String startTagPrefix = elPrefix[depth]; 717 if (!startTagPrefix.isEmpty()) { 718 write(startTagPrefix); 719 write(':'); 720 } 721 write(name); 722 write('>'); 723 } 724 --depth; 725 namespaceEnd = elNamespaceCount[depth]; 726 startTagIncomplete = false; 727 seenTag = true; 728 return this; 729 } 730 731 @Override 732 public XmlSerializer text(String text) throws IOException { 733 if (startTagIncomplete || setPrefixCalled) { 734 closeStartTag(); 735 } 736 if (doIndent && seenTag) { 737 seenTag = false; 738 } 739 writeElementContent(text); 740 return this; 741 } 742 743 @Override 744 public XmlSerializer text(char[] buf, int start, int len) throws IOException { 745 if (startTagIncomplete || setPrefixCalled) { 746 closeStartTag(); 747 } 748 if (doIndent && seenTag) { 749 seenTag = false; 750 } 751 writeElementContent(buf, start, len); 752 return this; 753 } 754 755 @Override 756 public void cdsect(String text) throws IOException { 757 if (startTagIncomplete || setPrefixCalled || seenBracket) { 758 closeStartTag(); 759 } 760 if (doIndent && seenTag) { 761 seenTag = false; 762 } 763 write("<![CDATA["); 764 write(text); 765 write("]]>"); 766 } 767 768 @Override 769 public void entityRef(String text) throws IOException { 770 if (startTagIncomplete || setPrefixCalled || seenBracket) { 771 closeStartTag(); 772 } 773 if (doIndent && seenTag) { 774 seenTag = false; 775 } 776 write('&'); 777 write(text); 778 write(';'); 779 } 780 781 @Override 782 public void processingInstruction(String text) throws IOException { 783 if (startTagIncomplete || setPrefixCalled || seenBracket) { 784 closeStartTag(); 785 } 786 if (doIndent && seenTag) { 787 seenTag = false; 788 } 789 write("<?"); 790 write(text); 791 write("?>"); 792 } 793 794 @Override 795 public void comment(String text) throws IOException { 796 if (startTagIncomplete || setPrefixCalled || seenBracket) { 797 closeStartTag(); 798 } 799 if (doIndent && seenTag) { 800 seenTag = false; 801 } 802 write("<!--"); 803 write(text); 804 write("-->"); 805 } 806 807 @Override 808 public void docdecl(String text) throws IOException { 809 if (startTagIncomplete || setPrefixCalled || seenBracket) { 810 closeStartTag(); 811 } 812 if (doIndent && seenTag) { 813 seenTag = false; 814 } 815 write("<!DOCTYPE"); 816 write(text); 817 write(">"); 818 } 819 820 @Override 821 public void ignorableWhitespace(String text) throws IOException { 822 if (startTagIncomplete || setPrefixCalled || seenBracket) { 823 closeStartTag(); 824 } 825 if (doIndent && seenTag) { 826 seenTag = false; 827 } 828 if (text.isEmpty()) { 829 throw new IllegalArgumentException("empty string is not allowed for ignorable whitespace" + getLocation()); 830 } 831 write(text); 832 } 833 834 @Override 835 public void flush() throws IOException { 836 if (!finished && startTagIncomplete) { 837 closeStartTag(); 838 } 839 flushBuffer(); 840 } 841 842 // --- utility methods 843 844 private String generatePrefix(String namespace) { 845 ++autoDeclaredPrefixes; 846 // fast lookup uses table that was pre-initialized in static{} .... 847 String prefix = autoDeclaredPrefixes < precomputedPrefixes.length 848 ? precomputedPrefixes[autoDeclaredPrefixes] 849 : ("n" + autoDeclaredPrefixes).intern(); 850 851 // declare prefix 852 if (namespaceEnd >= namespacePrefix.length) { 853 ensureNamespacesCapacity(); 854 } 855 namespacePrefix[namespaceEnd] = prefix; 856 namespaceUri[namespaceEnd] = namespace; 857 ++namespaceEnd; 858 859 return prefix; 860 } 861 862 private void writeNamespaceDeclarations() throws IOException { 863 Set<String> uniqueNamespaces = new HashSet<>(); 864 for (int i = elNamespaceCount[depth - 1]; i < namespaceEnd; i++) { 865 String prefix = namespacePrefix[i]; 866 String uri = namespaceUri[i]; 867 868 // Some applications as seen in #2664 have duplicated namespaces. 869 // AOSP doesn't care, but the parser does. So we filter them writer. 870 if (uniqueNamespaces.contains(prefix + uri)) { 871 continue; 872 } 873 874 if (doIndent && uri.length() > 40) { 875 writeIndent(); 876 write(' '); 877 } 878 write(" xmlns"); 879 if (prefix != "") { 880 write(':'); 881 write(prefix); 882 } 883 write("=\""); 884 writeAttributeValue(uri); 885 write('"'); 886 887 uniqueNamespaces.add(prefix + uri); 888 } 889 } 890 891 private void writeAttributeValue(String value) throws IOException { 892 if (attrValueNoEscape) { 893 write(value); 894 return; 895 } 896 // .[&, < and " escaped], 897 int pos = 0; 898 for (int i = 0; i < value.length(); i++) { 899 char ch = value.charAt(i); 900 if (ch == '&') { 901 if (i > pos) { 902 write(value.substring(pos, i)); 903 } 904 write("&"); 905 pos = i + 1; 906 } 907 if (ch == '<') { 908 if (i > pos) { 909 write(value.substring(pos, i)); 910 } 911 write("<"); 912 pos = i + 1; 913 } else if (ch == '"') { 914 if (i > pos) { 915 write(value.substring(pos, i)); 916 } 917 write("""); 918 pos = i + 1; 919 } else if (ch < 32) { 920 // in XML 1.0 only legal character are #x9 | #xA | #xD 921 // and they must be escaped otherwise in attribute value they 922 // are normalized to spaces 923 if (ch == 13 || ch == 10 || ch == 9) { 924 if (i > pos) { 925 write(value.substring(pos, i)); 926 } 927 write("&#"); 928 write(Integer.toString(ch)); 929 write(';'); 930 pos = i + 1; 931 } else { 932 if (TRACE_ESCAPING) { 933 System.err.println(getClass().getName() + " DEBUG ATTR value.len=" + value.length() 934 + " " + printable(value)); 935 } 936 throw new IllegalStateException( 937 "character " + printable(ch) + " (" + Integer.toString(ch) + ") is not allowed in output" 938 + getLocation() + " (attr value=" 939 + printable(value) + ")"); 940 } 941 } 942 } 943 write(pos > 0 ? value.substring(pos) : value); 944 } 945 946 private void writeElementContent(String text) throws IOException { 947 // For some reason, some non-empty, empty characters are surviving this far and getting filtered writer 948 // So we are left with null, which causes an NPE 949 if (text == null) { 950 return; 951 } 952 953 // escape '<', '&', ']]>', <32 if necessary 954 int pos = 0; 955 for (int i = 0; i < text.length(); i++) { 956 // TODO: check if doing char[] text.getChars() would be faster than 957 // getCharAt(i) ... 958 char ch = text.charAt(i); 959 if (ch == ']') { 960 if (seenBracket) { 961 seenBracketBracket = true; 962 } else { 963 seenBracket = true; 964 } 965 } else { 966 if (ch == '&') { 967 if (!(i < text.length() - 3 && text.charAt(i+1) == 'l' 968 && text.charAt(i+2) == 't' && text.charAt(i+3) == ';')) { 969 if (i > pos) { 970 write(text.substring(pos, i)); 971 } 972 write("&"); 973 pos = i + 1; 974 } 975 } else if (ch == '<') { 976 if (i > pos) { 977 write(text.substring(pos, i)); 978 } 979 write("<"); 980 pos = i + 1; 981 } else if (seenBracketBracket && ch == '>') { 982 if (i > pos) { 983 write(text.substring(pos, i)); 984 } 985 write(">"); 986 pos = i + 1; 987 } else if (ch < 32) { 988 // in XML 1.0 only legal character are #x9 | #xA | #xD 989 if (ch == 9 || ch == 10 || ch == 13) { 990 // pass through 991 } else { 992 if (TRACE_ESCAPING) { 993 System.err.println(getClass().getName() + " DEBUG TEXT value.len=" + text.length() 994 + " " + printable(text)); 995 } 996 throw new IllegalStateException("character " + Integer.toString(ch) 997 + " is not allowed in output" + getLocation() 998 + " (text value=" + printable(text) + ")"); 999 } 1000 } 1001 if (seenBracket) { 1002 seenBracketBracket = seenBracket = false; 1003 } 1004 } 1005 } 1006 write(pos > 0 ? text.substring(pos) : text); 1007 } 1008 1009 private void writeElementContent(char[] buf, int off, int len) throws IOException { 1010 // escape '<', '&', ']]>' 1011 int end = off + len; 1012 int pos = off; 1013 for (int i = off; i < end; i++) { 1014 char ch = buf[i]; 1015 if (ch == ']') { 1016 if (seenBracket) { 1017 seenBracketBracket = true; 1018 } else { 1019 seenBracket = true; 1020 } 1021 } else { 1022 if (ch == '&') { 1023 if (i > pos) { 1024 write(buf, pos, i - pos); 1025 } 1026 write("&"); 1027 pos = i + 1; 1028 } else if (ch == '<') { 1029 if (i > pos) { 1030 write(buf, pos, i - pos); 1031 } 1032 write("<"); 1033 pos = i + 1; 1034 1035 } else if (seenBracketBracket && ch == '>') { 1036 if (i > pos) { 1037 write(buf, pos, i - pos); 1038 } 1039 write(">"); 1040 pos = i + 1; 1041 } else if (ch < 32) { 1042 // in XML 1.0 only legal character are #x9 | #xA | #xD 1043 if (ch == 9 || ch == 10 || ch == 13) { 1044 // pass through 1045 } else { 1046 if (TRACE_ESCAPING) { 1047 System.err.println(getClass().getName() + " DEBUG TEXT value.len=" + len + " " 1048 + printable(new String(buf, off, len))); 1049 } 1050 throw new IllegalStateException("character " 1051 + printable(ch) + " (" + Integer.toString(ch) 1052 + ") is not allowed in output" + getLocation()); 1053 } 1054 } 1055 if (seenBracket) { 1056 seenBracketBracket = seenBracket = false; 1057 } 1058 } 1059 } 1060 if (end > pos) { 1061 write(buf, pos, end - pos); 1062 } 1063 } 1064 1065 private static String printable(String str) { 1066 if (str == null) { 1067 return "null"; 1068 } 1069 StringBuffer retval = new StringBuffer(str.length() + 16); 1070 retval.append("'"); 1071 for (int i = 0; i < str.length(); i++) { 1072 addPrintable(retval, str.charAt(i)); 1073 } 1074 retval.append("'"); 1075 return retval.toString(); 1076 } 1077 1078 private static String printable(char ch) { 1079 StringBuffer retval = new StringBuffer(); 1080 addPrintable(retval, ch); 1081 return retval.toString(); 1082 } 1083 1084 private static void addPrintable(StringBuffer retval, char ch) { 1085 switch (ch) { 1086 case '\b': 1087 retval.append("\\b"); 1088 break; 1089 case '\t': 1090 retval.append("\\t"); 1091 break; 1092 case '\n': 1093 retval.append("\\n"); 1094 break; 1095 case '\f': 1096 retval.append("\\f"); 1097 break; 1098 case '\r': 1099 retval.append("\\r"); 1100 break; 1101 case '\"': 1102 retval.append("\\\""); 1103 break; 1104 case '\'': 1105 retval.append("\\'"); 1106 break; 1107 case '\\': 1108 retval.append("\\\\"); 1109 break; 1110 default: 1111 if (ch < 0x20 || ch > 0x7E) { 1112 String str = "0000" + Integer.toString(ch, 16); 1113 retval.append("\\u").append(str.substring(str.length() - 4)); 1114 } else { 1115 retval.append(ch); 1116 } 1117 break; 1118 } 1119 } 1120 }