1/*
2 * Copyright (c) 2013, 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
26package com.sun.xml.internal.ws.api.message.saaj;
27
28import java.util.Iterator;
29import java.util.Arrays;
30import java.util.List;
31import java.util.LinkedList;
32
33import javax.xml.namespace.NamespaceContext;
34import javax.xml.namespace.QName;
35import javax.xml.soap.SOAPElement;
36import javax.xml.soap.SOAPException;
37import javax.xml.soap.SOAPMessage;
38import javax.xml.stream.XMLStreamException;
39import javax.xml.stream.XMLStreamWriter;
40
41import org.w3c.dom.Comment;
42import 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 */
60public 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}
567