/*
 * Copyright (c) 2013, 2017, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package com.sun.xml.internal.ws.api.message.saaj;

import java.util.Iterator;
import java.util.Arrays;
import java.util.List;
import java.util.LinkedList;

import javax.xml.namespace.NamespaceContext;
import javax.xml.namespace.QName;
import javax.xml.soap.SOAPElement;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPMessage;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;

import org.w3c.dom.Comment;
import org.w3c.dom.Node;

SaajStaxWriter builds a SAAJ SOAPMessage by using XMLStreamWriter interface.

Defers creation of SOAPElement until all the aspects of the name of the element are known. In some cases, the namespace uri is indicated only by the writeNamespace(String, String) call. After opening an element (writeStartElement, writeEmptyElement methods), all attributes and namespace assignments are retained within DeferredElement object (deferredElement field). As soon as any other method than writeAttribute, writeNamespace, writeDefaultNamespace or setNamespace is called, the contents of deferredElement is transformed into new SOAPElement (which is appropriately inserted into the SOAPMessage under construction). This mechanism is necessary to fix JDK-8159058 issue.

Author:shih-chang.chen@oracle.com
/** * SaajStaxWriter builds a SAAJ SOAPMessage by using XMLStreamWriter interface. * * <p> * Defers creation of SOAPElement until all the aspects of the name of the element are known. * In some cases, the namespace uri is indicated only by the {@link #writeNamespace(String, String)} call. * After opening an element ({@code writeStartElement}, {@code writeEmptyElement} methods), all attributes * and namespace assignments are retained within {@link DeferredElement} object ({@code deferredElement} field). * As soon as any other method than {@code writeAttribute}, {@code writeNamespace}, {@code writeDefaultNamespace} * or {@code setNamespace} is called, the contents of {@code deferredElement} is transformed into new SOAPElement * (which is appropriately inserted into the SOAPMessage under construction). * This mechanism is necessary to fix JDK-8159058 issue. * </p> * * @author shih-chang.chen@oracle.com */
public class SaajStaxWriter implements XMLStreamWriter { protected SOAPMessage soap; protected String envURI; protected SOAPElement currentElement; protected DeferredElement deferredElement; static final protected String Envelope = "Envelope"; static final protected String Header = "Header"; static final protected String Body = "Body"; static final protected String xmlns = "xmlns"; public SaajStaxWriter(final SOAPMessage msg, String uri) throws SOAPException { soap = msg; this.envURI = uri; this.deferredElement = new DeferredElement(); } public SOAPMessage getSOAPMessage() { return soap; } protected SOAPElement getEnvelope() throws SOAPException { return soap.getSOAPPart().getEnvelope(); } @Override public void writeStartElement(final String localName) throws XMLStreamException { currentElement = deferredElement.flushTo(currentElement); deferredElement.setLocalName(localName); } @Override public void writeStartElement(final String ns, final String ln) throws XMLStreamException { writeStartElement(null, ln, ns); } @Override public void writeStartElement(final String prefix, final String ln, final String ns) throws XMLStreamException { currentElement = deferredElement.flushTo(currentElement); if (envURI.equals(ns)) { try { if (Envelope.equals(ln)) { currentElement = getEnvelope(); fixPrefix(prefix); return; } else if (Header.equals(ln)) { currentElement = soap.getSOAPHeader(); fixPrefix(prefix); return; } else if (Body.equals(ln)) { currentElement = soap.getSOAPBody(); fixPrefix(prefix); return; } } catch (SOAPException e) { throw new XMLStreamException(e); } } deferredElement.setLocalName(ln); deferredElement.setNamespaceUri(ns); deferredElement.setPrefix(prefix); } private void fixPrefix(final String prfx) throws XMLStreamException { fixPrefix(prfx, currentElement); } private void fixPrefix(final String prfx, SOAPElement element) throws XMLStreamException { String oldPrfx = element.getPrefix(); if (prfx != null && !prfx.equals(oldPrfx)) { element.setPrefix(prfx); } } @Override public void writeEmptyElement(final String uri, final String ln) throws XMLStreamException { writeStartElement(null, ln, uri); } @Override public void writeEmptyElement(final String prefix, final String ln, final String uri) throws XMLStreamException { writeStartElement(prefix, ln, uri); } @Override public void writeEmptyElement(final String ln) throws XMLStreamException { writeStartElement(null, ln, null); } @Override public void writeEndElement() throws XMLStreamException { currentElement = deferredElement.flushTo(currentElement); if (currentElement != null) currentElement = currentElement.getParentElement(); } @Override public void writeEndDocument() throws XMLStreamException { currentElement = deferredElement.flushTo(currentElement); } @Override public void close() throws XMLStreamException { } @Override public void flush() throws XMLStreamException { } @Override public void writeAttribute(final String ln, final String val) throws XMLStreamException { writeAttribute(null, null, ln, val); } @Override public void writeAttribute(final String prefix, final String ns, final String ln, final String value) throws XMLStreamException { if (ns == null && prefix == null && xmlns.equals(ln)) { writeNamespace("", value); } else { if (deferredElement.isInitialized()) { deferredElement.addAttribute(prefix, ns, ln, value); } else { addAttibuteToElement(currentElement, prefix, ns, ln, value); } } } @Override public void writeAttribute(final String ns, final String ln, final String val) throws XMLStreamException { writeAttribute(null, ns, ln, val); } @Override public void writeNamespace(String prefix, final String uri) throws XMLStreamException { // make prefix default if null or "xmlns" (according to javadoc) String thePrefix = prefix == null || "xmlns".equals(prefix) ? "" : prefix; if (deferredElement.isInitialized()) { deferredElement.addNamespaceDeclaration(thePrefix, uri); } else { try { currentElement.addNamespaceDeclaration(thePrefix, uri); } catch (SOAPException e) { throw new XMLStreamException(e); } } } @Override public void writeDefaultNamespace(final String uri) throws XMLStreamException { writeNamespace("", uri); } @Override public void writeComment(final String data) throws XMLStreamException { currentElement = deferredElement.flushTo(currentElement); Comment c = soap.getSOAPPart().createComment(data); currentElement.appendChild(c); } @Override public void writeProcessingInstruction(final String target) throws XMLStreamException { currentElement = deferredElement.flushTo(currentElement); Node n = soap.getSOAPPart().createProcessingInstruction(target, ""); currentElement.appendChild(n); } @Override public void writeProcessingInstruction(final String target, final String data) throws XMLStreamException { currentElement = deferredElement.flushTo(currentElement); Node n = soap.getSOAPPart().createProcessingInstruction(target, data); currentElement.appendChild(n); } @Override public void writeCData(final String data) throws XMLStreamException { currentElement = deferredElement.flushTo(currentElement); Node n = soap.getSOAPPart().createCDATASection(data); currentElement.appendChild(n); } @Override public void writeDTD(final String dtd) throws XMLStreamException { currentElement = deferredElement.flushTo(currentElement); } @Override public void writeEntityRef(final String name) throws XMLStreamException { currentElement = deferredElement.flushTo(currentElement); Node n = soap.getSOAPPart().createEntityReference(name); currentElement.appendChild(n); } @Override public void writeStartDocument() throws XMLStreamException { } @Override public void writeStartDocument(final String version) throws XMLStreamException { if (version != null) soap.getSOAPPart().setXmlVersion(version); } @Override public void writeStartDocument(final String encoding, final String version) throws XMLStreamException { if (version != null) soap.getSOAPPart().setXmlVersion(version); if (encoding != null) { try { soap.setProperty(SOAPMessage.CHARACTER_SET_ENCODING, encoding); } catch (SOAPException e) { throw new XMLStreamException(e); } } } @Override public void writeCharacters(final String text) throws XMLStreamException { currentElement = deferredElement.flushTo(currentElement); try { currentElement.addTextNode(text); } catch (SOAPException e) { throw new XMLStreamException(e); } } @Override public void writeCharacters(final char[] text, final int start, final int len) throws XMLStreamException { currentElement = deferredElement.flushTo(currentElement); char[] chr = (start == 0 && len == text.length) ? text : Arrays.copyOfRange(text, start, start + len); try { currentElement.addTextNode(new String(chr)); } catch (SOAPException e) { throw new XMLStreamException(e); } } @Override public String getPrefix(final String uri) throws XMLStreamException { return currentElement.lookupPrefix(uri); } @Override public void setPrefix(final String prefix, final String uri) throws XMLStreamException { // TODO: this in fact is not what would be expected from XMLStreamWriter // (e.g. XMLStreamWriter for writing to output stream does not write anything as result of // this method, it just rememebers that given prefix is associated with the given uri // for the scope; to actually declare the prefix assignment in the resulting XML, one // needs to call writeNamespace(...) method // Kept for backwards compatibility reasons - this might be worth of further investigation. if (deferredElement.isInitialized()) { deferredElement.addNamespaceDeclaration(prefix, uri); } else { throw new XMLStreamException("Namespace not associated with any element"); } } @Override public void setDefaultNamespace(final String uri) throws XMLStreamException { setPrefix("", uri); } @Override public void setNamespaceContext(final NamespaceContext context)throws XMLStreamException { throw new UnsupportedOperationException(); } @Override public Object getProperty(final String name) throws IllegalArgumentException { //TODO the following line is to make eclipselink happy ... they are aware of this problem - if (javax.xml.stream.XMLOutputFactory.IS_REPAIRING_NAMESPACES.equals(name)) return Boolean.FALSE; return null; } @Override public NamespaceContext getNamespaceContext() { return new NamespaceContext() { public String getNamespaceURI(final String prefix) { return currentElement.getNamespaceURI(prefix); } public String getPrefix(final String namespaceURI) { return currentElement.lookupPrefix(namespaceURI); } public Iterator getPrefixes(final String namespaceURI) { return new Iterator<String>() { String prefix = getPrefix(namespaceURI); public boolean hasNext() { return (prefix != null); } public String next() { if (!hasNext()) throw new java.util.NoSuchElementException(); String next = prefix; prefix = null; return next; } public void remove() {} }; } }; } static void addAttibuteToElement(SOAPElement element, String prefix, String ns, String ln, String value) throws XMLStreamException { try { if (ns == null) { element.setAttributeNS("", ln, value); } else { QName name = prefix == null ? new QName(ns, ln) : new QName(ns, ln, prefix); element.addAttribute(name, value); } } catch (SOAPException e) { throw new XMLStreamException(e); } }
Holds details of element that needs to be deferred in order to manage namespace assignments correctly.

An instance of can be set with all the aspects of the element name (local name, prefix, namespace uri). Attributes and namespace declarations (special case of attribute) can be added. Namespace declarations are handled so that the element namespace is updated if it is implied by the namespace declaration and the namespace was not set to non-null value previously.

The state of this object can be flushed to SOAPElement - new SOAPElement will be added a child element; the new element will have exactly the shape as represented by the state of this object. Note that the flushTo(SOAPElement) method does nothing (and returns the argument immediately) if the state of this object is not initialized (i.e. local name is null).

Author:ondrej.cerny@oracle.com
/** * Holds details of element that needs to be deferred in order to manage namespace assignments correctly. * * <p> * An instance of can be set with all the aspects of the element name (local name, prefix, namespace uri). * Attributes and namespace declarations (special case of attribute) can be added. * Namespace declarations are handled so that the element namespace is updated if it is implied by the namespace * declaration and the namespace was not set to non-{@code null} value previously. * </p> * * <p> * The state of this object can be {@link #flushTo(SOAPElement) flushed} to SOAPElement - new SOAPElement will * be added a child element; the new element will have exactly the shape as represented by the state of this * object. Note that the {@link #flushTo(SOAPElement)} method does nothing * (and returns the argument immediately) if the state of this object is not initialized * (i.e. local name is null). * </p> * * @author ondrej.cerny@oracle.com */
static class DeferredElement { private String prefix; private String localName; private String namespaceUri; private final List<NamespaceDeclaration> namespaceDeclarations; private final List<AttributeDeclaration> attributeDeclarations; DeferredElement() { this.namespaceDeclarations = new LinkedList<NamespaceDeclaration>(); this.attributeDeclarations = new LinkedList<AttributeDeclaration>(); reset(); }
Set prefix of the element.
Params:
  • prefix – namespace prefix
/** * Set prefix of the element. * @param prefix namespace prefix */
public void setPrefix(final String prefix) { this.prefix = prefix; }
Set local name of the element.

This method initializes the element.

Params:
  • localName – local name not null
/** * Set local name of the element. * * <p> * This method initializes the element. * </p> * * @param localName local name {@code not null} */
public void setLocalName(final String localName) { if (localName == null) { throw new IllegalArgumentException("localName can not be null"); } this.localName = localName; }
Set namespace uri.
Params:
  • namespaceUri – namespace uri
/** * Set namespace uri. * * @param namespaceUri namespace uri */
public void setNamespaceUri(final String namespaceUri) { this.namespaceUri = namespaceUri; }
Adds namespace prefix assignment to the element.
Params:
  • prefix – prefix (not null)
  • namespaceUri – namespace uri
/** * Adds namespace prefix assignment to the element. * * @param prefix prefix (not {@code null}) * @param namespaceUri namespace uri */
public void addNamespaceDeclaration(final String prefix, final String namespaceUri) { if (null == this.namespaceUri && null != namespaceUri && prefix.equals(emptyIfNull(this.prefix))) { this.namespaceUri = namespaceUri; } this.namespaceDeclarations.add(new NamespaceDeclaration(prefix, namespaceUri)); }
Adds attribute to the element.
Params:
  • prefix – prefix
  • ns – namespace
  • ln – local name
  • value – value
/** * Adds attribute to the element. * @param prefix prefix * @param ns namespace * @param ln local name * @param value value */
public void addAttribute(final String prefix, final String ns, final String ln, final String value) { if (ns == null && prefix == null && xmlns.equals(ln)) { this.addNamespaceDeclaration(prefix, value); } else { this.attributeDeclarations.add(new AttributeDeclaration(prefix, ns, ln, value)); } }
Flushes state of this element to the target element.

If this element is initialized then it is added with all the namespace declarations and attributes to the target element as a child. The state of this element is reset to uninitialized. The newly added element object is returned.

If this element is not initialized then the target is returned immediately, nothing else is done.

Params:
  • target – target element
Throws:
Returns:target or new element
/** * Flushes state of this element to the {@code target} element. * * <p> * If this element is initialized then it is added with all the namespace declarations and attributes * to the {@code target} element as a child. The state of this element is reset to uninitialized. * The newly added element object is returned. * </p> * <p> * If this element is not initialized then the {@code target} is returned immediately, nothing else is done. * </p> * * @param target target element * @return {@code target} or new element * @throws XMLStreamException on error */
public SOAPElement flushTo(final SOAPElement target) throws XMLStreamException { try { if (this.localName != null) { // add the element appropriately (based on namespace declaration) final SOAPElement newElement; if (this.namespaceUri == null) { // add element with inherited scope newElement = target.addChildElement(this.localName); } else if (prefix == null) { newElement = target.addChildElement(new QName(this.namespaceUri, this.localName)); } else { newElement = target.addChildElement(this.localName, this.prefix, this.namespaceUri); } // add namespace declarations for (NamespaceDeclaration namespace : this.namespaceDeclarations) { newElement.addNamespaceDeclaration(namespace.prefix, namespace.namespaceUri); } // add attribute declarations for (AttributeDeclaration attribute : this.attributeDeclarations) { addAttibuteToElement(newElement, attribute.prefix, attribute.namespaceUri, attribute.localName, attribute.value); } // reset state this.reset(); return newElement; } else { return target; } // else after reset state -> not initialized } catch (SOAPException e) { throw new XMLStreamException(e); } }
Is the element initialized?
Returns:boolean indicating whether it was initialized after last flush
/** * Is the element initialized? * @return boolean indicating whether it was initialized after last flush */
public boolean isInitialized() { return this.localName != null; } private void reset() { this.localName = null; this.prefix = null; this.namespaceUri = null; this.namespaceDeclarations.clear(); this.attributeDeclarations.clear(); } private static String emptyIfNull(String s) { return s == null ? "" : s; } } static class NamespaceDeclaration { final String prefix; final String namespaceUri; NamespaceDeclaration(String prefix, String namespaceUri) { this.prefix = prefix; this.namespaceUri = namespaceUri; } } static class AttributeDeclaration { final String prefix; final String namespaceUri; final String localName; final String value; AttributeDeclaration(String prefix, String namespaceUri, String localName, String value) { this.prefix = prefix; this.namespaceUri = namespaceUri; this.localName = localName; this.value = value; } } }