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