1 /*
   2  * Copyright (c) 2014, 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.messaging.saaj.util.stax;
  27 
  28 import java.util.Iterator;
  29 import java.util.Arrays;
  30 import java.util.List;
  31 import java.util.LinkedList;
  32 
  33 import javax.xml.namespace.NamespaceContext;
  34 import javax.xml.namespace.QName;
  35 import javax.xml.soap.SOAPElement;
  36 import javax.xml.soap.SOAPException;
  37 import javax.xml.soap.SOAPMessage;
  38 import javax.xml.stream.XMLStreamException;
  39 import javax.xml.stream.XMLStreamWriter;
  40 
  41 import org.w3c.dom.Comment;
  42 import org.w3c.dom.Node;
  43 
  44 /**
  45  * SaajStaxWriter builds a SAAJ SOAPMessage by using XMLStreamWriter interface.
  46  *
  47  * <p>
  48  * Defers creation of SOAPElement until all the aspects of the name of the element are known.
  49  * In some cases, the namespace uri is indicated only by the {@link #writeNamespace(String, String)} call.
  50  * After opening an element ({@code writeStartElement}, {@code writeEmptyElement} methods), all attributes
  51  * and namespace assignments are retained within {@link DeferredElement} object ({@code deferredElement} field).
  52  * As soon as any other method than {@code writeAttribute}, {@code writeNamespace}, {@code writeDefaultNamespace}
  53  * or {@code setNamespace} is called, the contents of {@code deferredElement} is transformed into new SOAPElement
  54  * (which is appropriately inserted into the SOAPMessage under construction).
  55  * This mechanism is necessary to fix JDK-8159058 issue.
  56  * </p>
  57  *
  58  * @author shih-chang.chen@oracle.com
  59  */
  60 public class SaajStaxWriter implements XMLStreamWriter {
  61 
  62     protected SOAPMessage soap;
  63     protected String envURI;
  64     protected SOAPElement currentElement;
  65     protected DeferredElement deferredElement;
  66 
  67     static final protected String Envelope = "Envelope";
  68     static final protected String Header = "Header";
  69     static final protected String Body = "Body";
  70     static final protected String xmlns = "xmlns";
  71 
  72     public SaajStaxWriter(final SOAPMessage msg, String uri) throws SOAPException {
  73         soap = msg;
  74         this.envURI = uri;
  75         this.deferredElement = new DeferredElement();
  76     }
  77 
  78     public SOAPMessage getSOAPMessage() {
  79         return soap;
  80     }
  81 
  82     protected SOAPElement getEnvelope() throws SOAPException {
  83         return soap.getSOAPPart().getEnvelope();
  84     }
  85 
  86     @Override
  87     public void writeStartElement(final String localName) throws XMLStreamException {
  88         currentElement = deferredElement.flushTo(currentElement);
  89         deferredElement.setLocalName(localName);
  90     }
  91 
  92     @Override
  93     public void writeStartElement(final String ns, final String ln) throws XMLStreamException {
  94         writeStartElement(null, ln, ns);
  95     }
  96 
  97     @Override
  98     public void writeStartElement(final String prefix, final String ln, final String ns) throws XMLStreamException {
  99         currentElement = deferredElement.flushTo(currentElement);
 100 
 101         if (envURI.equals(ns)) {
 102             try {
 103                 if (Envelope.equals(ln)) {
 104                     currentElement = getEnvelope();
 105                     fixPrefix(prefix);
 106                     return;
 107                 } else if (Header.equals(ln)) {
 108                     currentElement = soap.getSOAPHeader();
 109                     fixPrefix(prefix);
 110                     return;
 111                 } else if (Body.equals(ln)) {
 112                     currentElement = soap.getSOAPBody();
 113                     fixPrefix(prefix);
 114                     return;
 115                 }
 116             } catch (SOAPException e) {
 117                 throw new XMLStreamException(e);
 118             }
 119 
 120         }
 121 
 122         deferredElement.setLocalName(ln);
 123         deferredElement.setNamespaceUri(ns);
 124         deferredElement.setPrefix(prefix);
 125 
 126     }
 127 
 128     private void fixPrefix(final String prfx) throws XMLStreamException {
 129         fixPrefix(prfx, currentElement);
 130     }
 131 
 132     private void fixPrefix(final String prfx, SOAPElement element) throws XMLStreamException {
 133         String oldPrfx = element.getPrefix();
 134         if (prfx != null && !prfx.equals(oldPrfx)) {
 135             element.setPrefix(prfx);
 136         }
 137     }
 138 
 139     @Override
 140     public void writeEmptyElement(final String uri, final String ln) throws XMLStreamException {
 141         writeStartElement(null, ln, uri);
 142     }
 143 
 144     @Override
 145     public void writeEmptyElement(final String prefix, final String ln, final String uri) throws XMLStreamException {
 146         writeStartElement(prefix, ln, uri);
 147     }
 148 
 149     @Override
 150     public void writeEmptyElement(final String ln) throws XMLStreamException {
 151         writeStartElement(null, ln, null);
 152     }
 153 
 154     @Override
 155     public void writeEndElement() throws XMLStreamException {
 156         currentElement = deferredElement.flushTo(currentElement);
 157         if (currentElement != null) currentElement = currentElement.getParentElement();
 158     }
 159 
 160     @Override
 161     public void writeEndDocument() throws XMLStreamException {
 162         currentElement = deferredElement.flushTo(currentElement);
 163     }
 164 
 165     @Override
 166     public void close() throws XMLStreamException {
 167     }
 168 
 169     @Override
 170     public void flush() throws XMLStreamException {
 171     }
 172 
 173     @Override
 174     public void writeAttribute(final String ln, final String val) throws XMLStreamException {
 175         writeAttribute(null, null, ln, val);
 176     }
 177 
 178     @Override
 179     public void writeAttribute(final String prefix, final String ns, final String ln, final String value) throws XMLStreamException {
 180         if (ns == null && prefix == null && xmlns.equals(ln)) {
 181             writeNamespace("", value);
 182         } else {
 183             if (deferredElement.isInitialized()) {
 184                 deferredElement.addAttribute(prefix, ns, ln, value);
 185             } else {
 186                 addAttibuteToElement(currentElement, prefix, ns, ln, value);
 187             }
 188         }
 189     }
 190 
 191     @Override
 192     public void writeAttribute(final String ns, final String ln, final String val) throws XMLStreamException {
 193         writeAttribute(null, ns, ln, val);
 194     }
 195 
 196     @Override
 197     public void writeNamespace(String prefix, final String uri) throws XMLStreamException {
 198         // make prefix default if null or "xmlns" (according to javadoc)
 199         String thePrefix = prefix == null || "xmlns".equals(prefix) ? "" : prefix;
 200         if (deferredElement.isInitialized()) {
 201             deferredElement.addNamespaceDeclaration(thePrefix, uri);
 202         } else {
 203             try {
 204                 currentElement.addNamespaceDeclaration(thePrefix, uri);
 205             } catch (SOAPException e) {
 206                 throw new XMLStreamException(e);
 207             }
 208         }
 209     }
 210 
 211     @Override
 212     public void writeDefaultNamespace(final String uri) throws XMLStreamException {
 213         writeNamespace("", uri);
 214     }
 215 
 216     @Override
 217     public void writeComment(final String data) throws XMLStreamException {
 218         currentElement = deferredElement.flushTo(currentElement);
 219         Comment c = soap.getSOAPPart().createComment(data);
 220         currentElement.appendChild(c);
 221     }
 222 
 223     @Override
 224     public void writeProcessingInstruction(final String target) throws XMLStreamException {
 225         currentElement = deferredElement.flushTo(currentElement);
 226         Node n = soap.getSOAPPart().createProcessingInstruction(target, "");
 227         currentElement.appendChild(n);
 228     }
 229 
 230     @Override
 231     public void writeProcessingInstruction(final String target, final String data) throws XMLStreamException {
 232         currentElement = deferredElement.flushTo(currentElement);
 233         Node n = soap.getSOAPPart().createProcessingInstruction(target, data);
 234         currentElement.appendChild(n);
 235     }
 236 
 237     @Override
 238     public void writeCData(final String data) throws XMLStreamException {
 239         currentElement = deferredElement.flushTo(currentElement);
 240         Node n = soap.getSOAPPart().createCDATASection(data);
 241         currentElement.appendChild(n);
 242     }
 243 
 244     @Override
 245     public void writeDTD(final String dtd) throws XMLStreamException {
 246         currentElement = deferredElement.flushTo(currentElement);
 247     }
 248 
 249     @Override
 250     public void writeEntityRef(final String name) throws XMLStreamException {
 251         currentElement = deferredElement.flushTo(currentElement);
 252         Node n = soap.getSOAPPart().createEntityReference(name);
 253         currentElement.appendChild(n);
 254     }
 255 
 256     @Override
 257     public void writeStartDocument() throws XMLStreamException {
 258     }
 259 
 260     @Override
 261     public void writeStartDocument(final String version) throws XMLStreamException {
 262         if (version != null) soap.getSOAPPart().setXmlVersion(version);
 263     }
 264 
 265     @Override
 266     public void writeStartDocument(final String encoding, final String version) throws XMLStreamException {
 267         if (version != null) soap.getSOAPPart().setXmlVersion(version);
 268         if (encoding != null) {
 269             try {
 270                 soap.setProperty(SOAPMessage.CHARACTER_SET_ENCODING, encoding);
 271             } catch (SOAPException e) {
 272                 throw new XMLStreamException(e);
 273             }
 274         }
 275     }
 276 
 277     @Override
 278     public void writeCharacters(final String text) throws XMLStreamException {
 279         currentElement = deferredElement.flushTo(currentElement);
 280         try {
 281             currentElement.addTextNode(text);
 282         } catch (SOAPException e) {
 283             throw new XMLStreamException(e);
 284         }
 285     }
 286 
 287     @Override
 288     public void writeCharacters(final char[] text, final int start, final int len) throws XMLStreamException {
 289         currentElement = deferredElement.flushTo(currentElement);
 290         char[] chr = (start == 0 && len == text.length) ? text : Arrays.copyOfRange(text, start, start + len);
 291         try {
 292             currentElement.addTextNode(new String(chr));
 293         } catch (SOAPException e) {
 294             throw new XMLStreamException(e);
 295         }
 296     }
 297 
 298     @Override
 299     public String getPrefix(final String uri) throws XMLStreamException {
 300         return currentElement.lookupPrefix(uri);
 301     }
 302 
 303     @Override
 304     public void setPrefix(final String prefix, final String uri) throws XMLStreamException {
 305         // TODO: this in fact is not what would be expected from XMLStreamWriter
 306         //       (e.g. XMLStreamWriter for writing to output stream does not write anything as result of
 307         //        this method, it just rememebers that given prefix is associated with the given uri
 308         //        for the scope; to actually declare the prefix assignment in the resulting XML, one
 309         //        needs to call writeNamespace(...) method
 310         // Kept for backwards compatibility reasons - this might be worth of further investigation.
 311         if (deferredElement.isInitialized()) {
 312             deferredElement.addNamespaceDeclaration(prefix, uri);
 313         } else {
 314             throw new XMLStreamException("Namespace not associated with any element");
 315         }
 316     }
 317 
 318     @Override
 319     public void setDefaultNamespace(final String uri) throws XMLStreamException {
 320         setPrefix("", uri);
 321     }
 322 
 323     @Override
 324     public void setNamespaceContext(final NamespaceContext context)throws XMLStreamException {
 325         throw new UnsupportedOperationException();
 326     }
 327 
 328     @Override
 329     public Object getProperty(final String name) throws IllegalArgumentException {
 330         //TODO the following line is to make eclipselink happy ... they are aware of this problem -
 331         if (javax.xml.stream.XMLOutputFactory.IS_REPAIRING_NAMESPACES.equals(name)) return Boolean.FALSE;
 332         return null;
 333     }
 334 
 335     @Override
 336     public NamespaceContext getNamespaceContext() {
 337         return new NamespaceContext() {
 338             @Override
 339             public String getNamespaceURI(final String prefix) {
 340                 return currentElement.getNamespaceURI(prefix);
 341             }
 342             @Override
 343             public String getPrefix(final String namespaceURI) {
 344                 return currentElement.lookupPrefix(namespaceURI);
 345             }
 346             @Override
 347             public Iterator getPrefixes(final String namespaceURI) {
 348                 return new Iterator<String>() {
 349                     String prefix = getPrefix(namespaceURI);
 350                     @Override
 351                     public boolean hasNext() {
 352                         return (prefix != null);
 353                     }
 354                     @Override
 355                     public String next() {
 356                         if (!hasNext()) throw new java.util.NoSuchElementException();
 357                         String next = prefix;
 358                         prefix = null;
 359                         return next;
 360                     }
 361                     @Override
 362                     public void remove() {}
 363                 };
 364             }
 365         };
 366     }
 367 
 368     static void addAttibuteToElement(SOAPElement element, String prefix, String ns, String ln, String value)
 369             throws XMLStreamException {
 370         try {
 371             if (ns == null) {
 372                 element.setAttributeNS("", ln, value);
 373             } else {
 374                 QName name = prefix == null ? new QName(ns, ln) : new QName(ns, ln, prefix);
 375                 element.addAttribute(name, value);
 376             }
 377         } catch (SOAPException e) {
 378             throw new XMLStreamException(e);
 379         }
 380     }
 381 
 382     /**
 383      * Holds details of element that needs to be deferred in order to manage namespace assignments correctly.
 384      *
 385      * <p>
 386      * An instance of can be set with all the aspects of the element name (local name, prefix, namespace uri).
 387      * Attributes and namespace declarations (special case of attribute) can be added.
 388      * Namespace declarations are handled so that the element namespace is updated if it is implied by the namespace
 389      * declaration and the namespace was not set to non-{@code null} value previously.
 390      * </p>
 391      *
 392      * <p>
 393      * The state of this object can be {@link #flushTo(SOAPElement) flushed} to SOAPElement - new SOAPElement will
 394      * be added a child element; the new element will have exactly the shape as represented by the state of this
 395      * object. Note that the {@link #flushTo(SOAPElement)} method does nothing
 396      * (and returns the argument immediately) if the state of this object is not initialized
 397      * (i.e. local name is null).
 398      * </p>
 399      *
 400      * @author ondrej.cerny@oracle.com
 401      */
 402     static class DeferredElement {
 403         private String prefix;
 404         private String localName;
 405         private String namespaceUri;
 406         private final List<NamespaceDeclaration> namespaceDeclarations;
 407         private final List<AttributeDeclaration> attributeDeclarations;
 408 
 409         DeferredElement() {
 410             this.namespaceDeclarations = new LinkedList<NamespaceDeclaration>();
 411             this.attributeDeclarations = new LinkedList<AttributeDeclaration>();
 412             reset();
 413         }
 414 
 415 
 416         /**
 417          * Set prefix of the element.
 418          * @param prefix namespace prefix
 419          */
 420         public void setPrefix(final String prefix) {
 421             this.prefix = prefix;
 422         }
 423 
 424         /**
 425          * Set local name of the element.
 426          *
 427          * <p>
 428          *     This method initializes the element.
 429          * </p>
 430          *
 431          * @param localName local name {@code not null}
 432          */
 433         public void setLocalName(final String localName) {
 434             if (localName == null) {
 435                 throw new IllegalArgumentException("localName can not be null");
 436             }
 437             this.localName = localName;
 438         }
 439 
 440         /**
 441          * Set namespace uri.
 442          *
 443          * @param namespaceUri namespace uri
 444          */
 445         public void setNamespaceUri(final String namespaceUri) {
 446             this.namespaceUri = namespaceUri;
 447         }
 448 
 449         /**
 450          * Adds namespace prefix assignment to the element.
 451          *
 452          * @param prefix prefix (not {@code null})
 453          * @param namespaceUri namespace uri
 454          */
 455         public void addNamespaceDeclaration(final String prefix, final String namespaceUri) {
 456             if (null == this.namespaceUri && null != namespaceUri && prefix.equals(emptyIfNull(this.prefix))) {
 457                 this.namespaceUri = namespaceUri;
 458             }
 459             this.namespaceDeclarations.add(new NamespaceDeclaration(prefix, namespaceUri));
 460         }
 461 
 462         /**
 463          * Adds attribute to the element.
 464          * @param prefix prefix
 465          * @param ns namespace
 466          * @param ln local name
 467          * @param value value
 468          */
 469         public void addAttribute(final String prefix, final String ns, final String ln, final String value) {
 470             if (ns == null && prefix == null && xmlns.equals(ln)) {
 471                 this.addNamespaceDeclaration(prefix, value);
 472             } else {
 473                 this.attributeDeclarations.add(new AttributeDeclaration(prefix, ns, ln, value));
 474             }
 475         }
 476 
 477         /**
 478          * Flushes state of this element to the {@code target} element.
 479          *
 480          * <p>
 481          * If this element is initialized then it is added with all the namespace declarations and attributes
 482          * to the {@code target} element as a child. The state of this element is reset to uninitialized.
 483          * The newly added element object is returned.
 484          * </p>
 485          * <p>
 486          * If this element is not initialized then the {@code target} is returned immediately, nothing else is done.
 487          * </p>
 488          *
 489          * @param target target element
 490          * @return {@code target} or new element
 491          * @throws XMLStreamException on error
 492          */
 493         public SOAPElement flushTo(final SOAPElement target) throws XMLStreamException {
 494             try {
 495                 if (this.localName != null) {
 496                     // add the element appropriately (based on namespace declaration)
 497                     final SOAPElement newElement;
 498                     if (this.namespaceUri == null) {
 499                         // add element with inherited scope
 500                         newElement = target.addChildElement(this.localName);
 501                     } else if (prefix == null) {
 502                         newElement = target.addChildElement(new QName(this.namespaceUri, this.localName));
 503                     } else {
 504                         newElement = target.addChildElement(this.localName, this.prefix, this.namespaceUri);
 505                     }
 506                     // add namespace declarations
 507                     for (NamespaceDeclaration namespace : this.namespaceDeclarations) {
 508                         target.addNamespaceDeclaration(namespace.prefix, namespace.namespaceUri);
 509                     }
 510                     // add attribute declarations
 511                     for (AttributeDeclaration attribute : this.attributeDeclarations) {
 512                         addAttibuteToElement(newElement,
 513                                 attribute.prefix, attribute.namespaceUri, attribute.localName, attribute.value);
 514                     }
 515                     // reset state
 516                     this.reset();
 517 
 518                     return newElement;
 519                 } else {
 520                     return target;
 521                 }
 522                 // else after reset state -> not initialized
 523             } catch (SOAPException e) {
 524                 throw new XMLStreamException(e);
 525             }
 526         }
 527 
 528         /**
 529          * Is the element initialized?
 530          * @return boolean indicating whether it was initialized after last flush
 531          */
 532         public boolean isInitialized() {
 533             return this.localName != null;
 534         }
 535 
 536         private void reset() {
 537             this.localName = null;
 538             this.prefix = null;
 539             this.namespaceUri = null;
 540             this.namespaceDeclarations.clear();
 541             this.attributeDeclarations.clear();
 542         }
 543 
 544         private static String emptyIfNull(String s) {
 545             return s == null ? "" : s;
 546         }
 547     }
 548 
 549     static class NamespaceDeclaration {
 550         final String prefix;
 551         final String namespaceUri;
 552 
 553         NamespaceDeclaration(String prefix, String namespaceUri) {
 554             this.prefix = prefix;
 555             this.namespaceUri = namespaceUri;
 556         }
 557     }
 558 
 559     static class AttributeDeclaration {
 560         final String prefix;
 561         final String namespaceUri;
 562         final String localName;
 563         final String value;
 564 
 565         AttributeDeclaration(String prefix, String namespaceUri, String localName, String value) {
 566             this.prefix = prefix;
 567             this.namespaceUri = namespaceUri;
 568             this.localName = localName;
 569             this.value = value;
 570         }
 571     }
 572 }