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