1/*
2 * Copyright (c) 1997, 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.util.pipe;
27
28import com.sun.istack.internal.NotNull;
29import com.sun.istack.internal.Nullable;
30import com.sun.xml.internal.stream.buffer.XMLStreamBufferResult;
31import com.sun.xml.internal.ws.api.WSBinding;
32import com.sun.xml.internal.ws.api.message.Message;
33import com.sun.xml.internal.ws.api.message.Packet;
34import com.sun.xml.internal.ws.api.pipe.Tube;
35import com.sun.xml.internal.ws.api.pipe.TubeCloner;
36import com.sun.xml.internal.ws.api.pipe.helper.AbstractFilterTubeImpl;
37import com.sun.xml.internal.ws.api.server.DocumentAddressResolver;
38import com.sun.xml.internal.ws.api.server.SDDocument;
39import com.sun.xml.internal.ws.api.server.SDDocumentSource;
40import com.sun.xml.internal.ws.developer.SchemaValidationFeature;
41import com.sun.xml.internal.ws.developer.ValidationErrorHandler;
42import com.sun.xml.internal.ws.server.SDDocumentImpl;
43import com.sun.xml.internal.ws.util.ByteArrayBuffer;
44import com.sun.xml.internal.ws.util.xml.XmlUtil;
45import com.sun.xml.internal.ws.wsdl.SDDocumentResolver;
46import com.sun.xml.internal.ws.wsdl.parser.WSDLConstants;
47import org.w3c.dom.*;
48import org.w3c.dom.ls.LSInput;
49import org.w3c.dom.ls.LSResourceResolver;
50import org.xml.sax.SAXException;
51import org.xml.sax.helpers.NamespaceSupport;
52
53import javax.xml.XMLConstants;
54import javax.xml.namespace.QName;
55import javax.xml.transform.Source;
56import javax.xml.transform.Transformer;
57import javax.xml.transform.TransformerException;
58import javax.xml.transform.dom.DOMResult;
59import javax.xml.transform.dom.DOMSource;
60import javax.xml.transform.stream.StreamSource;
61import javax.xml.validation.SchemaFactory;
62import javax.xml.validation.Validator;
63import javax.xml.ws.WebServiceException;
64import java.io.IOException;
65import java.io.InputStream;
66import java.io.Reader;
67import java.io.StringReader;
68import java.net.MalformedURLException;
69import java.net.URI;
70import java.net.URL;
71import java.util.*;
72import java.util.logging.Level;
73import java.util.logging.Logger;
74
75import static com.sun.xml.internal.ws.util.xml.XmlUtil.allowExternalAccess;
76
77/**
78 * {@link Tube} that does the schema validation.
79 *
80 * @author Jitendra Kotamraju
81 */
82public abstract class AbstractSchemaValidationTube extends AbstractFilterTubeImpl {
83
84    private static final Logger LOGGER = Logger.getLogger(AbstractSchemaValidationTube.class.getName());
85
86    protected final WSBinding binding;
87    protected final SchemaValidationFeature feature;
88    protected final DocumentAddressResolver resolver = new ValidationDocumentAddressResolver();
89    protected final SchemaFactory sf;
90
91    public AbstractSchemaValidationTube(WSBinding binding, Tube next) {
92        super(next);
93        this.binding = binding;
94        feature = binding.getFeature(SchemaValidationFeature.class);
95        sf = allowExternalAccess(SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI), "all", false);
96    }
97
98    protected AbstractSchemaValidationTube(AbstractSchemaValidationTube that, TubeCloner cloner) {
99        super(that, cloner);
100        this.binding = that.binding;
101        this.feature = that.feature;
102        this.sf = that.sf;
103    }
104
105    protected abstract Validator getValidator();
106
107    protected abstract boolean isNoValidation();
108
109    private static class ValidationDocumentAddressResolver implements DocumentAddressResolver {
110
111        @Nullable
112        @Override
113        public String getRelativeAddressFor(@NotNull SDDocument current, @NotNull SDDocument referenced) {
114            LOGGER.log(Level.FINE, "Current = {0} resolved relative={1}", new Object[]{current.getURL(), referenced.getURL()});
115            return referenced.getURL().toExternalForm();
116        }
117    }
118
119    private Document createDOM(SDDocument doc) {
120        // Get infoset
121        ByteArrayBuffer bab = new ByteArrayBuffer();
122        try {
123            doc.writeTo(null, resolver, bab);
124        } catch (IOException ioe) {
125            throw new WebServiceException(ioe);
126        }
127
128        // Convert infoset to DOM
129        Transformer trans = XmlUtil.newTransformer();
130        Source source = new StreamSource(bab.newInputStream(), null); //doc.getURL().toExternalForm());
131        DOMResult result = new DOMResult();
132        try {
133            trans.transform(source, result);
134        } catch(TransformerException te) {
135            throw new WebServiceException(te);
136        }
137        return (Document)result.getNode();
138    }
139
140    protected class MetadataResolverImpl implements SDDocumentResolver, LSResourceResolver {
141
142        // systemID --> SDDocument
143        final Map<String, SDDocument> docs = new HashMap<String, SDDocument>();
144
145        // targetnamespace --> SDDocument
146        final Map<String, SDDocument> nsMapping = new HashMap<String, SDDocument>();
147
148        public MetadataResolverImpl() {
149        }
150
151        public MetadataResolverImpl(Iterable<SDDocument> it) {
152            for(SDDocument doc : it) {
153                if (doc.isSchema()) {
154                    docs.put(doc.getURL().toExternalForm(), doc);
155                    nsMapping.put(((SDDocument.Schema)doc).getTargetNamespace(), doc);
156                }
157            }
158        }
159
160        void addSchema(Source schema) {
161            assert schema.getSystemId() != null;
162
163            String systemId = schema.getSystemId();
164            try {
165                XMLStreamBufferResult xsbr = XmlUtil.identityTransform(schema, new XMLStreamBufferResult());
166                SDDocumentSource sds = SDDocumentSource.create(new URL(systemId), xsbr.getXMLStreamBuffer());
167                SDDocument sdoc = SDDocumentImpl.create(sds, new QName(""), new QName(""));
168                docs.put(systemId, sdoc);
169                nsMapping.put(((SDDocument.Schema)sdoc).getTargetNamespace(), sdoc);
170            } catch(Exception ex) {
171                LOGGER.log(Level.WARNING, "Exception in adding schemas to resolver", ex);
172            }
173        }
174
175        void addSchemas(Collection<? extends Source> schemas) {
176            for(Source src :  schemas) {
177                addSchema(src);
178            }
179        }
180
181        @Override
182        public SDDocument resolve(String systemId) {
183            SDDocument sdi = docs.get(systemId);
184            if (sdi == null) {
185                SDDocumentSource sds;
186                try {
187                    sds = SDDocumentSource.create(new URL(systemId));
188                } catch(MalformedURLException e) {
189                    throw new WebServiceException(e);
190                }
191                sdi = SDDocumentImpl.create(sds, new QName(""), new QName(""));
192                docs.put(systemId, sdi);
193            }
194            return sdi;
195        }
196
197        @Override
198        public LSInput resolveResource(String type, String namespaceURI, String publicId, final String systemId, final String baseURI) {
199            if (LOGGER.isLoggable(Level.FINE)) {
200                LOGGER.log(Level.FINE, "type={0} namespaceURI={1} publicId={2} systemId={3} baseURI={4}", new Object[]{type, namespaceURI, publicId, systemId, baseURI});
201            }
202            try {
203                final SDDocument doc;
204                if (systemId == null) {
205                    doc = nsMapping.get(namespaceURI);
206                } else {
207                    URI rel = (baseURI != null)
208                        ? new URI(baseURI).resolve(systemId)
209                        : new URI(systemId);
210                    doc = docs.get(rel.toString());
211                }
212                if (doc != null) {
213                    return new LSInput() {
214
215                        @Override
216                        public Reader getCharacterStream() {
217                            return null;
218                        }
219
220                        @Override
221                        public void setCharacterStream(Reader characterStream) {
222                            throw new UnsupportedOperationException();
223                        }
224
225                        @Override
226                        public InputStream getByteStream() {
227                            ByteArrayBuffer bab = new ByteArrayBuffer();
228                            try {
229                                doc.writeTo(null, resolver, bab);
230                            } catch (IOException ioe) {
231                                throw new WebServiceException(ioe);
232                            }
233                            return bab.newInputStream();
234                        }
235
236                        @Override
237                        public void setByteStream(InputStream byteStream) {
238                            throw new UnsupportedOperationException();
239                        }
240
241                        @Override
242                        public String getStringData() {
243                            return null;
244                        }
245
246                        @Override
247                        public void setStringData(String stringData) {
248                            throw new UnsupportedOperationException();
249                        }
250
251                        @Override
252                        public String getSystemId() {
253                            return doc.getURL().toExternalForm();
254                        }
255
256                        @Override
257                        public void setSystemId(String systemId) {
258                            throw new UnsupportedOperationException();
259                        }
260
261                        @Override
262                        public String getPublicId() {
263                            return null;
264                        }
265
266                        @Override
267                        public void setPublicId(String publicId) {
268                            throw new UnsupportedOperationException();
269                        }
270
271                        @Override
272                        public String getBaseURI() {
273                            return doc.getURL().toExternalForm();
274                        }
275
276                        @Override
277                        public void setBaseURI(String baseURI) {
278                            throw new UnsupportedOperationException();
279                        }
280
281                        @Override
282                        public String getEncoding() {
283                            return null;
284                        }
285
286                        @Override
287                        public void setEncoding(String encoding) {
288                            throw new UnsupportedOperationException();
289                        }
290
291                        @Override
292                        public boolean getCertifiedText() {
293                            return false;
294                        }
295
296                        @Override
297                        public void setCertifiedText(boolean certifiedText) {
298                            throw new UnsupportedOperationException();
299                        }
300                    };
301                }
302            } catch(Exception e) {
303                LOGGER.log(Level.WARNING, "Exception in LSResourceResolver impl", e);
304            }
305            if (LOGGER.isLoggable(Level.FINE)) {
306                LOGGER.log(Level.FINE, "Don''t know about systemId={0} baseURI={1}", new Object[]{systemId, baseURI});
307            }
308            return null;
309        }
310
311    }
312
313    private void updateMultiSchemaForTns(String tns, String systemId, Map<String, List<String>> schemas) {
314        List<String> docIdList = schemas.get(tns);
315        if (docIdList == null) {
316            docIdList = new ArrayList<String>();
317            schemas.put(tns, docIdList);
318        }
319        docIdList.add(systemId);
320    }
321
322    /*
323     * Using the following algorithm described in the xerces discussion thread:
324     *
325     * "If you're synthesizing schema documents to glue together the ones in
326     * the WSDL then you may not even need to use "honour-all-schemaLocations".
327     * Create a schema document for each namespace with <xs:include>s
328     * (for each schema document in the WSDL with that target namespace)
329     * and then combine those together with <xs:import>s for each of those
330     * namespaces in a "master" schema document.
331     *
332     * That should work with any schema processor, not just those which
333     * honour multiple imports for the same namespace."
334     */
335    protected Source[] getSchemaSources(Iterable<SDDocument> docs, MetadataResolverImpl mdresolver) {
336        // All schema fragments in WSDLs are put inlinedSchemas
337        // systemID --> DOMSource
338        Map<String, DOMSource> inlinedSchemas = new HashMap<String, DOMSource>();
339
340        // Consolidates all the schemas(inlined and external) for a tns
341        // tns --> list of systemId
342        Map<String, List<String>> multiSchemaForTns = new HashMap<String, List<String>>();
343
344        for(SDDocument sdoc: docs) {
345            if (sdoc.isWSDL()) {
346                Document dom = createDOM(sdoc);
347                // Get xsd:schema node from WSDL's DOM
348                addSchemaFragmentSource(dom, sdoc.getURL().toExternalForm(), inlinedSchemas);
349            } else if (sdoc.isSchema()) {
350                updateMultiSchemaForTns(((SDDocument.Schema)sdoc).getTargetNamespace(), sdoc.getURL().toExternalForm(), multiSchemaForTns);
351            }
352        }
353        if (LOGGER.isLoggable(Level.FINE)) {
354            LOGGER.log(Level.FINE, "WSDL inlined schema fragment documents(these are used to create a pseudo schema) = {0}", inlinedSchemas.keySet());
355        }
356        for(DOMSource src: inlinedSchemas.values()) {
357            String tns = getTargetNamespace(src);
358            updateMultiSchemaForTns(tns, src.getSystemId(), multiSchemaForTns);
359        }
360
361        if (multiSchemaForTns.isEmpty()) {
362            return new Source[0];   // WSDL doesn't have any schema fragments
363        } else if (multiSchemaForTns.size() == 1 && multiSchemaForTns.values().iterator().next().size() == 1) {
364            // It must be a inlined schema, otherwise there would be at least two schemas
365            String systemId = multiSchemaForTns.values().iterator().next().get(0);
366            return new Source[] {inlinedSchemas.get(systemId)};
367        }
368
369        // need to resolve these inlined schema fragments
370        mdresolver.addSchemas(inlinedSchemas.values());
371
372        // If there are multiple schema fragments for the same tns, create a
373        // pseudo schema for that tns by using <xsd:include> of those.
374        // tns --> systemId of a pseudo schema document (consolidated for that tns)
375        Map<String, String> oneSchemaForTns = new HashMap<String, String>();
376        int i = 0;
377        for(Map.Entry<String, List<String>> e: multiSchemaForTns.entrySet()) {
378            String systemId;
379            List<String> sameTnsSchemas = e.getValue();
380            if (sameTnsSchemas.size() > 1) {
381                // SDDocumentSource should be changed to take String systemId
382                // String pseudoSystemId = "urn:x-jax-ws-include-"+i++;
383                systemId = "file:x-jax-ws-include-"+i++;
384                Source src = createSameTnsPseudoSchema(e.getKey(), sameTnsSchemas, systemId);
385                mdresolver.addSchema(src);
386            } else {
387                systemId = sameTnsSchemas.get(0);
388            }
389            oneSchemaForTns.put(e.getKey(), systemId);
390        }
391
392        // create a master pseudo schema with all the different tns
393        Source pseudoSchema = createMasterPseudoSchema(oneSchemaForTns);
394        return new Source[] { pseudoSchema };
395    }
396
397    private @Nullable void addSchemaFragmentSource(Document doc, String systemId, Map<String, DOMSource> map) {
398        Element e = doc.getDocumentElement();
399        assert e.getNamespaceURI().equals(WSDLConstants.NS_WSDL);
400        assert e.getLocalName().equals("definitions");
401
402        NodeList typesList = e.getElementsByTagNameNS(WSDLConstants.NS_WSDL, "types");
403        for(int i=0; i < typesList.getLength(); i++) {
404            NodeList schemaList = ((Element)typesList.item(i)).getElementsByTagNameNS(WSDLConstants.NS_XMLNS, "schema");
405            for(int j=0; j < schemaList.getLength(); j++) {
406                Element elem = (Element)schemaList.item(j);
407                NamespaceSupport nss = new NamespaceSupport();
408                // Doing this because transformer is not picking up inscope namespaces
409                // why doesn't transformer pickup the inscope namespaces ??
410                buildNamespaceSupport(nss, elem);
411                patchDOMFragment(nss, elem);
412                String docId = systemId+"#schema"+j;
413                map.put(docId, new DOMSource(elem, docId));
414            }
415        }
416    }
417
418
419    /*
420     * Recursively visit ancestors and build up {@link org.xml.sax.helpers.NamespaceSupport} object.
421     */
422    private void buildNamespaceSupport(NamespaceSupport nss, Node node) {
423        if (node==null || node.getNodeType()!=Node.ELEMENT_NODE) {
424            return;
425        }
426
427        buildNamespaceSupport( nss, node.getParentNode() );
428
429        nss.pushContext();
430        NamedNodeMap atts = node.getAttributes();
431        for( int i=0; i<atts.getLength(); i++ ) {
432            Attr a = (Attr)atts.item(i);
433            if( "xmlns".equals(a.getPrefix()) ) {
434                nss.declarePrefix( a.getLocalName(), a.getValue() );
435                continue;
436            }
437            if( "xmlns".equals(a.getName()) ) {
438                nss.declarePrefix( "", a.getValue() );
439                //continue;
440            }
441        }
442    }
443
444    /**
445     * Adds inscope namespaces as attributes to  <xsd:schema> fragment nodes.
446     *
447     * @param nss namespace context info
448     * @param elem that is patched with inscope namespaces
449     */
450    private @Nullable void patchDOMFragment(NamespaceSupport nss, Element elem) {
451        NamedNodeMap atts = elem.getAttributes();
452        for( Enumeration en = nss.getPrefixes(); en.hasMoreElements(); ) {
453            String prefix = (String)en.nextElement();
454
455            for( int i=0; i<atts.getLength(); i++ ) {
456                Attr a = (Attr)atts.item(i);
457                if (!"xmlns".equals(a.getPrefix()) || !a.getLocalName().equals(prefix)) {
458                    if (LOGGER.isLoggable(Level.FINE)) {
459                        LOGGER.log(Level.FINE, "Patching with xmlns:{0}={1}", new Object[]{prefix, nss.getURI(prefix)});
460                    }
461                    elem.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, "xmlns:"+prefix, nss.getURI(prefix));
462                }
463            }
464        }
465    }
466
467    /*
468     * Creates a pseudo schema for the WSDL schema fragments that have the same
469     * targetNamespace.
470     *
471     * <xsd:schema targetNamespace="X">
472     *   <xsd:include schemaLocation="Y1"/>
473     *   <xsd:include schemaLocation="Y2"/>
474     * </xsd:schema>
475     *
476     * @param tns targetNamespace of the the schema documents
477     * @param docs collection of systemId for the schema documents that have the
478     *        same tns, the collection must have more than one document
479     * @param psuedoSystemId for the created pseudo schema
480     * @return Source of pseudo schema that can be used multiple times
481     */
482    private @Nullable Source createSameTnsPseudoSchema(String tns, Collection<String> docs, String pseudoSystemId) {
483        assert docs.size() > 1;
484
485        final StringBuilder sb = new StringBuilder("<xsd:schema xmlns:xsd='http://www.w3.org/2001/XMLSchema'");
486        if (tns != null && !("".equals(tns)) && !("null".equals(tns))) {
487            sb.append(" targetNamespace='").append(tns).append("'");
488        }
489        sb.append(">\n");
490        for(String systemId : docs) {
491            sb.append("<xsd:include schemaLocation='").append(systemId).append("'/>\n");
492        }
493        sb.append("</xsd:schema>\n");
494        if (LOGGER.isLoggable(Level.FINE)) {
495            LOGGER.log(Level.FINE, "Pseudo Schema for the same tns={0}is {1}", new Object[]{tns, sb});
496        }
497
498        // override getReader() so that the same source can be used multiple times
499        return new StreamSource(pseudoSystemId) {
500            @Override
501            public Reader getReader() {
502                return new StringReader(sb.toString());
503            }
504        };
505    }
506
507    /*
508     * Creates a master pseudo schema importing all WSDL schema fragments with
509     * different tns+pseudo schema for same tns.
510     * <xsd:schema targetNamespace="urn:x-jax-ws-master">
511     *   <xsd:import schemaLocation="Y1" namespace="X1"/>
512     *   <xsd:import schemaLocation="Y2" namespace="X2"/>
513     * </xsd:schema>
514     *
515     * @param pseudo a map(tns-->systemId) of schema documents
516     * @return Source of pseudo schema that can be used multiple times
517     */
518    private Source createMasterPseudoSchema(Map<String, String> docs) {
519        final StringBuilder sb = new StringBuilder("<xsd:schema xmlns:xsd='http://www.w3.org/2001/XMLSchema' targetNamespace='urn:x-jax-ws-master'>\n");
520        for(Map.Entry<String, String> e : docs.entrySet()) {
521            String systemId = e.getValue();
522            String ns = e.getKey();
523            sb.append("<xsd:import schemaLocation='").append(systemId).append("'");
524            if (ns != null && !("".equals(ns))) {
525                sb.append(" namespace='").append(ns).append("'");
526            }
527            sb.append("/>\n");
528        }
529        sb.append("</xsd:schema>");
530        if (LOGGER.isLoggable(Level.FINE)) {
531            LOGGER.log(Level.FINE, "Master Pseudo Schema = {0}", sb);
532        }
533
534        // override getReader() so that the same source can be used multiple times
535        return new StreamSource("file:x-jax-ws-master-doc") {
536            @Override
537            public Reader getReader() {
538                return new StringReader(sb.toString());
539            }
540        };
541    }
542
543    protected void doProcess(Packet packet) throws SAXException {
544        getValidator().reset();
545        Class<? extends ValidationErrorHandler> handlerClass = feature.getErrorHandler();
546        ValidationErrorHandler handler;
547        try {
548            handler = handlerClass.newInstance();
549        } catch(Exception e) {
550            throw new WebServiceException(e);
551        }
552        handler.setPacket(packet);
553        getValidator().setErrorHandler(handler);
554        Message msg = packet.getMessage().copy();
555        Source source = msg.readPayloadAsSource();
556        try {
557            // Validator javadoc allows ONLY SAX, and DOM Sources
558            // But the impl seems to handle all kinds.
559            getValidator().validate(source);
560        } catch(IOException e) {
561            throw new WebServiceException(e);
562        }
563    }
564
565    private String getTargetNamespace(DOMSource src) {
566        Element elem = (Element)src.getNode();
567        return elem.getAttribute("targetNamespace");
568    }
569
570//    protected static void printSource(Source src) {
571//        try {
572//            ByteArrayBuffer bos = new ByteArrayBuffer();
573//            StreamResult sr = new StreamResult(bos );
574//            Transformer trans = TransformerFactory.newInstance().newTransformer();
575//            trans.transform(src, sr);
576//            LOGGER.info("**** src ******"+bos.toString());
577//            bos.close();
578//        } catch(Exception e) {
579//            e.printStackTrace();
580//        }
581//    }
582
583}
584