/ brut.j.xml / src / main / java / brut / xmlpull / MXSerializer.java
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("&amp;");
 905                  pos = i + 1;
 906              }
 907              if (ch == '<') {
 908                  if (i > pos) {
 909                      write(value.substring(pos, i));
 910                  }
 911                  write("&lt;");
 912                  pos = i + 1;
 913              } else if (ch == '"') {
 914                  if (i > pos) {
 915                      write(value.substring(pos, i));
 916                  }
 917                  write("&quot;");
 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("&amp;");
 973                          pos = i + 1;
 974                      }
 975                  } else if (ch == '<') {
 976                      if (i > pos) {
 977                          write(text.substring(pos, i));
 978                      }
 979                      write("&lt;");
 980                      pos = i + 1;
 981                  } else if (seenBracketBracket && ch == '>') {
 982                      if (i > pos) {
 983                          write(text.substring(pos, i));
 984                      }
 985                      write("&gt;");
 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("&amp;");
1027                      pos = i + 1;
1028                  } else if (ch == '<') {
1029                      if (i > pos) {
1030                          write(buf, pos, i - pos);
1031                      }
1032                      write("&lt;");
1033                      pos = i + 1;
1034  
1035                  } else if (seenBracketBracket && ch == '>') {
1036                      if (i > pos) {
1037                          write(buf, pos, i - pos);
1038                      }
1039                      write("&gt;");
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  }