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