001    /**
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements.  See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License.  You may obtain a copy of the License at
008     *
009     *      http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    package org.apache.camel.processor.validation;
018    
019    import java.io.ByteArrayInputStream;
020    import java.io.File;
021    import java.io.IOException;
022    import java.io.InputStream;
023    import java.net.URL;
024    import java.util.Collections;
025    
026    import javax.xml.XMLConstants;
027    import javax.xml.parsers.ParserConfigurationException;
028    import javax.xml.transform.Result;
029    import javax.xml.transform.Source;
030    import javax.xml.transform.dom.DOMResult;
031    import javax.xml.transform.dom.DOMSource;
032    import javax.xml.transform.sax.SAXResult;
033    import javax.xml.transform.sax.SAXSource;
034    import javax.xml.transform.stax.StAXSource;
035    import javax.xml.transform.stream.StreamSource;
036    import javax.xml.validation.Schema;
037    import javax.xml.validation.SchemaFactory;
038    import javax.xml.validation.Validator;
039    
040    import org.w3c.dom.Node;
041    import org.w3c.dom.ls.LSResourceResolver;
042    
043    import org.xml.sax.SAXException;
044    import org.xml.sax.SAXParseException;
045    
046    import org.apache.camel.AsyncCallback;
047    import org.apache.camel.AsyncProcessor;
048    import org.apache.camel.Exchange;
049    import org.apache.camel.ExpectedBodyTypeException;
050    import org.apache.camel.RuntimeTransformException;
051    import org.apache.camel.TypeConverter;
052    import org.apache.camel.converter.jaxp.XmlConverter;
053    import org.apache.camel.util.AsyncProcessorHelper;
054    import org.apache.camel.util.IOHelper;
055    import org.slf4j.Logger;
056    import org.slf4j.LoggerFactory;
057    
058    
059    /**
060     * A processor which validates the XML version of the inbound message body
061     * against some schema either in XSD or RelaxNG
062     */
063    public class ValidatingProcessor implements AsyncProcessor {
064        private static final Logger LOG = LoggerFactory.getLogger(ValidatingProcessor.class);
065        private XmlConverter converter = new XmlConverter();
066        private String schemaLanguage = XMLConstants.W3C_XML_SCHEMA_NS_URI;
067        private volatile Schema schema;
068        private Source schemaSource;
069        private volatile SchemaFactory schemaFactory;
070        private URL schemaUrl;
071        private File schemaFile;
072        private byte[] schemaAsByteArray;
073        private ValidatorErrorHandler errorHandler = new DefaultValidationErrorHandler();
074        private boolean useDom;
075        private boolean useSharedSchema = true;
076        private LSResourceResolver resourceResolver;
077        private boolean failOnNullBody = true;
078        private boolean failOnNullHeader = true;
079        private String headerName;
080    
081        public void process(Exchange exchange) throws Exception {
082            AsyncProcessorHelper.process(this, exchange);
083        }
084    
085        public boolean process(Exchange exchange, AsyncCallback callback) {
086            try {
087                doProcess(exchange);
088            } catch (Exception e) {
089                exchange.setException(e);
090            }
091            callback.done(true);
092            return true;
093        }
094    
095        protected void doProcess(Exchange exchange) throws Exception {
096            Schema schema;
097            if (isUseSharedSchema()) {
098                schema = getSchema();
099            } else {
100                schema = createSchema();
101            }
102    
103            Validator validator = schema.newValidator();
104    
105            // the underlying input stream, which we need to close to avoid locking files or other resources
106            Source source = null;
107            InputStream is = null;
108            try {
109                Result result = null;
110                // only convert to input stream if really needed
111                if (isInputStreamNeeded(exchange)) {
112                    is = getContentToValidate(exchange, InputStream.class);
113                    if (is != null) {
114                        source = getSource(exchange, is);
115                    }
116                } else {
117                    Object content = getContentToValidate(exchange);
118                    if (content != null) {
119                        source = getSource(exchange, content);
120                    }
121                }
122    
123                if (shouldUseHeader()) {
124                    if (source == null && isFailOnNullHeader()) {
125                        throw new NoXmlHeaderValidationException(exchange, headerName);
126                    }
127                } else {
128                    if (source == null && isFailOnNullBody()) {
129                        throw new NoXmlBodyValidationException(exchange);
130                    }
131                }
132    
133                //CAMEL-7036 We don't need to set the result if the source is an instance of StreamSource
134                if (source instanceof DOMSource) {
135                    result = new DOMResult();
136                } else if (source instanceof SAXSource) {
137                    result = new SAXResult();
138                } else if (source instanceof StAXSource || source instanceof StreamSource) {
139                    result = null;
140                }
141    
142                if (source != null) {
143                    // create a new errorHandler and set it on the validator
144                    // must be a local instance to avoid problems with concurrency (to be
145                    // thread safe)
146                    ValidatorErrorHandler handler = errorHandler.getClass().newInstance();
147                    validator.setErrorHandler(handler);
148    
149                    try {
150                        LOG.trace("Validating {}", source);
151                        validator.validate(source, result);
152                        handler.handleErrors(exchange, schema, result);
153                    } catch (SAXParseException e) {
154                        // can be thrown for non well formed XML
155                        throw new SchemaValidationException(exchange, schema, Collections.singletonList(e),
156                                Collections.<SAXParseException>emptyList(),
157                                Collections.<SAXParseException>emptyList());
158                    }
159                }
160            } finally {
161                IOHelper.close(is);
162            }
163        }
164    
165        private Object getContentToValidate(Exchange exchange) {
166            if (shouldUseHeader()) {
167                return exchange.getIn().getHeader(headerName);
168            } else {
169                return exchange.getIn().getBody();
170            }
171        }
172    
173        private <T> T getContentToValidate(Exchange exchange, Class<T> clazz) {
174            if (shouldUseHeader()) {
175                return exchange.getIn().getHeader(headerName, clazz);
176            } else {
177                return exchange.getIn().getBody(clazz);
178            }
179        }
180    
181        private boolean shouldUseHeader() {
182            return headerName != null;
183        }
184    
185        public void loadSchema() throws Exception {
186            // force loading of schema
187            schema = createSchema();
188        }
189    
190        // Properties
191        // -----------------------------------------------------------------------
192    
193        public Schema getSchema() throws IOException, SAXException {
194            if (schema == null) {
195                synchronized (this) {
196                    if (schema == null) {
197                        schema = createSchema();
198                    }
199                }
200            }
201            return schema;
202        }
203    
204        public void setSchema(Schema schema) {
205            this.schema = schema;
206        }
207    
208        public String getSchemaLanguage() {
209            return schemaLanguage;
210        }
211    
212        public void setSchemaLanguage(String schemaLanguage) {
213            this.schemaLanguage = schemaLanguage;
214        }
215    
216        public Source getSchemaSource() throws IOException {
217            if (schemaSource == null) {
218                schemaSource = createSchemaSource();
219            }
220            return schemaSource;
221        }
222    
223        public void setSchemaSource(Source schemaSource) {
224            this.schemaSource = schemaSource;
225        }
226    
227        public URL getSchemaUrl() {
228            return schemaUrl;
229        }
230    
231        public void setSchemaUrl(URL schemaUrl) {
232            this.schemaUrl = schemaUrl;
233        }
234    
235        public File getSchemaFile() {
236            return schemaFile;
237        }
238    
239        public void setSchemaFile(File schemaFile) {
240            this.schemaFile = schemaFile;
241        }
242    
243        public byte[] getSchemaAsByteArray() {
244            return schemaAsByteArray;
245        }
246    
247        public void setSchemaAsByteArray(byte[] schemaAsByteArray) {
248            this.schemaAsByteArray = schemaAsByteArray;
249        }
250    
251        public SchemaFactory getSchemaFactory() {
252            if (schemaFactory == null) {
253                synchronized (this) {
254                    if (schemaFactory == null) {
255                        schemaFactory = createSchemaFactory();
256                    }
257                }
258            }
259            return schemaFactory;
260        }
261    
262        public void setSchemaFactory(SchemaFactory schemaFactory) {
263            this.schemaFactory = schemaFactory;
264        }
265    
266        public ValidatorErrorHandler getErrorHandler() {
267            return errorHandler;
268        }
269    
270        public void setErrorHandler(ValidatorErrorHandler errorHandler) {
271            this.errorHandler = errorHandler;
272        }
273    
274        @Deprecated
275        public boolean isUseDom() {
276            return useDom;
277        }
278    
279        /**
280         * Sets whether DOMSource and DOMResult should be used.
281         *
282         * @param useDom true to use DOM otherwise
283         */
284        @Deprecated
285        public void setUseDom(boolean useDom) {
286            this.useDom = useDom;
287        }
288    
289        public boolean isUseSharedSchema() {
290            return useSharedSchema;
291        }
292    
293        public void setUseSharedSchema(boolean useSharedSchema) {
294            this.useSharedSchema = useSharedSchema;
295        }
296    
297        public LSResourceResolver getResourceResolver() {
298            return resourceResolver;
299        }
300    
301        public void setResourceResolver(LSResourceResolver resourceResolver) {
302            this.resourceResolver = resourceResolver;
303        }
304    
305        public boolean isFailOnNullBody() {
306            return failOnNullBody;
307        }
308    
309        public void setFailOnNullBody(boolean failOnNullBody) {
310            this.failOnNullBody = failOnNullBody;
311        }
312    
313        public boolean isFailOnNullHeader() {
314            return failOnNullHeader;
315        }
316    
317        public void setFailOnNullHeader(boolean failOnNullHeader) {
318            this.failOnNullHeader = failOnNullHeader;
319        }
320    
321        public String getHeaderName() {
322            return headerName;
323        }
324    
325        public void setHeaderName(String headerName) {
326            this.headerName = headerName;
327        }
328    
329        // Implementation methods
330        // -----------------------------------------------------------------------
331    
332        protected SchemaFactory createSchemaFactory() {
333            SchemaFactory factory = SchemaFactory.newInstance(schemaLanguage);
334            if (getResourceResolver() != null) {
335                factory.setResourceResolver(getResourceResolver());
336            }
337            return factory;
338        }
339    
340        protected Source createSchemaSource() throws IOException {
341            throw new IllegalArgumentException("You must specify either a schema, schemaFile, schemaSource or schemaUrl property");
342        }
343    
344        protected Schema createSchema() throws SAXException, IOException {
345            SchemaFactory factory = getSchemaFactory();
346    
347            URL url = getSchemaUrl();
348            if (url != null) {
349                synchronized (this) {
350                    return factory.newSchema(url);
351                }
352            }
353    
354            File file = getSchemaFile();
355            if (file != null) {
356                synchronized (this) {
357                    return factory.newSchema(file);
358                }
359            }
360    
361            byte[] bytes = getSchemaAsByteArray();
362            if (bytes != null) {
363                synchronized (this) {
364                    return factory.newSchema(new StreamSource(new ByteArrayInputStream(schemaAsByteArray)));
365                }
366            }
367    
368            Source source = getSchemaSource();
369            synchronized (this) {
370                return factory.newSchema(source);
371            }
372        }
373    
374        /**
375         * Checks whether we need an {@link InputStream} to access the message body or header.
376         * <p/>
377         * Depending on the content in the message body or header, we may not need to convert
378         * to {@link InputStream}.
379         *
380         * @param exchange the current exchange
381         * @return <tt>true</tt> to convert to {@link InputStream} beforehand converting to {@link Source} afterwards.
382         */
383        protected boolean isInputStreamNeeded(Exchange exchange) {
384            Object content = getContentToValidate(exchange);
385            if (content == null) {
386                return false;
387            }
388    
389            if (content instanceof InputStream) {
390                return true;
391            } else if (content instanceof Source) {
392                return false;
393            } else if (content instanceof String) {
394                return false;
395            } else if (content instanceof byte[]) {
396                return false;
397            } else if (content instanceof Node) {
398                return false;
399            } else if (exchange.getContext().getTypeConverterRegistry().lookup(Source.class, content.getClass()) != null) {
400                //there is a direct and hopefully optimized converter to Source
401                return false;
402            }
403            // yes an input stream is needed
404            return true;
405        }
406    
407        /**
408         * Converts the inbound body or header to a {@link Source}, if it is <b>not</b> already a {@link Source}.
409         * <p/>
410         * This implementation will prefer to source in the following order:
411         * <ul>
412         * <li>DOM - DOM if explicit configured to use DOM</li>
413         * <li>SAX - SAX as 2nd choice</li>
414         * <li>Stream - Stream as 3rd choice</li>
415         * <li>DOM - DOM as 4th choice</li>
416         * </ul>
417         */
418        protected Source getSource(Exchange exchange, Object content) {
419            if (isUseDom()) {
420                // force DOM
421                return exchange.getContext().getTypeConverter().tryConvertTo(DOMSource.class, exchange, content);
422            }
423    
424            // body or header may already be a source
425            if (content instanceof Source) {
426                return (Source) content;
427            }
428            Source source = null;
429            if (content instanceof InputStream) {
430                return new StreamSource((InputStream) content);
431            }
432            if (content != null) {
433                TypeConverter tc = exchange.getContext().getTypeConverterRegistry().lookup(Source.class, content.getClass());
434                if (tc != null) {
435                    source = tc.convertTo(Source.class, exchange, content);
436                }
437            }
438    
439            if (source == null) {
440                // then try SAX
441                source = exchange.getContext().getTypeConverter().tryConvertTo(SAXSource.class, exchange, content);
442            }
443            if (source == null) {
444                // then try stream
445                source = exchange.getContext().getTypeConverter().tryConvertTo(StreamSource.class, exchange, content);
446            }
447            if (source == null) {
448                // and fallback to DOM
449                source = exchange.getContext().getTypeConverter().tryConvertTo(DOMSource.class, exchange, content);
450            }
451            if (source == null) {
452                if (isFailOnNullBody()) {
453                    throw new ExpectedBodyTypeException(exchange, Source.class);
454                } else {
455                    try {
456                        source = converter.toDOMSource(converter.createDocument());
457                    } catch (ParserConfigurationException e) {
458                        throw new RuntimeTransformException(e);
459                    }
460                }
461            }
462            return source;
463        }
464    
465    }