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             public String getNamespaceURI(final String prefix) {
 339                 return currentElement.getNamespaceURI(prefix);
 340             }
 341             public String getPrefix(final String namespaceURI) {
 342                 return currentElement.lookupPrefix(namespaceURI);
 343             }
 344             public Iterator getPrefixes(final String namespaceURI) {
 345                 return new Iterator<String>() {
 346                     String prefix = getPrefix(namespaceURI);
 347                     public boolean hasNext() {
 348                         return (prefix != null);
 349                     }
 350                     public String next() {
 351                         if (!hasNext()) throw new java.util.NoSuchElementException();
 352                         String next = prefix;
 353                         prefix = null;
 354                         return next;
 355                     }
 356                     public void remove() {}
 357                 };
 358             }
 359         };
 360     }
 361 
 362     static void addAttibuteToElement(SOAPElement element, String prefix, String ns, String ln, String value)
 363             throws XMLStreamException {
 364         try {
 365             if (ns == null) {
 366                 element.setAttributeNS("", ln, value);
 367             } else {
 368                 QName name = prefix == null ? new QName(ns, ln) : new QName(ns, ln, prefix);
 369                 element.addAttribute(name, value);
 370             }
 371         } catch (SOAPException e) {
 372             throw new XMLStreamException(e);
 373         }
 374     }
 375 
 376     /**
 377      * Holds details of element that needs to be deferred in order to manage namespace assignments correctly.
 378      *
 379      * <p>
 380      * An instance of can be set with all the aspects of the element name (local name, prefix, namespace uri).
 381      * Attributes and namespace declarations (special case of attribute) can be added.
 382      * Namespace declarations are handled so that the element namespace is updated if it is implied by the namespace
 383      * declaration and the namespace was not set to non-{@code null} value previously.
 384      * </p>
 385      *
 386      * <p>
 387      * The state of this object can be {@link #flushTo(SOAPElement) flushed} to SOAPElement - new SOAPElement will
 388      * be added a child element; the new element will have exactly the shape as represented by the state of this
 389      * object. Note that the {@link #flushTo(SOAPElement)} method does nothing
 390      * (and returns the argument immediately) if the state of this object is not initialized
 391      * (i.e. local name is null).
 392      * </p>
 393      *
 394      * @author ondrej.cerny@oracle.com
 395      */
 396     static class DeferredElement {
 397         private String prefix;
 398         private String localName;
 399         private String namespaceUri;
 400         private final List<NamespaceDeclaration> namespaceDeclarations;
 401         private final List<AttributeDeclaration> attributeDeclarations;
 402 
 403         DeferredElement() {
 404             this.namespaceDeclarations = new LinkedList<NamespaceDeclaration>();
 405             this.attributeDeclarations = new LinkedList<AttributeDeclaration>();
 406             reset();
 407         }
 408 
 409 
 410         /**
 411          * Set prefix of the element.
 412          * @param prefix namespace prefix
 413          */
 414         public void setPrefix(final String prefix) {
 415             this.prefix = prefix;
 416         }
 417 
 418         /**
 419          * Set local name of the element.
 420          *
 421          * <p>
 422          *     This method initializes the element.
 423          * </p>
 424          *
 425          * @param localName local name {@code not null}
 426          */
 427         public void setLocalName(final String localName) {
 428             if (localName == null) {
 429                 throw new IllegalArgumentException("localName can not be null");
 430             }
 431             this.localName = localName;
 432         }
 433 
 434         /**
 435          * Set namespace uri.
 436          *
 437          * @param namespaceUri namespace uri
 438          */
 439         public void setNamespaceUri(final String namespaceUri) {
 440             this.namespaceUri = namespaceUri;
 441         }
 442 
 443         /**
 444          * Adds namespace prefix assignment to the element.
 445          *
 446          * @param prefix prefix (not {@code null})
 447          * @param namespaceUri namespace uri
 448          */
 449         public void addNamespaceDeclaration(final String prefix, final String namespaceUri) {
 450             if (null == this.namespaceUri && null != namespaceUri && prefix.equals(emptyIfNull(this.prefix))) {
 451                 this.namespaceUri = namespaceUri;
 452             }
 453             this.namespaceDeclarations.add(new NamespaceDeclaration(prefix, namespaceUri));
 454         }
 455 
 456         /**
 457          * Adds attribute to the element.
 458          * @param prefix prefix
 459          * @param ns namespace
 460          * @param ln local name
 461          * @param value value
 462          */
 463         public void addAttribute(final String prefix, final String ns, final String ln, final String value) {
 464             if (ns == null && prefix == null && xmlns.equals(ln)) {
 465                 this.addNamespaceDeclaration(prefix, value);
 466             } else {
 467                 this.attributeDeclarations.add(new AttributeDeclaration(prefix, ns, ln, value));
 468             }
 469         }
 470 
 471         /**
 472          * Flushes state of this element to the {@code target} element.
 473          *
 474          * <p>
 475          * If this element is initialized then it is added with all the namespace declarations and attributes
 476          * to the {@code target} element as a child. The state of this element is reset to uninitialized.
 477          * The newly added element object is returned.
 478          * </p>
 479          * <p>
 480          * If this element is not initialized then the {@code target} is returned immediately, nothing else is done.
 481          * </p>
 482          *
 483          * @param target target element
 484          * @return {@code target} or new element
 485          * @throws XMLStreamException on error
 486          */
 487         public SOAPElement flushTo(final SOAPElement target) throws XMLStreamException {
 488             try {
 489                 if (this.localName != null) {
 490                     // add the element appropriately (based on namespace declaration)
 491                     final SOAPElement newElement;
 492                     if (this.namespaceUri == null) {
 493                         // add element with inherited scope
 494                         newElement = target.addChildElement(this.localName);
 495                     } else if (prefix == null) {
 496                         newElement = target.addChildElement(new QName(this.namespaceUri, this.localName));
 497                     } else {
 498                         newElement = target.addChildElement(this.localName, this.prefix, this.namespaceUri);
 499                     }
 500                     // add namespace declarations
 501                     for (NamespaceDeclaration namespace : this.namespaceDeclarations) {
 502                         target.addNamespaceDeclaration(namespace.prefix, namespace.namespaceUri);
 503                     }
 504                     // add attribute declarations
 505                     for (AttributeDeclaration attribute : this.attributeDeclarations) {
 506                         addAttibuteToElement(newElement,
 507                                 attribute.prefix, attribute.namespaceUri, attribute.localName, attribute.value);
 508                     }
 509                     // reset state
 510                     this.reset();
 511 
 512                     return newElement;
 513                 } else {
 514                     return target;
 515                 }
 516                 // else after reset state -> not initialized
 517             } catch (SOAPException e) {
 518                 throw new XMLStreamException(e);
 519             }
 520         }
 521 
 522         /**
 523          * Is the element initialized?
 524          * @return boolean indicating whether it was initialized after last flush
 525          */
 526         public boolean isInitialized() {
 527             return this.localName != null;
 528         }
 529 
 530         private void reset() {
 531             this.localName = null;
 532             this.prefix = null;
 533             this.namespaceUri = null;
 534             this.namespaceDeclarations.clear();
 535             this.attributeDeclarations.clear();
 536         }
 537 
 538         private static String emptyIfNull(String s) {
 539             return s == null ? "" : s;
 540         }
 541     }
 542 
 543     static class NamespaceDeclaration {
 544         final String prefix;
 545         final String namespaceUri;
 546 
 547         NamespaceDeclaration(String prefix, String namespaceUri) {
 548             this.prefix = prefix;
 549             this.namespaceUri = namespaceUri;
 550         }
 551     }
 552 
 553     static class AttributeDeclaration {
 554         final String prefix;
 555         final String namespaceUri;
 556         final String localName;
 557         final String value;
 558 
 559         AttributeDeclaration(String prefix, String namespaceUri, String localName, String value) {
 560             this.prefix = prefix;
 561             this.namespaceUri = namespaceUri;
 562             this.localName = localName;
 563             this.value = value;
 564         }
 565     }
 566 }