1 /*
   2  * Copyright (c) 2005, 2015, 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 {@code ->} 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 {@code buf} 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 }