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