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