1 /*
   2  * Copyright (c) 1997, 2013, 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;
  27 
  28 import java.io.BufferedWriter;
  29 import java.io.Closeable;
  30 import java.io.FileOutputStream;
  31 import java.io.Flushable;
  32 import java.io.IOException;
  33 import java.io.OutputStream;
  34 import java.io.OutputStreamWriter;
  35 import java.io.UnsupportedEncodingException;
  36 import java.io.Writer;
  37 
  38 import java.net.URI;
  39 import javax.xml.bind.JAXBException;
  40 import javax.xml.bind.MarshalException;
  41 import javax.xml.bind.Marshaller;
  42 import javax.xml.bind.PropertyException;
  43 import javax.xml.bind.ValidationEvent;
  44 import javax.xml.bind.ValidationEventHandler;
  45 import javax.xml.bind.annotation.adapters.XmlAdapter;
  46 import javax.xml.bind.attachment.AttachmentMarshaller;
  47 import javax.xml.bind.helpers.AbstractMarshallerImpl;
  48 import javax.xml.stream.XMLEventWriter;
  49 import javax.xml.stream.XMLStreamException;
  50 import javax.xml.stream.XMLStreamWriter;
  51 import javax.xml.transform.Result;
  52 import javax.xml.transform.dom.DOMResult;
  53 import javax.xml.transform.sax.SAXResult;
  54 import javax.xml.transform.stream.StreamResult;
  55 import javax.xml.validation.Schema;
  56 import javax.xml.validation.ValidatorHandler;
  57 import javax.xml.namespace.NamespaceContext;
  58 
  59 import com.sun.xml.internal.bind.api.JAXBRIContext;
  60 import com.sun.xml.internal.bind.marshaller.CharacterEscapeHandler;
  61 import com.sun.xml.internal.bind.marshaller.DataWriter;
  62 import com.sun.xml.internal.bind.marshaller.DumbEscapeHandler;
  63 import com.sun.xml.internal.bind.marshaller.MinimumEscapeHandler;
  64 import com.sun.xml.internal.bind.marshaller.NamespacePrefixMapper;
  65 import com.sun.xml.internal.bind.marshaller.NioEscapeHandler;
  66 import com.sun.xml.internal.bind.marshaller.SAX2DOMEx;
  67 import com.sun.xml.internal.bind.marshaller.XMLWriter;
  68 import com.sun.xml.internal.bind.v2.runtime.output.C14nXmlOutput;
  69 import com.sun.xml.internal.bind.v2.runtime.output.Encoded;
  70 import com.sun.xml.internal.bind.v2.runtime.output.ForkXmlOutput;
  71 import com.sun.xml.internal.bind.v2.runtime.output.IndentingUTF8XmlOutput;
  72 import com.sun.xml.internal.bind.v2.runtime.output.NamespaceContextImpl;
  73 import com.sun.xml.internal.bind.v2.runtime.output.SAXOutput;
  74 import com.sun.xml.internal.bind.v2.runtime.output.UTF8XmlOutput;
  75 import com.sun.xml.internal.bind.v2.runtime.output.XMLEventWriterOutput;
  76 import com.sun.xml.internal.bind.v2.runtime.output.XMLStreamWriterOutput;
  77 import com.sun.xml.internal.bind.v2.runtime.output.XmlOutput;
  78 import com.sun.xml.internal.bind.v2.util.FatalAdapter;
  79 
  80 import java.net.URISyntaxException;
  81 import org.w3c.dom.Document;
  82 import org.w3c.dom.Node;
  83 import org.xml.sax.SAXException;
  84 import org.xml.sax.helpers.XMLFilterImpl;
  85 
  86 /**
  87  * Implementation of {@link Marshaller} interface for the JAXB RI.
  88  *
  89  * <p>
  90  * Eventually all the {@link #marshal} methods call into
  91  * the {@link #write} method.
  92  *
  93  * @author Kohsuke Kawaguchi
  94  * @author Vivek Pandey
  95  */
  96 public /*to make unit tests happy*/ final class MarshallerImpl extends AbstractMarshallerImpl implements ValidationEventHandler
  97 {
  98     /** Indentation string. Default is four whitespaces. */
  99     private String indent = "    ";
 100 
 101     /** Used to assign prefixes to namespace URIs. */
 102     private NamespacePrefixMapper prefixMapper = null;
 103 
 104     /** Object that handles character escaping. */
 105     private CharacterEscapeHandler escapeHandler = null;
 106 
 107     /** XML BLOB written after the XML declaration. */
 108     private String header=null;
 109 
 110     /** reference to the context that created this object */
 111     final JAXBContextImpl context;
 112 
 113     protected final XMLSerializer serializer;
 114 
 115     /**
 116      * Non-null if we do the marshal-time validation.
 117      */
 118     private Schema schema;
 119 
 120     /** Marshaller.Listener */
 121     private Listener externalListener = null;
 122 
 123     /** Configured for c14n? */
 124     private boolean c14nSupport;
 125 
 126     // while createing XmlOutput those values may be set.
 127     // if these are non-null they need to be cleaned up
 128     private Flushable toBeFlushed;
 129     private Closeable toBeClosed;
 130 
 131     /**
 132      * @param assoc
 133      *      non-null if the marshaller is working inside {@link BinderImpl}.
 134      */
 135     public MarshallerImpl( JAXBContextImpl c, AssociationMap assoc ) {
 136         context = c;
 137         serializer = new XMLSerializer(this);
 138         c14nSupport = context.c14nSupport;
 139 
 140         try {
 141             setEventHandler(this);
 142         } catch (JAXBException e) {
 143             throw new AssertionError(e);    // impossible
 144         }
 145     }
 146 
 147     public JAXBContextImpl getContext() {
 148         return context;
 149     }
 150 
 151     /**
 152      * Marshals to {@link OutputStream} with the given in-scope namespaces
 153      * taken into account.
 154      *
 155      * @since 2.1.5
 156      */
 157     public void marshal(Object obj, OutputStream out, NamespaceContext inscopeNamespace) throws JAXBException {
 158         write(obj, createWriter(out), new StAXPostInitAction(inscopeNamespace,serializer));
 159     }
 160 
 161     @Override
 162     public void marshal(Object obj, XMLStreamWriter writer) throws JAXBException {
 163         write(obj, XMLStreamWriterOutput.create(writer,context), new StAXPostInitAction(writer,serializer));
 164     }
 165 
 166     @Override
 167     public void marshal(Object obj, XMLEventWriter writer) throws JAXBException {
 168         write(obj, new XMLEventWriterOutput(writer), new StAXPostInitAction(writer,serializer));
 169     }
 170 
 171     public void marshal(Object obj, XmlOutput output) throws JAXBException {
 172         write(obj, output, null );
 173     }
 174 
 175     /**
 176      * Creates {@link XmlOutput} from the given {@link Result} object.
 177      */
 178     final XmlOutput createXmlOutput(Result result) throws JAXBException {
 179         if (result instanceof SAXResult)
 180             return new SAXOutput(((SAXResult) result).getHandler());
 181 
 182         if (result instanceof DOMResult) {
 183             final Node node = ((DOMResult) result).getNode();
 184 
 185             if (node == null) {
 186                 Document doc = JAXBContextImpl.createDom(getContext().disableSecurityProcessing);
 187                 ((DOMResult) result).setNode(doc);
 188                 return new SAXOutput(new SAX2DOMEx(doc));
 189             } else {
 190                 return new SAXOutput(new SAX2DOMEx(node));
 191             }
 192         }
 193         if (result instanceof StreamResult) {
 194             StreamResult sr = (StreamResult) result;
 195 
 196             if (sr.getWriter() != null)
 197                 return createWriter(sr.getWriter());
 198             else if (sr.getOutputStream() != null)
 199                 return createWriter(sr.getOutputStream());
 200             else if (sr.getSystemId() != null) {
 201                 String fileURL = sr.getSystemId();
 202 
 203                 try {
 204                     fileURL = new URI(fileURL).getPath();
 205                 } catch (URISyntaxException use) {
 206                     // otherwise assume that it's a file name
 207                 }
 208 
 209                 try {
 210                     FileOutputStream fos = new FileOutputStream(fileURL);
 211                     assert toBeClosed==null;
 212                     toBeClosed = fos;
 213                     return createWriter(fos);
 214                 } catch (IOException e) {
 215                     throw new MarshalException(e);
 216                 }
 217             }
 218         }
 219 
 220         // unsupported parameter type
 221         throw new MarshalException(Messages.UNSUPPORTED_RESULT.format());
 222     }
 223 
 224     /**
 225      * Creates an appropriate post-init action object.
 226      */
 227     final Runnable createPostInitAction(Result result) {
 228         if (result instanceof DOMResult) {
 229             Node node = ((DOMResult) result).getNode();
 230             return new DomPostInitAction(node,serializer);
 231         }
 232         return null;
 233     }
 234 
 235     public void marshal(Object target,Result result) throws JAXBException {
 236         write(target, createXmlOutput(result), createPostInitAction(result));
 237     }
 238 
 239 
 240     /**
 241      * Used by {@link BridgeImpl} to write an arbitrary object as a fragment.
 242      */
 243     protected final <T> void write(Name rootTagName, JaxBeanInfo<T> bi, T obj, XmlOutput out,Runnable postInitAction) throws JAXBException {
 244         try {
 245             try {
 246                 prewrite(out, true, postInitAction);
 247                 serializer.startElement(rootTagName,null);
 248                 if(bi.jaxbType==Void.class || bi.jaxbType==void.class) {
 249                     // special case for void
 250                     serializer.endNamespaceDecls(null);
 251                     serializer.endAttributes();
 252                 } else { // normal cases
 253                     if(obj==null)
 254                         serializer.writeXsiNilTrue();
 255                     else
 256                         serializer.childAsXsiType(obj,"root",bi, false);
 257                 }
 258                 serializer.endElement();
 259                 postwrite();
 260             } catch( SAXException e ) {
 261                 throw new MarshalException(e);
 262             } catch (IOException e) {
 263                 throw new MarshalException(e);
 264             } catch (XMLStreamException e) {
 265                 throw new MarshalException(e);
 266             } finally {
 267                 serializer.close();
 268             }
 269         } finally {
 270             cleanUp();
 271         }
 272     }
 273 
 274     /**
 275      * All the marshal method invocation eventually comes down to this call.
 276      */
 277     private void write(Object obj, XmlOutput out, Runnable postInitAction) throws JAXBException {
 278         try {
 279             if( obj == null )
 280                 throw new IllegalArgumentException(Messages.NOT_MARSHALLABLE.format());
 281 
 282             if( schema!=null ) {
 283                 // send the output to the validator as well
 284                 ValidatorHandler validator = schema.newValidatorHandler();
 285                 validator.setErrorHandler(new FatalAdapter(serializer));
 286                 // work around a bug in JAXP validator in Tiger
 287                 XMLFilterImpl f = new XMLFilterImpl() {
 288                     @Override
 289                     public void startPrefixMapping(String prefix, String uri) throws SAXException {
 290                         super.startPrefixMapping(prefix.intern(), uri.intern());
 291                     }
 292                 };
 293                 f.setContentHandler(validator);
 294                 out = new ForkXmlOutput( new SAXOutput(f) {
 295                     @Override
 296                     public void startDocument(XMLSerializer serializer, boolean fragment, int[] nsUriIndex2prefixIndex, NamespaceContextImpl nsContext) throws SAXException, IOException, XMLStreamException {
 297                         super.startDocument(serializer, false, nsUriIndex2prefixIndex, nsContext);
 298                     }
 299                     @Override
 300                     public void endDocument(boolean fragment) throws SAXException, IOException, XMLStreamException {
 301                         super.endDocument(false);
 302                     }
 303                 }, out );
 304             }
 305 
 306             try {
 307                 prewrite(out,isFragment(),postInitAction);
 308                 serializer.childAsRoot(obj);
 309                 postwrite();
 310             } catch( SAXException e ) {
 311                 throw new MarshalException(e);
 312             } catch (IOException e) {
 313                 throw new MarshalException(e);
 314             } catch (XMLStreamException e) {
 315                 throw new MarshalException(e);
 316             } finally {
 317                 serializer.close();
 318             }
 319         } finally {
 320             cleanUp();
 321         }
 322     }
 323 
 324     private void cleanUp() {
 325         if(toBeFlushed!=null)
 326             try {
 327                 toBeFlushed.flush();
 328             } catch (IOException e) {
 329                 // ignore
 330             }
 331         if(toBeClosed!=null)
 332             try {
 333                 toBeClosed.close();
 334             } catch (IOException e) {
 335                 // ignore
 336             }
 337         toBeFlushed = null;
 338         toBeClosed = null;
 339     }
 340 
 341     // common parts between two write methods.
 342 
 343     private void prewrite(XmlOutput out, boolean fragment, Runnable postInitAction) throws IOException, SAXException, XMLStreamException {
 344         serializer.startDocument(out,fragment,getSchemaLocation(),getNoNSSchemaLocation());
 345         if(postInitAction!=null)    postInitAction.run();
 346         if(prefixMapper!=null) {
 347             // be defensive as we work with the user's code
 348             String[] decls = prefixMapper.getContextualNamespaceDecls();
 349             if(decls!=null) { // defensive check
 350                 for( int i=0; i<decls.length; i+=2 ) {
 351                     String prefix = decls[i];
 352                     String nsUri = decls[i+1];
 353                     if(nsUri!=null && prefix!=null) // defensive check
 354                         serializer.addInscopeBinding(nsUri,prefix);
 355                 }
 356             }
 357         }
 358         serializer.setPrefixMapper(prefixMapper);
 359     }
 360 
 361     private void postwrite() throws IOException, SAXException, XMLStreamException {
 362         serializer.endDocument();
 363         serializer.reconcileID();   // extra check
 364     }
 365 
 366 
 367     //
 368     //
 369     // create XMLWriter by specifing various type of output.
 370     //
 371     //
 372 
 373     protected CharacterEscapeHandler createEscapeHandler( String encoding ) {
 374         if( escapeHandler!=null )
 375             // user-specified one takes precedence.
 376             return escapeHandler;
 377 
 378         if( encoding.startsWith("UTF") )
 379             // no need for character reference. Use the handler
 380             // optimized for that pattern.
 381             return MinimumEscapeHandler.theInstance;
 382 
 383         // otherwise try to find one from the encoding
 384         try {
 385             // try new JDK1.4 NIO
 386             return new NioEscapeHandler( getJavaEncoding(encoding) );
 387         } catch( Throwable e ) {
 388             // if that fails, fall back to the dumb mode
 389             return DumbEscapeHandler.theInstance;
 390         }
 391     }
 392 
 393     public XmlOutput createWriter( Writer w, String encoding ) {
 394         // XMLWriter doesn't do buffering, so do it here if it looks like a good idea
 395         if(!(w instanceof BufferedWriter))
 396             w = new BufferedWriter(w);
 397 
 398         assert toBeFlushed==null;
 399         toBeFlushed = w;
 400 
 401         CharacterEscapeHandler ceh = createEscapeHandler(encoding);
 402         XMLWriter xw;
 403 
 404         if(isFormattedOutput()) {
 405             DataWriter d = new DataWriter(w,encoding,ceh);
 406             d.setIndentStep(indent);
 407             xw=d;
 408         } else
 409             xw = new XMLWriter(w,encoding,ceh);
 410 
 411         xw.setXmlDecl(!isFragment());
 412         xw.setHeader(header);
 413         return new SAXOutput(xw);   // TODO: don't we need a better writer?
 414     }
 415 
 416     public XmlOutput createWriter(Writer w) {
 417         return createWriter(w, getEncoding());
 418     }
 419 
 420     public XmlOutput createWriter( OutputStream os ) throws JAXBException {
 421         return createWriter(os, getEncoding());
 422     }
 423 
 424     public XmlOutput createWriter( OutputStream os, String encoding ) throws JAXBException {
 425         // UTF8XmlOutput does buffering on its own, and
 426         // otherwise createWriter(Writer) inserts a buffering,
 427         // so no point in doing a buffering here.
 428 
 429         if(encoding.equals("UTF-8")) {
 430             Encoded[] table = context.getUTF8NameTable();
 431             final UTF8XmlOutput out;
 432             if(isFormattedOutput())
 433                 out = new IndentingUTF8XmlOutput(os, indent, table, escapeHandler);
 434             else {
 435                 if(c14nSupport)
 436                     out = new C14nXmlOutput(os, table, context.c14nSupport, escapeHandler);
 437                 else
 438                     out = new UTF8XmlOutput(os, table, escapeHandler);
 439             }
 440             if(header!=null)
 441                 out.setHeader(header);
 442             return out;
 443         }
 444 
 445         try {
 446             return createWriter(
 447                 new OutputStreamWriter(os,getJavaEncoding(encoding)),
 448                 encoding );
 449         } catch( UnsupportedEncodingException e ) {
 450             throw new MarshalException(
 451                 Messages.UNSUPPORTED_ENCODING.format(encoding),
 452                 e );
 453         }
 454     }
 455 
 456 
 457     @Override
 458     public Object getProperty(String name) throws PropertyException {
 459         if( INDENT_STRING.equals(name) )
 460             return indent;
 461         if( ENCODING_HANDLER.equals(name) || ENCODING_HANDLER2.equals(name) )
 462             return escapeHandler;
 463         if( PREFIX_MAPPER.equals(name) )
 464             return prefixMapper;
 465         if( XMLDECLARATION.equals(name) )
 466             return !isFragment();
 467         if( XML_HEADERS.equals(name) )
 468             return header;
 469         if( C14N.equals(name) )
 470             return c14nSupport;
 471         if ( OBJECT_IDENTITY_CYCLE_DETECTION.equals(name))
 472                 return serializer.getObjectIdentityCycleDetection();
 473 
 474         return super.getProperty(name);
 475     }
 476 
 477     @Override
 478     public void setProperty(String name, Object value) throws PropertyException {
 479         if( INDENT_STRING.equals(name) ) {
 480             checkString(name, value);
 481             indent = (String)value;
 482             return;
 483         }
 484         if( ENCODING_HANDLER.equals(name) || ENCODING_HANDLER2.equals(name)) {
 485             if(!(value instanceof CharacterEscapeHandler))
 486                 throw new PropertyException(
 487                     Messages.MUST_BE_X.format(
 488                             name,
 489                             CharacterEscapeHandler.class.getName(),
 490                             value.getClass().getName() ) );
 491             escapeHandler = (CharacterEscapeHandler)value;
 492             return;
 493         }
 494         if( PREFIX_MAPPER.equals(name) ) {
 495             if(!(value instanceof NamespacePrefixMapper))
 496                 throw new PropertyException(
 497                     Messages.MUST_BE_X.format(
 498                             name,
 499                             NamespacePrefixMapper.class.getName(),
 500                             value.getClass().getName() ) );
 501             prefixMapper = (NamespacePrefixMapper)value;
 502             return;
 503         }
 504         if( XMLDECLARATION.equals(name) ) {
 505             checkBoolean(name, value);
 506             // com.sun.xml.internal.bind.xmlDeclaration is an alias for JAXB_FRAGMENT
 507             // setting it to false is treated the same as setting fragment to true.
 508             super.setProperty(JAXB_FRAGMENT, !(Boolean)value);
 509             return;
 510         }
 511         if( XML_HEADERS.equals(name) ) {
 512             checkString(name, value);
 513             header = (String)value;
 514             return;
 515         }
 516         if( C14N.equals(name) ) {
 517             checkBoolean(name,value);
 518             c14nSupport = (Boolean)value;
 519             return;
 520         }
 521         if (OBJECT_IDENTITY_CYCLE_DETECTION.equals(name)) {
 522                 checkBoolean(name,value);
 523             serializer.setObjectIdentityCycleDetection((Boolean)value);
 524             return;
 525         }
 526 
 527         super.setProperty(name, value);
 528     }
 529 
 530     /*
 531      * assert that the given object is a Boolean
 532      */
 533     private void checkBoolean( String name, Object value ) throws PropertyException {
 534         if(!(value instanceof Boolean))
 535             throw new PropertyException(
 536                 Messages.MUST_BE_X.format(
 537                         name,
 538                         Boolean.class.getName(),
 539                         value.getClass().getName() ) );
 540     }
 541 
 542     /*
 543      * assert that the given object is a String
 544      */
 545     private void checkString( String name, Object value ) throws PropertyException {
 546         if(!(value instanceof String))
 547             throw new PropertyException(
 548                 Messages.MUST_BE_X.format(
 549                         name,
 550                         String.class.getName(),
 551                         value.getClass().getName() ) );
 552     }
 553 
 554     @Override
 555     public <A extends XmlAdapter> void setAdapter(Class<A> type, A adapter) {
 556         if(type==null)
 557             throw new IllegalArgumentException();
 558         serializer.putAdapter(type,adapter);
 559     }
 560 
 561     @Override
 562     public <A extends XmlAdapter> A getAdapter(Class<A> type) {
 563         if(type==null)
 564             throw new IllegalArgumentException();
 565         if(serializer.containsAdapter(type))
 566             // so as not to create a new instance when this method is called
 567             return serializer.getAdapter(type);
 568         else
 569             return null;
 570     }
 571 
 572     @Override
 573     public void setAttachmentMarshaller(AttachmentMarshaller am) {
 574         serializer.attachmentMarshaller = am;
 575     }
 576 
 577     @Override
 578     public AttachmentMarshaller getAttachmentMarshaller() {
 579         return serializer.attachmentMarshaller;
 580     }
 581 
 582     @Override
 583     public Schema getSchema() {
 584         return schema;
 585     }
 586 
 587     @Override
 588     public void setSchema(Schema s) {
 589         this.schema = s;
 590     }
 591 
 592     /**
 593      * Default error handling behavior fot {@link Marshaller}.
 594      */
 595     public boolean handleEvent(ValidationEvent event) {
 596         // draconian by default
 597         return false;
 598     }
 599 
 600     @Override
 601     public Listener getListener() {
 602         return externalListener;
 603     }
 604 
 605     @Override
 606     public void setListener(Listener listener) {
 607         externalListener = listener;
 608     }
 609 
 610     // features supported
 611     protected static final String INDENT_STRING = "com.sun.xml.internal.bind.indentString";
 612     protected static final String PREFIX_MAPPER = "com.sun.xml.internal.bind.namespacePrefixMapper";
 613     protected static final String ENCODING_HANDLER = "com.sun.xml.internal.bind.characterEscapeHandler";
 614     protected static final String ENCODING_HANDLER2 = "com.sun.xml.internal.bind.marshaller.CharacterEscapeHandler";
 615     protected static final String XMLDECLARATION = "com.sun.xml.internal.bind.xmlDeclaration";
 616     protected static final String XML_HEADERS = "com.sun.xml.internal.bind.xmlHeaders";
 617     protected static final String C14N = JAXBRIContext.CANONICALIZATION_SUPPORT;
 618     protected static final String OBJECT_IDENTITY_CYCLE_DETECTION = "com.sun.xml.internal.bind.objectIdentitityCycleDetection";
 619 }