1 /*
   2  * Copyright (c) 1997, 2017, 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, escapeHandler), 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      * Returns escape handler provided with JAXB context parameters.
 369      *
 370      * @return escape handler
 371      */
 372     CharacterEscapeHandler getEscapeHandler() {
 373         return escapeHandler;
 374     }
 375 
 376     //
 377     //
 378     // create XMLWriter by specifing various type of output.
 379     //
 380     //
 381 
 382     protected CharacterEscapeHandler createEscapeHandler( String encoding ) {
 383         if( escapeHandler!=null )
 384             // user-specified one takes precedence.
 385             return escapeHandler;
 386 
 387         if( encoding.startsWith("UTF") )
 388             // no need for character reference. Use the handler
 389             // optimized for that pattern.
 390             return MinimumEscapeHandler.theInstance;
 391 
 392         // otherwise try to find one from the encoding
 393         try {
 394             // try new JDK1.4 NIO
 395             return new NioEscapeHandler( getJavaEncoding(encoding) );
 396         } catch( Throwable e ) {
 397             // if that fails, fall back to the dumb mode
 398             return DumbEscapeHandler.theInstance;
 399         }
 400     }
 401 
 402     public XmlOutput createWriter( Writer w, String encoding ) {
 403         // XMLWriter doesn't do buffering, so do it here if it looks like a good idea
 404         if(!(w instanceof BufferedWriter))
 405             w = new BufferedWriter(w);
 406 
 407         assert toBeFlushed==null;
 408         toBeFlushed = w;
 409 
 410         CharacterEscapeHandler ceh = createEscapeHandler(encoding);
 411         XMLWriter xw;
 412 
 413         if(isFormattedOutput()) {
 414             DataWriter d = new DataWriter(w,encoding,ceh);
 415             d.setIndentStep(indent);
 416             xw=d;
 417         } else
 418             xw = new XMLWriter(w,encoding,ceh);
 419 
 420         xw.setXmlDecl(!isFragment());
 421         xw.setHeader(header);
 422         return new SAXOutput(xw);   // TODO: don't we need a better writer?
 423     }
 424 
 425     public XmlOutput createWriter(Writer w) {
 426         return createWriter(w, getEncoding());
 427     }
 428 
 429     public XmlOutput createWriter( OutputStream os ) throws JAXBException {
 430         return createWriter(os, getEncoding());
 431     }
 432 
 433     public XmlOutput createWriter( OutputStream os, String encoding ) throws JAXBException {
 434         // UTF8XmlOutput does buffering on its own, and
 435         // otherwise createWriter(Writer) inserts a buffering,
 436         // so no point in doing a buffering here.
 437 
 438         if(encoding.equals("UTF-8")) {
 439             Encoded[] table = context.getUTF8NameTable();
 440             final UTF8XmlOutput out;
 441             if(isFormattedOutput())
 442                 out = new IndentingUTF8XmlOutput(os, indent, table, escapeHandler);
 443             else {
 444                 if(c14nSupport)
 445                     out = new C14nXmlOutput(os, table, context.c14nSupport, escapeHandler);
 446                 else
 447                     out = new UTF8XmlOutput(os, table, escapeHandler);
 448             }
 449             if(header!=null)
 450                 out.setHeader(header);
 451             return out;
 452         }
 453 
 454         try {
 455             return createWriter(
 456                 new OutputStreamWriter(os,getJavaEncoding(encoding)),
 457                 encoding );
 458         } catch( UnsupportedEncodingException e ) {
 459             throw new MarshalException(
 460                 Messages.UNSUPPORTED_ENCODING.format(encoding),
 461                 e );
 462         }
 463     }
 464 
 465 
 466     @Override
 467     public Object getProperty(String name) throws PropertyException {
 468         if( INDENT_STRING.equals(name) )
 469             return indent;
 470         if( ENCODING_HANDLER.equals(name) || ENCODING_HANDLER2.equals(name) )
 471             return escapeHandler;
 472         if( PREFIX_MAPPER.equals(name) )
 473             return prefixMapper;
 474         if( XMLDECLARATION.equals(name) )
 475             return !isFragment();
 476         if( XML_HEADERS.equals(name) )
 477             return header;
 478         if( C14N.equals(name) )
 479             return c14nSupport;
 480         if ( OBJECT_IDENTITY_CYCLE_DETECTION.equals(name))
 481                 return serializer.getObjectIdentityCycleDetection();
 482 
 483         return super.getProperty(name);
 484     }
 485 
 486     @Override
 487     public void setProperty(String name, Object value) throws PropertyException {
 488         if( INDENT_STRING.equals(name) ) {
 489             checkString(name, value);
 490             indent = (String)value;
 491             return;
 492         }
 493         if( ENCODING_HANDLER.equals(name) || ENCODING_HANDLER2.equals(name)) {
 494             if(!(value instanceof CharacterEscapeHandler))
 495                 throw new PropertyException(
 496                     Messages.MUST_BE_X.format(
 497                             name,
 498                             CharacterEscapeHandler.class.getName(),
 499                             value.getClass().getName() ) );
 500             escapeHandler = (CharacterEscapeHandler)value;
 501             return;
 502         }
 503         if( PREFIX_MAPPER.equals(name) ) {
 504             if(!(value instanceof NamespacePrefixMapper))
 505                 throw new PropertyException(
 506                     Messages.MUST_BE_X.format(
 507                             name,
 508                             NamespacePrefixMapper.class.getName(),
 509                             value.getClass().getName() ) );
 510             prefixMapper = (NamespacePrefixMapper)value;
 511             return;
 512         }
 513         if( XMLDECLARATION.equals(name) ) {
 514             checkBoolean(name, value);
 515             // com.sun.xml.internal.bind.xmlDeclaration is an alias for JAXB_FRAGMENT
 516             // setting it to false is treated the same as setting fragment to true.
 517             super.setProperty(JAXB_FRAGMENT, !(Boolean)value);
 518             return;
 519         }
 520         if( XML_HEADERS.equals(name) ) {
 521             checkString(name, value);
 522             header = (String)value;
 523             return;
 524         }
 525         if( C14N.equals(name) ) {
 526             checkBoolean(name,value);
 527             c14nSupport = (Boolean)value;
 528             return;
 529         }
 530         if (OBJECT_IDENTITY_CYCLE_DETECTION.equals(name)) {
 531                 checkBoolean(name,value);
 532             serializer.setObjectIdentityCycleDetection((Boolean)value);
 533             return;
 534         }
 535 
 536         super.setProperty(name, value);
 537     }
 538 
 539     /*
 540      * assert that the given object is a Boolean
 541      */
 542     private void checkBoolean( String name, Object value ) throws PropertyException {
 543         if(!(value instanceof Boolean))
 544             throw new PropertyException(
 545                 Messages.MUST_BE_X.format(
 546                         name,
 547                         Boolean.class.getName(),
 548                         value.getClass().getName() ) );
 549     }
 550 
 551     /*
 552      * assert that the given object is a String
 553      */
 554     private void checkString( String name, Object value ) throws PropertyException {
 555         if(!(value instanceof String))
 556             throw new PropertyException(
 557                 Messages.MUST_BE_X.format(
 558                         name,
 559                         String.class.getName(),
 560                         value.getClass().getName() ) );
 561     }
 562 
 563     @Override
 564     public <A extends XmlAdapter> void setAdapter(Class<A> type, A adapter) {
 565         if(type==null)
 566             throw new IllegalArgumentException();
 567         serializer.putAdapter(type,adapter);
 568     }
 569 
 570     @Override
 571     public <A extends XmlAdapter> A getAdapter(Class<A> type) {
 572         if(type==null)
 573             throw new IllegalArgumentException();
 574         if(serializer.containsAdapter(type))
 575             // so as not to create a new instance when this method is called
 576             return serializer.getAdapter(type);
 577         else
 578             return null;
 579     }
 580 
 581     @Override
 582     public void setAttachmentMarshaller(AttachmentMarshaller am) {
 583         serializer.attachmentMarshaller = am;
 584     }
 585 
 586     @Override
 587     public AttachmentMarshaller getAttachmentMarshaller() {
 588         return serializer.attachmentMarshaller;
 589     }
 590 
 591     @Override
 592     public Schema getSchema() {
 593         return schema;
 594     }
 595 
 596     @Override
 597     public void setSchema(Schema s) {
 598         this.schema = s;
 599     }
 600 
 601     /**
 602      * Default error handling behavior fot {@link Marshaller}.
 603      */
 604     public boolean handleEvent(ValidationEvent event) {
 605         // draconian by default
 606         return false;
 607     }
 608 
 609     @Override
 610     public Listener getListener() {
 611         return externalListener;
 612     }
 613 
 614     @Override
 615     public void setListener(Listener listener) {
 616         externalListener = listener;
 617     }
 618 
 619     // features supported
 620     protected static final String INDENT_STRING = "com.sun.xml.internal.bind.indentString";
 621     protected static final String PREFIX_MAPPER = "com.sun.xml.internal.bind.namespacePrefixMapper";
 622     protected static final String ENCODING_HANDLER = "com.sun.xml.internal.bind.characterEscapeHandler";
 623     protected static final String ENCODING_HANDLER2 = "com.sun.xml.internal.bind.marshaller.CharacterEscapeHandler";
 624     protected static final String XMLDECLARATION = "com.sun.xml.internal.bind.xmlDeclaration";
 625     protected static final String XML_HEADERS = "com.sun.xml.internal.bind.xmlHeaders";
 626     protected static final String C14N = JAXBRIContext.CANONICALIZATION_SUPPORT;
 627     protected static final String OBJECT_IDENTITY_CYCLE_DETECTION = "com.sun.xml.internal.bind.objectIdentitityCycleDetection";
 628 }