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