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 */
017package org.apache.camel.processor.validation;
018
019import java.io.ByteArrayInputStream;
020import java.io.File;
021import java.io.IOException;
022import java.io.InputStream;
023import java.net.URL;
024import java.util.Collections;
025
026import javax.xml.XMLConstants;
027import javax.xml.parsers.ParserConfigurationException;
028import javax.xml.transform.Result;
029import javax.xml.transform.Source;
030import javax.xml.transform.dom.DOMResult;
031import javax.xml.transform.dom.DOMSource;
032import javax.xml.transform.sax.SAXResult;
033import javax.xml.transform.sax.SAXSource;
034import javax.xml.transform.stax.StAXSource;
035import javax.xml.transform.stream.StreamSource;
036import javax.xml.validation.Schema;
037import javax.xml.validation.SchemaFactory;
038import javax.xml.validation.Validator;
039
040import org.w3c.dom.Node;
041import org.w3c.dom.ls.LSResourceResolver;
042
043import org.xml.sax.SAXException;
044import org.xml.sax.SAXParseException;
045
046import org.apache.camel.AsyncCallback;
047import org.apache.camel.AsyncProcessor;
048import org.apache.camel.Exchange;
049import org.apache.camel.ExpectedBodyTypeException;
050import org.apache.camel.RuntimeTransformException;
051import org.apache.camel.TypeConverter;
052import org.apache.camel.converter.jaxp.XmlConverter;
053import org.apache.camel.util.AsyncProcessorHelper;
054import org.apache.camel.util.IOHelper;
055import org.slf4j.Logger;
056import 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 */
063public 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}