View Javadoc
1   /*
2    * Copyright (c) 2005, 2010, Oracle and/or its affiliates. All rights reserved.
3    * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4    *
5    * This code is free software; you can redistribute it and/or modify it
6    * under the terms of the GNU General Public License version 2 only, as
7    * published by the Free Software Foundation.  Oracle designates this
8    * particular file as subject to the "Classpath" exception as provided
9    * by Oracle in the LICENSE file that accompanied this code.
10   *
11   * This code is distributed in the hope that it will be useful, but WITHOUT
12   * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13   * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14   * version 2 for more details (a copy is included in the LICENSE file that
15   * accompanied this code).
16   *
17   * You should have received a copy of the GNU General Public License version
18   * 2 along with this work; if not, write to the Free Software Foundation,
19   * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20   *
21   * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22   * or visit www.oracle.com if you need additional information or have any
23   * questions.
24   */
25  
26  package com.sun.xml.internal.txw2;
27  
28  import com.sun.xml.internal.txw2.output.XmlSerializer;
29  
30  import java.util.Map;
31  import java.util.HashMap;
32  
33  /**
34   * Coordinates the entire writing process.
35   *
36   * @author Kohsuke Kawaguchi (kohsuke.kawaguchi@sun.com)
37   */
38  public final class Document {
39  
40      private final XmlSerializer out;
41  
42      /**
43       * Set to true once we invoke {@link XmlSerializer#startDocument()}.
44       *
45       * <p>
46       * This is so that we can defer the writing as much as possible.
47       */
48      private boolean started=false;
49  
50      /**
51       * Currently active writer.
52       *
53       * <p>
54       * This points to the last written token.
55       */
56      private Content current = null;
57  
58      private final Map<Class,DatatypeWriter> datatypeWriters = new HashMap<Class,DatatypeWriter>();
59  
60      /**
61       * Used to generate unique namespace prefix.
62       */
63      private int iota = 1;
64  
65      /**
66       * Used to keep track of in-scope namespace bindings declared in ancestors.
67       */
68      private final NamespaceSupport inscopeNamespace = new NamespaceSupport();
69  
70      /**
71       * Remembers the namespace declarations of the last unclosed start tag,
72       * so that we can fix up dummy prefixes in {@link Pcdata}.
73       */
74      private NamespaceDecl activeNamespaces;
75  
76  
77      Document(XmlSerializer out) {
78          this.out = out;
79          for( DatatypeWriter dw : DatatypeWriter.BUILTIN )
80              datatypeWriters.put(dw.getType(),dw);
81      }
82  
83      void flush() {
84          out.flush();
85      }
86  
87      void setFirstContent(Content c) {
88          assert current==null;
89          current = new StartDocument();
90          current.setNext(this,c);
91      }
92  
93      /**
94       * Defines additional user object -> string conversion logic.
95       *
96       * <p>
97       * Applications can add their own {@link DatatypeWriter} so that
98       * application-specific objects can be turned into {@link String}
99       * for output.
100      *
101      * @param dw
102      *      The {@link DatatypeWriter} to be added. Must not be null.
103      */
104     public void addDatatypeWriter( DatatypeWriter<?> dw ) {
105         datatypeWriters.put(dw.getType(),dw);
106     }
107 
108     /**
109      * Performs the output as much as possible
110      */
111     void run() {
112         while(true) {
113             Content next = current.getNext();
114             if(next==null || !next.isReadyToCommit())
115                 return;
116             next.accept(visitor);
117             next.written();
118             current = next;
119         }
120     }
121 
122     /**
123      * Appends the given object to the end of the given buffer.
124      *
125      * @param nsResolver
126      *      use
127      */
128     void writeValue( Object obj, NamespaceResolver nsResolver, StringBuilder buf ) {
129         if(obj==null)
130             throw new IllegalArgumentException("argument contains null");
131 
132         if(obj instanceof Object[]) {
133             for( Object o : (Object[])obj )
134                 writeValue(o,nsResolver,buf);
135             return;
136         }
137         if(obj instanceof Iterable) {
138             for( Object o : (Iterable<?>)obj )
139                 writeValue(o,nsResolver,buf);
140             return;
141         }
142 
143         if(buf.length()>0)
144             buf.append(' ');
145 
146         Class c = obj.getClass();
147         while(c!=null) {
148             DatatypeWriter dw = datatypeWriters.get(c);
149             if(dw!=null) {
150                 dw.print(obj,nsResolver,buf);
151                 return;
152             }
153             c = c.getSuperclass();
154         }
155 
156         // if nothing applies, just use toString
157         buf.append(obj);
158     }
159 
160     // I wanted to hide those write method from users
161     private final ContentVisitor visitor = new ContentVisitor() {
162         public void onStartDocument() {
163             // the startDocument token is used as the sentry, so this method shall never
164             // be called.
165             // out.startDocument() is invoked when we write the start tag of the root element.
166             throw new IllegalStateException();
167         }
168 
169         public void onEndDocument() {
170             out.endDocument();
171         }
172 
173         public void onEndTag() {
174             out.endTag();
175             inscopeNamespace.popContext();
176             activeNamespaces = null;
177         }
178 
179         public void onPcdata(StringBuilder buffer) {
180             if(activeNamespaces!=null)
181                 buffer = fixPrefix(buffer);
182             out.text(buffer);
183         }
184 
185         public void onCdata(StringBuilder buffer) {
186             if(activeNamespaces!=null)
187                 buffer = fixPrefix(buffer);
188             out.cdata(buffer);
189         }
190 
191         public void onComment(StringBuilder buffer) {
192             if(activeNamespaces!=null)
193                 buffer = fixPrefix(buffer);
194             out.comment(buffer);
195         }
196 
197         public void onStartTag(String nsUri, String localName, Attribute attributes, NamespaceDecl namespaces) {
198             assert nsUri!=null;
199             assert localName!=null;
200 
201             activeNamespaces = namespaces;
202 
203             if(!started) {
204                 started = true;
205                 out.startDocument();
206             }
207 
208             inscopeNamespace.pushContext();
209 
210             // declare the explicitly bound namespaces
211             for( NamespaceDecl ns=namespaces; ns!=null; ns=ns.next ) {
212                 ns.declared = false;    // reset this flag
213 
214                 if(ns.prefix!=null) {
215                     String uri = inscopeNamespace.getURI(ns.prefix);
216                     if(uri!=null && uri.equals(ns.uri))
217                         ; // already declared
218                     else {
219                         // declare this new binding
220                         inscopeNamespace.declarePrefix(ns.prefix,ns.uri);
221                         ns.declared = true;
222                     }
223                 }
224             }
225 
226             // then use in-scope namespace to assign prefixes to others
227             for( NamespaceDecl ns=namespaces; ns!=null; ns=ns.next ) {
228                 if(ns.prefix==null) {
229                     if(inscopeNamespace.getURI("").equals(ns.uri))
230                         ns.prefix="";
231                     else {
232                         String p = inscopeNamespace.getPrefix(ns.uri);
233                         if(p==null) {
234                             // assign a new one
235                             while(inscopeNamespace.getURI(p=newPrefix())!=null)
236                                 ;
237                             ns.declared = true;
238                             inscopeNamespace.declarePrefix(p,ns.uri);
239                         }
240                         ns.prefix = p;
241                     }
242                 }
243             }
244 
245             // the first namespace decl must be the one for the element
246             assert namespaces.uri.equals(nsUri);
247             assert namespaces.prefix!=null : "a prefix must have been all allocated";
248             out.beginStartTag(nsUri,localName,namespaces.prefix);
249 
250             // declare namespaces
251             for( NamespaceDecl ns=namespaces; ns!=null; ns=ns.next ) {
252                 if(ns.declared)
253                     out.writeXmlns( ns.prefix, ns.uri );
254             }
255 
256             // writeBody attributes
257             for( Attribute a=attributes; a!=null; a=a.next) {
258                 String prefix;
259                 if(a.nsUri.length()==0) prefix="";
260                 else                    prefix=inscopeNamespace.getPrefix(a.nsUri);
261                 out.writeAttribute( a.nsUri, a.localName, prefix, fixPrefix(a.value) );
262             }
263 
264             out.endStartTag(nsUri,localName,namespaces.prefix);
265         }
266     };
267 
268     /**
269      * Used by {@link #newPrefix()}.
270      */
271     private final StringBuilder prefixSeed = new StringBuilder("ns");
272 
273     private int prefixIota = 0;
274 
275     /**
276      * Allocates a new unique prefix.
277      */
278     private String newPrefix() {
279         prefixSeed.setLength(2);
280         prefixSeed.append(++prefixIota);
281         return prefixSeed.toString();
282     }
283 
284     /**
285      * Replaces dummy prefixes in the value to the real ones
286      * by using {@link #activeNamespaces}.
287      *
288      * @return
289      *      the buffer passed as the <tt>buf</tt> parameter.
290      */
291     private StringBuilder fixPrefix(StringBuilder buf) {
292         assert activeNamespaces!=null;
293 
294         int i;
295         int len=buf.length();
296         for(i=0;i<len;i++)
297             if( buf.charAt(i)==MAGIC )
298                 break;
299         // typically it doens't contain any prefix.
300         // just return the original buffer in that case
301         if(i==len)
302             return buf;
303 
304         while(i<len) {
305             char uriIdx = buf.charAt(i+1);
306             NamespaceDecl ns = activeNamespaces;
307             while(ns!=null && ns.uniqueId!=uriIdx)
308                 ns=ns.next;
309             if(ns==null)
310                 throw new IllegalStateException("Unexpected use of prefixes "+buf);
311 
312             int length = 2;
313             String prefix = ns.prefix;
314             if(prefix.length()==0) {
315                 if(buf.length()<=i+2 || buf.charAt(i+2)!=':')
316                     throw new IllegalStateException("Unexpected use of prefixes "+buf);
317                 length=3;
318             }
319 
320             buf.replace(i,i+length,prefix);
321             len += prefix.length()-length;
322 
323             while(i<len && buf.charAt(i)!=MAGIC)
324                 i++;
325         }
326 
327         return buf;
328     }
329 
330     /**
331      * The first char of the dummy prefix.
332      */
333     static final char MAGIC = '\u0000';
334 
335     char assignNewId() {
336         return (char)iota++;
337     }
338 }