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.File;
020    import java.io.IOException;
021    import java.io.InputStream;
022    import java.io.StringWriter;
023    import java.net.URL;
024    import java.util.Collections;
025    import javax.xml.XMLConstants;
026    import javax.xml.parsers.ParserConfigurationException;
027    import javax.xml.transform.Result;
028    import javax.xml.transform.Source;
029    import javax.xml.transform.dom.DOMResult;
030    import javax.xml.transform.dom.DOMSource;
031    import javax.xml.transform.sax.SAXResult;
032    import javax.xml.transform.sax.SAXSource;
033    import javax.xml.transform.stax.StAXSource;
034    import javax.xml.transform.stream.StreamResult;
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    import org.xml.sax.SAXException;
043    import org.xml.sax.SAXParseException;
044    
045    import org.apache.camel.Exchange;
046    import org.apache.camel.ExpectedBodyTypeException;
047    import org.apache.camel.Processor;
048    import org.apache.camel.RuntimeTransformException;
049    import org.apache.camel.TypeConverter;
050    import org.apache.camel.converter.jaxp.XmlConverter;
051    import org.apache.camel.util.IOHelper;
052    import org.slf4j.Logger;
053    import org.slf4j.LoggerFactory;
054    
055    /**
056     * A processor which validates the XML version of the inbound message body
057     * against some schema either in XSD or RelaxNG
058     * 
059     * @version
060     */
061    public class ValidatingProcessor implements Processor {
062        private static final Logger LOG = LoggerFactory.getLogger(ValidatingProcessor.class);
063        private XmlConverter converter = new XmlConverter();
064        private String schemaLanguage = XMLConstants.W3C_XML_SCHEMA_NS_URI;
065        private Schema schema;
066        private Source schemaSource;
067        private SchemaFactory schemaFactory;
068        private URL schemaUrl;
069        private File schemaFile;
070        private ValidatorErrorHandler errorHandler = new DefaultValidationErrorHandler();
071        private boolean useDom;
072        private boolean useSharedSchema = true;
073        private LSResourceResolver resourceResolver;
074        private boolean failOnNullBody = true;
075    
076        public void process(Exchange exchange) throws Exception {
077            Schema schema;
078            if (isUseSharedSchema()) {
079                schema = getSchema();
080            } else {
081                schema = createSchema();
082            }
083    
084            Validator validator = schema.newValidator();
085    
086            // the underlying input stream, which we need to close to avoid locking files or other resources
087            Source source = null;
088            InputStream is = null;
089            try {
090                Result result = null;
091                // only convert to input stream if really needed
092                if (isInputStreamNeeded(exchange)) {
093                    is = exchange.getIn().getBody(InputStream.class);
094                    if (is != null) {
095                        source = getSource(exchange, is);
096                    }
097                } else {
098                    Object body = exchange.getIn().getBody();
099                    if (body != null) {
100                        source = getSource(exchange, body);
101                    }
102                }
103    
104                if (source == null && isFailOnNullBody()) {
105                    throw new NoXmlBodyValidationException(exchange);
106                }
107    
108                if (source instanceof DOMSource) {
109                    result = new DOMResult();
110                } else if (source instanceof StreamSource) {
111                    result = new StreamResult(new StringWriter());
112                } else if (source instanceof SAXSource) {
113                    result = new SAXResult();
114                } else if (source instanceof StAXSource) {
115                    result = null;
116                }
117    
118                if (source != null) {
119                    // create a new errorHandler and set it on the validator
120                    // must be a local instance to avoid problems with concurrency (to be
121                    // thread safe)
122                    ValidatorErrorHandler handler = errorHandler.getClass().newInstance();
123                    validator.setErrorHandler(handler);
124    
125                    try {
126                        LOG.trace("Validating {}", source);
127                        validator.validate(source, result);
128                        handler.handleErrors(exchange, schema, result);
129                    } catch (SAXParseException e) {
130                        // can be thrown for non well formed XML
131                        throw new SchemaValidationException(exchange, schema, Collections.singletonList(e),
132                                Collections.<SAXParseException> emptyList(),
133                                Collections.<SAXParseException> emptyList());
134                    }
135                }
136            } finally {
137                IOHelper.close(is);
138            }
139        }
140    
141        public void loadSchema() throws Exception {
142            // force loading of schema
143            schema = createSchema();
144        }
145    
146        // Properties
147        // -----------------------------------------------------------------------
148    
149        public Schema getSchema() throws IOException, SAXException {
150            if (schema == null) {
151                schema = createSchema();
152            }
153            return schema;
154        }
155    
156        public void setSchema(Schema schema) {
157            this.schema = schema;
158        }
159    
160        public String getSchemaLanguage() {
161            return schemaLanguage;
162        }
163    
164        public void setSchemaLanguage(String schemaLanguage) {
165            this.schemaLanguage = schemaLanguage;
166        }
167    
168        public Source getSchemaSource() throws IOException {
169            if (schemaSource == null) {
170                schemaSource = createSchemaSource();
171            }
172            return schemaSource;
173        }
174    
175        public void setSchemaSource(Source schemaSource) {
176            this.schemaSource = schemaSource;
177        }
178    
179        public URL getSchemaUrl() {
180            return schemaUrl;
181        }
182    
183        public void setSchemaUrl(URL schemaUrl) {
184            this.schemaUrl = schemaUrl;
185        }
186    
187        public File getSchemaFile() {
188            return schemaFile;
189        }
190    
191        public void setSchemaFile(File schemaFile) {
192            this.schemaFile = schemaFile;
193        }
194    
195        public SchemaFactory getSchemaFactory() {
196            if (schemaFactory == null) {
197                schemaFactory = createSchemaFactory();
198            }
199            return schemaFactory;
200        }
201    
202        public void setSchemaFactory(SchemaFactory schemaFactory) {
203            this.schemaFactory = schemaFactory;
204        }
205    
206        public ValidatorErrorHandler getErrorHandler() {
207            return errorHandler;
208        }
209    
210        public void setErrorHandler(ValidatorErrorHandler errorHandler) {
211            this.errorHandler = errorHandler;
212        }
213    
214        @Deprecated
215        public boolean isUseDom() {
216            return useDom;
217        }
218    
219        /**
220         * Sets whether DOMSource and DOMResult should be used.
221         *
222         * @param useDom true to use DOM otherwise
223         */
224        @Deprecated
225        public void setUseDom(boolean useDom) {
226            this.useDom = useDom;
227        }
228    
229        public boolean isUseSharedSchema() {
230            return useSharedSchema;
231        }
232    
233        public void setUseSharedSchema(boolean useSharedSchema) {
234            this.useSharedSchema = useSharedSchema;
235        }
236    
237        public LSResourceResolver getResourceResolver() {
238            return resourceResolver;
239        }
240    
241        public void setResourceResolver(LSResourceResolver resourceResolver) {
242            this.resourceResolver = resourceResolver;
243        }
244    
245        public boolean isFailOnNullBody() {
246            return failOnNullBody;
247        }
248    
249        public void setFailOnNullBody(boolean failOnNullBody) {
250            this.failOnNullBody = failOnNullBody;
251        }
252    
253        // Implementation methods
254        // -----------------------------------------------------------------------
255    
256        protected SchemaFactory createSchemaFactory() {
257            SchemaFactory factory = SchemaFactory.newInstance(schemaLanguage);
258            if (getResourceResolver() != null) {
259                factory.setResourceResolver(getResourceResolver());
260            }
261            return factory;
262        }
263    
264        protected Source createSchemaSource() throws IOException {
265            throw new IllegalArgumentException("You must specify either a schema, schemaFile, schemaSource or schemaUrl property");
266        }
267    
268        protected Schema createSchema() throws SAXException, IOException {
269            SchemaFactory factory = getSchemaFactory();
270    
271            URL url = getSchemaUrl();
272            if (url != null) {
273                return factory.newSchema(url);
274            }
275            File file = getSchemaFile();
276            if (file != null) {
277                return factory.newSchema(file);
278            }
279            return factory.newSchema(getSchemaSource());
280        }
281    
282        /**
283         * Checks whether we need an {@link InputStream} to access the message body.
284         * <p/>
285         * Depending on the content in the message body, we may not need to convert
286         * to {@link InputStream}.
287         *
288         * @param exchange the current exchange
289         * @return <tt>true</tt> to convert to {@link InputStream} beforehand converting to {@link Source} afterwards.
290         */
291        protected boolean isInputStreamNeeded(Exchange exchange) {
292            Object body = exchange.getIn().getBody();
293            if (body == null) {
294                return false;
295            }
296    
297            if (body instanceof InputStream) {
298                return true;
299            } else if (body instanceof Source) {
300                return false;
301            } else if (body instanceof String) {
302                return false;
303            } else if (body instanceof byte[]) {
304                return false;
305            } else if (body instanceof Node) {
306                return false;
307            } else if (exchange.getContext().getTypeConverterRegistry().lookup(Source.class, body.getClass()) != null) {
308                //there is a direct and hopefully optimized converter to Source
309                return false;
310            }
311            // yes an input stream is needed
312            return true;
313        }
314    
315        /**
316         * Converts the inbound body to a {@link Source}, if the body is <b>not</b> already a {@link Source}.
317         * <p/>
318         * This implementation will prefer to source in the following order:
319         * <ul>
320         *   <li>DOM - DOM if explicit configured to use DOM</li>
321         *   <li>SAX - SAX as 2nd choice</li>
322         *   <li>Stream - Stream as 3rd choice</li>
323         *   <li>DOM - DOM as 4th choice</li>
324         * </ul>
325         */
326        protected Source getSource(Exchange exchange, Object body) {
327            if (isUseDom()) {
328                // force DOM
329                return exchange.getContext().getTypeConverter().tryConvertTo(DOMSource.class, exchange, body);
330            }
331    
332            // body may already be a source
333            if (body instanceof Source) {
334                return (Source) body;
335            }
336            Source source = null;
337            if (body instanceof InputStream) {
338                return new StreamSource((InputStream)body);
339            }
340            if (body != null) {
341                TypeConverter tc = exchange.getContext().getTypeConverterRegistry().lookup(Source.class, body.getClass());
342                if (tc != null) {
343                    source = tc.convertTo(Source.class, exchange, body);
344                }
345            }
346    
347            if (source == null) {
348                // then try SAX
349                source = exchange.getContext().getTypeConverter().tryConvertTo(SAXSource.class, exchange, body);
350            }
351            if (source == null) {
352                // then try stream
353                source = exchange.getContext().getTypeConverter().tryConvertTo(StreamSource.class, exchange, body);
354            }
355            if (source == null) {
356                // and fallback to DOM
357                source = exchange.getContext().getTypeConverter().tryConvertTo(DOMSource.class, exchange, body);
358            }
359            if (source == null) {
360                if (isFailOnNullBody()) {
361                    throw new ExpectedBodyTypeException(exchange, Source.class);
362                } else {
363                    try {
364                        source = converter.toDOMSource(converter.createDocument());
365                    } catch (ParserConfigurationException e) {
366                        throw new RuntimeTransformException(e);
367                    }
368                }
369            }
370            return source;
371        }
372    
373    }