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.File;
020import java.io.IOException;
021import java.io.InputStream;
022import java.net.URL;
023import java.util.Collections;
024
025import javax.xml.XMLConstants;
026import javax.xml.parsers.ParserConfigurationException;
027import javax.xml.transform.Result;
028import javax.xml.transform.Source;
029import javax.xml.transform.dom.DOMResult;
030import javax.xml.transform.dom.DOMSource;
031import javax.xml.transform.sax.SAXResult;
032import javax.xml.transform.sax.SAXSource;
033import javax.xml.transform.stax.StAXSource;
034import javax.xml.transform.stream.StreamSource;
035import javax.xml.validation.Schema;
036import javax.xml.validation.SchemaFactory;
037import javax.xml.validation.Validator;
038
039import org.w3c.dom.Node;
040import org.w3c.dom.ls.LSResourceResolver;
041
042import org.xml.sax.SAXException;
043import org.xml.sax.SAXParseException;
044
045import org.apache.camel.AsyncCallback;
046import org.apache.camel.AsyncProcessor;
047import org.apache.camel.Exchange;
048import org.apache.camel.ExpectedBodyTypeException;
049import org.apache.camel.RuntimeTransformException;
050import org.apache.camel.TypeConverter;
051import org.apache.camel.converter.jaxp.XmlConverter;
052import org.apache.camel.util.AsyncProcessorHelper;
053import org.apache.camel.util.IOHelper;
054import org.slf4j.Logger;
055import org.slf4j.LoggerFactory;
056
057import static org.apache.camel.processor.validation.SchemaReader.ACCESS_EXTERNAL_DTD;
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 final SchemaReader schemaReader;
066    private ValidatorErrorHandler errorHandler = new DefaultValidationErrorHandler();
067    private final XmlConverter converter = new XmlConverter();
068    private boolean useDom;
069    private boolean useSharedSchema = true;
070    private boolean failOnNullBody = true;
071    private boolean failOnNullHeader = true;
072    private String headerName;
073
074    public ValidatingProcessor() {
075        schemaReader = new SchemaReader();
076    }
077
078    public ValidatingProcessor(SchemaReader schemaReader) {
079        // schema reader can be a singelton per schema, therefore make reuse, see ValidatorEndpoint and ValidatorProducer
080        this.schemaReader = schemaReader;
081    }
082
083    public void process(Exchange exchange) throws Exception {
084        AsyncProcessorHelper.process(this, exchange);
085    }
086
087    public boolean process(Exchange exchange, AsyncCallback callback) {
088        try {
089            doProcess(exchange);
090        } catch (Exception e) {
091            exchange.setException(e);
092        }
093        callback.done(true);
094        return true;
095    }
096
097    protected void doProcess(Exchange exchange) throws Exception {
098        Schema schema;
099        if (isUseSharedSchema()) {
100            schema = getSchema();
101        } else {
102            schema = createSchema();
103        }
104
105        Validator validator = schema.newValidator();
106        // turn off access to external schema by default
107        if (!Boolean.parseBoolean(exchange.getContext().getGlobalOptions().get(ACCESS_EXTERNAL_DTD))) {
108            try {
109                LOG.debug("Configuring Validator to not allow access to external DTD/Schema");
110                validator.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
111                validator.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
112            } catch (SAXException e) {
113                LOG.warn(e.getMessage(), e);
114            }
115        }
116
117        // the underlying input stream, which we need to close to avoid locking files or other resources
118        Source source = null;
119        InputStream is = null;
120        try {
121            Result result = null;
122            // only convert to input stream if really needed
123            if (isInputStreamNeeded(exchange)) {
124                is = getContentToValidate(exchange, InputStream.class);
125                if (is != null) {
126                    source = getSource(exchange, is);
127                }
128            } else {
129                Object content = getContentToValidate(exchange);
130                if (content != null) {
131                    source = getSource(exchange, content);
132                }
133            }
134
135            if (shouldUseHeader()) {
136                if (source == null && isFailOnNullHeader()) {
137                    throw new NoXmlHeaderValidationException(exchange, headerName);
138                }
139            } else {
140                if (source == null && isFailOnNullBody()) {
141                    throw new NoXmlBodyValidationException(exchange);
142                }
143            }
144
145            //CAMEL-7036 We don't need to set the result if the source is an instance of StreamSource
146            if (source instanceof DOMSource) {
147                result = new DOMResult();
148            } else if (source instanceof SAXSource) {
149                result = new SAXResult();
150            } else if (source instanceof StAXSource || source instanceof StreamSource) {
151                result = null;
152            }
153
154            if (source != null) {
155                // create a new errorHandler and set it on the validator
156                // must be a local instance to avoid problems with concurrency (to be
157                // thread safe)
158                ValidatorErrorHandler handler = errorHandler.getClass().newInstance();
159                validator.setErrorHandler(handler);
160
161                try {
162                    LOG.trace("Validating {}", source);
163                    validator.validate(source, result);
164                    handler.handleErrors(exchange, schema, result);
165                } catch (SAXParseException e) {
166                    // can be thrown for non well formed XML
167                    throw new SchemaValidationException(exchange, schema, Collections.singletonList(e),
168                            Collections.<SAXParseException>emptyList(),
169                            Collections.<SAXParseException>emptyList());
170                }
171            }
172        } finally {
173            IOHelper.close(is);
174        }
175    }
176
177    private Object getContentToValidate(Exchange exchange) {
178        if (shouldUseHeader()) {
179            return exchange.getIn().getHeader(headerName);
180        } else {
181            return exchange.getIn().getBody();
182        }
183    }
184
185    private <T> T getContentToValidate(Exchange exchange, Class<T> clazz) {
186        if (shouldUseHeader()) {
187            return exchange.getIn().getHeader(headerName, clazz);
188        } else {
189            return exchange.getIn().getBody(clazz);
190        }
191    }
192
193    private boolean shouldUseHeader() {
194        return headerName != null;
195    }
196
197    public void loadSchema() throws Exception {
198        schemaReader.loadSchema();
199    }
200
201    // Properties
202    // -----------------------------------------------------------------------
203
204    public Schema getSchema() throws IOException, SAXException {
205        return schemaReader.getSchema();
206    }
207
208    public void setSchema(Schema schema) {
209        schemaReader.setSchema(schema);
210    }
211
212    public String getSchemaLanguage() {
213        return schemaReader.getSchemaLanguage();
214    }
215
216    public void setSchemaLanguage(String schemaLanguage) {
217        schemaReader.setSchemaLanguage(schemaLanguage);
218    }
219
220    public Source getSchemaSource() throws IOException {
221        return schemaReader.getSchemaSource();
222    }
223
224    public void setSchemaSource(Source schemaSource) {
225        schemaReader.setSchemaSource(schemaSource);
226    }
227
228    public URL getSchemaUrl() {
229        return schemaReader.getSchemaUrl();
230    }
231
232    public void setSchemaUrl(URL schemaUrl) {
233        schemaReader.setSchemaUrl(schemaUrl);
234    }
235
236    public File getSchemaFile() {
237        return schemaReader.getSchemaFile();
238    }
239
240    public void setSchemaFile(File schemaFile) {
241        schemaReader.setSchemaFile(schemaFile);
242    }
243
244    public byte[] getSchemaAsByteArray() {
245        return schemaReader.getSchemaAsByteArray();
246    }
247
248    public void setSchemaAsByteArray(byte[] schemaAsByteArray) {
249        schemaReader.setSchemaAsByteArray(schemaAsByteArray);
250    }
251
252    public SchemaFactory getSchemaFactory() {
253        return schemaReader.getSchemaFactory();
254    }
255
256    public void setSchemaFactory(SchemaFactory schemaFactory) {
257        schemaReader.setSchemaFactory(schemaFactory);
258    }
259
260    public ValidatorErrorHandler getErrorHandler() {
261        return errorHandler;
262    }
263
264    public void setErrorHandler(ValidatorErrorHandler errorHandler) {
265        this.errorHandler = errorHandler;
266    }
267
268    @Deprecated
269    public boolean isUseDom() {
270        return useDom;
271    }
272
273    /**
274     * Sets whether DOMSource and DOMResult should be used.
275     *
276     * @param useDom true to use DOM otherwise
277     */
278    @Deprecated
279    public void setUseDom(boolean useDom) {
280        this.useDom = useDom;
281    }
282
283    public boolean isUseSharedSchema() {
284        return useSharedSchema;
285    }
286
287    public void setUseSharedSchema(boolean useSharedSchema) {
288        this.useSharedSchema = useSharedSchema;
289    }
290
291    public LSResourceResolver getResourceResolver() {
292        return schemaReader.getResourceResolver();
293    }
294
295    public void setResourceResolver(LSResourceResolver resourceResolver) {
296        schemaReader.setResourceResolver(resourceResolver);
297    }
298
299    public boolean isFailOnNullBody() {
300        return failOnNullBody;
301    }
302
303    public void setFailOnNullBody(boolean failOnNullBody) {
304        this.failOnNullBody = failOnNullBody;
305    }
306
307    public boolean isFailOnNullHeader() {
308        return failOnNullHeader;
309    }
310
311    public void setFailOnNullHeader(boolean failOnNullHeader) {
312        this.failOnNullHeader = failOnNullHeader;
313    }
314
315    public String getHeaderName() {
316        return headerName;
317    }
318
319    public void setHeaderName(String headerName) {
320        this.headerName = headerName;
321    }
322
323    // Implementation methods
324    // -----------------------------------------------------------------------
325
326    protected SchemaFactory createSchemaFactory() {
327        return schemaReader.createSchemaFactory();
328    }
329
330    protected Source createSchemaSource() throws IOException {
331        return schemaReader.createSchemaSource();
332    }
333
334    protected Schema createSchema() throws SAXException, IOException {
335        return schemaReader.createSchema();
336    }
337
338    /**
339     * Checks whether we need an {@link InputStream} to access the message body or header.
340     * <p/>
341     * Depending on the content in the message body or header, we may not need to convert
342     * to {@link InputStream}.
343     *
344     * @param exchange the current exchange
345     * @return <tt>true</tt> to convert to {@link InputStream} beforehand converting to {@link Source} afterwards.
346     */
347    protected boolean isInputStreamNeeded(Exchange exchange) {
348        Object content = getContentToValidate(exchange);
349        if (content == null) {
350            return false;
351        }
352
353        if (content instanceof InputStream) {
354            return true;
355        } else if (content instanceof Source) {
356            return false;
357        } else if (content instanceof String) {
358            return false;
359        } else if (content instanceof byte[]) {
360            return false;
361        } else if (content instanceof Node) {
362            return false;
363        } else if (exchange.getContext().getTypeConverterRegistry().lookup(Source.class, content.getClass()) != null) {
364            //there is a direct and hopefully optimized converter to Source
365            return false;
366        }
367        // yes an input stream is needed
368        return true;
369    }
370
371    /**
372     * Converts the inbound body or header to a {@link Source}, if it is <b>not</b> already a {@link Source}.
373     * <p/>
374     * This implementation will prefer to source in the following order:
375     * <ul>
376     * <li>DOM - DOM if explicit configured to use DOM</li>
377     * <li>SAX - SAX as 2nd choice</li>
378     * <li>Stream - Stream as 3rd choice</li>
379     * <li>DOM - DOM as 4th choice</li>
380     * </ul>
381     */
382    protected Source getSource(Exchange exchange, Object content) {
383        if (isUseDom()) {
384            // force DOM
385            return exchange.getContext().getTypeConverter().tryConvertTo(DOMSource.class, exchange, content);
386        }
387
388        // body or header may already be a source
389        if (content instanceof Source) {
390            return (Source) content;
391        }
392        Source source = null;
393        if (content instanceof InputStream) {
394            return new StreamSource((InputStream) content);
395        }
396        if (content != null) {
397            TypeConverter tc = exchange.getContext().getTypeConverterRegistry().lookup(Source.class, content.getClass());
398            if (tc != null) {
399                source = tc.convertTo(Source.class, exchange, content);
400            }
401        }
402
403        if (source == null) {
404            // then try SAX
405            source = exchange.getContext().getTypeConverter().tryConvertTo(SAXSource.class, exchange, content);
406        }
407        if (source == null) {
408            // then try stream
409            source = exchange.getContext().getTypeConverter().tryConvertTo(StreamSource.class, exchange, content);
410        }
411        if (source == null) {
412            // and fallback to DOM
413            source = exchange.getContext().getTypeConverter().tryConvertTo(DOMSource.class, exchange, content);
414        }
415        if (source == null) {
416            if (isFailOnNullBody()) {
417                throw new ExpectedBodyTypeException(exchange, Source.class);
418            } else {
419                try {
420                    source = converter.toDOMSource(converter.createDocument());
421                } catch (ParserConfigurationException e) {
422                    throw new RuntimeTransformException(e);
423                }
424            }
425        }
426        return source;
427    }
428
429}