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
26package com.sun.xml.internal.messaging.saaj.util.stax;
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            @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}
573