1 /*
   2  * Copyright (c) 1997, 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.bind.v2.runtime.output;
  27 
  28 import java.io.IOException;
  29 import java.io.OutputStream;
  30 
  31 import java.io.StringWriter;
  32 import javax.xml.stream.XMLStreamException;
  33 
  34 import com.sun.xml.internal.bind.DatatypeConverterImpl;
  35 import com.sun.xml.internal.bind.marshaller.CharacterEscapeHandler;
  36 import com.sun.xml.internal.bind.v2.runtime.Name;
  37 import com.sun.xml.internal.bind.v2.runtime.XMLSerializer;
  38 import com.sun.xml.internal.bind.v2.runtime.MarshallerImpl;
  39 
  40 import org.xml.sax.SAXException;
  41 
  42 /**
  43  * {@link XmlOutput} implementation specialized for UTF-8.
  44  *
  45  * @author Kohsuke Kawaguchi
  46  * @author Paul Sandoz
  47  */
  48 public class UTF8XmlOutput extends XmlOutputAbstractImpl {
  49     protected final OutputStream out;
  50 
  51     /** prefixes encoded. */
  52     private Encoded[] prefixes = new Encoded[8];
  53 
  54     /**
  55      * Of the {@link #prefixes}, number of filled entries.
  56      * This is almost the same as {@link NamespaceContextImpl#count()},
  57      * except that it allows us to handle contextual in-scope namespace bindings correctly.
  58      */
  59     private int prefixCount;
  60 
  61     /** local names encoded in UTF-8. All entries are pre-filled. */
  62     private final Encoded[] localNames;
  63 
  64     /** Temporary buffer used to encode text. */
  65     /*
  66      * TODO
  67      * The textBuffer could write directly to the _octetBuffer
  68      * when encoding a string if Encoder is modified.
  69      * This will avoid an additional memory copy.
  70      */
  71     private final Encoded textBuffer = new Encoded();
  72 
  73     /** Buffer of octets for writing. */
  74     // TODO: Obtain buffer size from property on the JAXB context
  75     protected final byte[] octetBuffer = new byte[1024];
  76 
  77     /** Index in buffer to write to. */
  78     protected int octetBufferIndex;
  79 
  80     /**
  81      * Set to true to indicate that we need to write {@code '>'}
  82      * to close a start tag. Deferring the write of this char
  83      * allows us to write {@code "/>"} for empty elements.
  84      */
  85     protected boolean closeStartTagPending = false;
  86 
  87     /**
  88      * @see MarshallerImpl#header
  89      */
  90     private String header;
  91 
  92     private CharacterEscapeHandler escapeHandler = null;
  93 
  94     /**
  95      *
  96      * @param localNames
  97      *      local names encoded in UTF-8.
  98      */
  99     public UTF8XmlOutput(OutputStream out, Encoded[] localNames, CharacterEscapeHandler escapeHandler) {
 100         this.out = out;
 101         this.localNames = localNames;
 102         for( int i=0; i<prefixes.length; i++ )
 103             prefixes[i] = new Encoded();
 104         this.escapeHandler = escapeHandler;
 105     }
 106 
 107     public void setHeader(String header) {
 108         this.header = header;
 109     }
 110 
 111     @Override
 112     public void startDocument(XMLSerializer serializer, boolean fragment, int[] nsUriIndex2prefixIndex, NamespaceContextImpl nsContext) throws IOException, SAXException, XMLStreamException {
 113         super.startDocument(serializer, fragment,nsUriIndex2prefixIndex,nsContext);
 114 
 115         octetBufferIndex = 0;
 116         if(!fragment) {
 117             write(XML_DECL);
 118         }
 119         if(header!=null) {
 120             textBuffer.set(header);
 121             textBuffer.write(this);
 122         }
 123     }
 124 
 125     @Override
 126     public void endDocument(boolean fragment) throws IOException, SAXException, XMLStreamException {
 127         flushBuffer();
 128         super.endDocument(fragment);
 129     }
 130 
 131     /**
 132      * Writes {@code '>'} to close the start tag, if necessary.
 133      */
 134     protected final void closeStartTag() throws IOException {
 135         if(closeStartTagPending) {
 136             write('>');
 137             closeStartTagPending = false;
 138         }
 139     }
 140 
 141     public void beginStartTag(int prefix, String localName) throws IOException {
 142         closeStartTag();
 143         int base= pushNsDecls();
 144         write('<');
 145         writeName(prefix,localName);
 146         writeNsDecls(base);
 147     }
 148 
 149     @Override
 150     public void beginStartTag(Name name) throws IOException {
 151         closeStartTag();
 152         int base = pushNsDecls();
 153         write('<');
 154         writeName(name);
 155         writeNsDecls(base);
 156     }
 157 
 158     private int pushNsDecls() {
 159         int total = nsContext.count();
 160         NamespaceContextImpl.Element ns = nsContext.getCurrent();
 161 
 162         if(total > prefixes.length) {
 163             // reallocate
 164             int m = Math.max(total,prefixes.length*2);
 165             Encoded[] buf = new Encoded[m];
 166             System.arraycopy(prefixes,0,buf,0,prefixes.length);
 167             for( int i=prefixes.length; i<buf.length; i++ )
 168                 buf[i] = new Encoded();
 169             prefixes = buf;
 170         }
 171 
 172         int base = Math.min(prefixCount,ns.getBase());
 173         int size = nsContext.count();
 174         for( int i=base; i<size; i++ ) {
 175             String p = nsContext.getPrefix(i);
 176 
 177             Encoded e = prefixes[i];
 178 
 179             if(p.length()==0) {
 180                 e.buf = EMPTY_BYTE_ARRAY;
 181                 e.len = 0;
 182             } else {
 183                 e.set(p);
 184                 e.append(':');
 185             }
 186         }
 187         prefixCount = size;
 188         return base;
 189     }
 190 
 191     protected void writeNsDecls(int base) throws IOException {
 192         NamespaceContextImpl.Element ns = nsContext.getCurrent();
 193         int size = nsContext.count();
 194 
 195         for( int i=ns.getBase(); i<size; i++ )
 196             writeNsDecl(i);
 197     }
 198 
 199     /**
 200      * Writes a single namespace declaration for the specified prefix.
 201      */
 202     protected final void writeNsDecl(int prefixIndex) throws IOException {
 203         String p = nsContext.getPrefix(prefixIndex);
 204 
 205         if(p.length()==0) {
 206             if(nsContext.getCurrent().isRootElement()
 207             && nsContext.getNamespaceURI(prefixIndex).length()==0)
 208                 return;     // no point in declaring xmlns="" on the root element
 209             write(XMLNS_EQUALS);
 210         } else {
 211             Encoded e = prefixes[prefixIndex];
 212             write(XMLNS_COLON);
 213             write(e.buf,0,e.len-1); // skip the trailing ':'
 214             write(EQUALS);
 215         }
 216         doText(nsContext.getNamespaceURI(prefixIndex),true);
 217         write('\"');
 218     }
 219 
 220     private void writePrefix(int prefix) throws IOException {
 221         prefixes[prefix].write(this);
 222     }
 223 
 224     private void writeName(Name name) throws IOException {
 225         writePrefix(nsUriIndex2prefixIndex[name.nsUriIndex]);
 226         localNames[name.localNameIndex].write(this);
 227     }
 228 
 229     private void writeName(int prefix, String localName) throws IOException {
 230         writePrefix(prefix);
 231         textBuffer.set(localName);
 232         textBuffer.write(this);
 233     }
 234 
 235     @Override
 236     public void attribute(Name name, String value) throws IOException {
 237         write(' ');
 238         if(name.nsUriIndex==-1) {
 239             localNames[name.localNameIndex].write(this);
 240         } else
 241             writeName(name);
 242         write(EQUALS);
 243         doText(value,true);
 244         write('\"');
 245     }
 246 
 247     public void attribute(int prefix, String localName, String value) throws IOException {
 248         write(' ');
 249         if(prefix==-1) {
 250             textBuffer.set(localName);
 251             textBuffer.write(this);
 252         } else
 253             writeName(prefix,localName);
 254         write(EQUALS);
 255         doText(value,true);
 256         write('\"');
 257     }
 258 
 259     public void endStartTag() throws IOException {
 260         closeStartTagPending = true;
 261     }
 262 
 263     @Override
 264     public void endTag(Name name) throws IOException {
 265         if(closeStartTagPending) {
 266             write(EMPTY_TAG);
 267             closeStartTagPending = false;
 268         } else {
 269             write(CLOSE_TAG);
 270             writeName(name);
 271             write('>');
 272         }
 273     }
 274 
 275     public void endTag(int prefix, String localName) throws IOException {
 276         if(closeStartTagPending) {
 277             write(EMPTY_TAG);
 278             closeStartTagPending = false;
 279         } else {
 280             write(CLOSE_TAG);
 281             writeName(prefix,localName);
 282             write('>');
 283         }
 284     }
 285 
 286     public void text(String value, boolean needSP) throws IOException {
 287         closeStartTag();
 288         if(needSP)
 289             write(' ');
 290         doText(value,false);
 291     }
 292 
 293     public void text(Pcdata value, boolean needSP) throws IOException {
 294         closeStartTag();
 295         if(needSP)
 296             write(' ');
 297         value.writeTo(this);
 298     }
 299 
 300     private void doText(String value,boolean isAttribute) throws IOException {
 301         if (escapeHandler != null) {
 302             StringWriter sw = new StringWriter();
 303             escapeHandler.escape(value.toCharArray(), 0, value.length(), isAttribute, sw);
 304             textBuffer.set(sw.toString());
 305         } else {
 306             textBuffer.setEscape(value, isAttribute);
 307         }
 308         textBuffer.write(this);
 309     }
 310 
 311     public final void text(int value) throws IOException {
 312         closeStartTag();
 313         /*
 314          * TODO
 315          * Change to use the octet buffer directly
 316          */
 317 
 318         // max is -2147483648 and 11 digits
 319         boolean minus = (value<0);
 320         textBuffer.ensureSize(11);
 321         byte[] buf = textBuffer.buf;
 322         int idx = 11;
 323 
 324         do {
 325             int r = value%10;
 326             if(r<0) r = -r;
 327             buf[--idx] = (byte)('0'|r);    // really measn 0x30+r but 0<=r<10, so bit-OR would do.
 328             value /= 10;
 329         } while(value!=0);
 330 
 331         if(minus)   buf[--idx] = (byte)'-';
 332 
 333         write(buf,idx,11-idx);
 334     }
 335 
 336     /**
 337      * Writes the given byte[] as base64 encoded binary to the output.
 338      *
 339      * <p>
 340      * Being defined on this class allows this method to access the buffer directly,
 341      * which translates to a better performance.
 342      */
 343     public void text(byte[] data, int dataLen) throws IOException {
 344         closeStartTag();
 345 
 346         int start = 0;
 347 
 348         while(dataLen>0) {
 349             // how many bytes (in data) can we write without overflowing the buffer?
 350             int batchSize = Math.min(((octetBuffer.length-octetBufferIndex)/4)*3,dataLen);
 351 
 352             // write the batch
 353             octetBufferIndex = DatatypeConverterImpl._printBase64Binary(data,start,batchSize,octetBuffer,octetBufferIndex);
 354 
 355             if(batchSize<dataLen)
 356                 flushBuffer();
 357 
 358             start += batchSize;
 359             dataLen -= batchSize;
 360 
 361         }
 362     }
 363 
 364 //
 365 //
 366 // series of the write method that places bytes to the output
 367 // (by doing some buffering internal to this class)
 368 //
 369 
 370     /**
 371      * Writes one byte directly into the buffer.
 372      *
 373      * <p>
 374      * This method can be used somewhat like the {@code text} method,
 375      * but it doesn't perform character escaping.
 376      */
 377     public final void write(int i) throws IOException {
 378         if (octetBufferIndex < octetBuffer.length) {
 379             octetBuffer[octetBufferIndex++] = (byte)i;
 380         } else {
 381             out.write(octetBuffer);
 382             octetBufferIndex = 1;
 383             octetBuffer[0] = (byte)i;
 384         }
 385     }
 386 
 387     protected final void write(byte[] b) throws IOException {
 388         write(b, 0,  b.length);
 389     }
 390 
 391     protected final void write(byte[] b, int start, int length) throws IOException {
 392         if ((octetBufferIndex + length) < octetBuffer.length) {
 393             System.arraycopy(b, start, octetBuffer, octetBufferIndex, length);
 394             octetBufferIndex += length;
 395         } else {
 396             out.write(octetBuffer, 0, octetBufferIndex);
 397             out.write(b, start, length);
 398             octetBufferIndex = 0;
 399         }
 400     }
 401 
 402     protected final void flushBuffer() throws IOException {
 403         out.write(octetBuffer, 0, octetBufferIndex);
 404         octetBufferIndex = 0;
 405     }
 406 
 407     static byte[] toBytes(String s) {
 408         byte[] buf = new byte[s.length()];
 409         for( int i=s.length()-1; i>=0; i-- )
 410             buf[i] = (byte)s.charAt(i);
 411         return buf;
 412     }
 413 
 414     // per instance copy to prevent an attack where malicious OutputStream
 415     // rewrites the byte array.
 416     private final byte[] XMLNS_EQUALS = _XMLNS_EQUALS.clone();
 417     private final byte[] XMLNS_COLON = _XMLNS_COLON.clone();
 418     private final byte[] EQUALS = _EQUALS.clone();
 419     private final byte[] CLOSE_TAG = _CLOSE_TAG.clone();
 420     private final byte[] EMPTY_TAG = _EMPTY_TAG.clone();
 421     private final byte[] XML_DECL = _XML_DECL.clone();
 422 
 423     // masters
 424     private static final byte[] _XMLNS_EQUALS = toBytes(" xmlns=\"");
 425     private static final byte[] _XMLNS_COLON = toBytes(" xmlns:");
 426     private static final byte[] _EQUALS = toBytes("=\"");
 427     private static final byte[] _CLOSE_TAG = toBytes("</");
 428     private static final byte[] _EMPTY_TAG = toBytes("/>");
 429     private static final byte[] _XML_DECL = toBytes("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>");
 430 
 431     // no need to copy
 432     private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
 433 }