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.parsers.ParserConfigurationException;
026import javax.xml.transform.Result;
027import javax.xml.transform.Source;
028import javax.xml.transform.dom.DOMResult;
029import javax.xml.transform.dom.DOMSource;
030import javax.xml.transform.sax.SAXResult;
031import javax.xml.transform.sax.SAXSource;
032import javax.xml.transform.stax.StAXSource;
033import javax.xml.transform.stream.StreamSource;
034import javax.xml.validation.Schema;
035import javax.xml.validation.SchemaFactory;
036import javax.xml.validation.Validator;
037
038import org.w3c.dom.Node;
039import org.w3c.dom.ls.LSResourceResolver;
040
041import org.xml.sax.SAXException;
042import org.xml.sax.SAXParseException;
043
044import org.apache.camel.AsyncCallback;
045import org.apache.camel.AsyncProcessor;
046import org.apache.camel.Exchange;
047import org.apache.camel.ExpectedBodyTypeException;
048import org.apache.camel.RuntimeTransformException;
049import org.apache.camel.TypeConverter;
050import org.apache.camel.converter.jaxp.XmlConverter;
051import org.apache.camel.util.AsyncProcessorHelper;
052import org.apache.camel.util.IOHelper;
053import org.slf4j.Logger;
054import org.slf4j.LoggerFactory;
055
056/**
057 * A processor which validates the XML version of the inbound message body
058 * against some schema either in XSD or RelaxNG
059 */
060public class ValidatingProcessor implements AsyncProcessor {
061    private static final Logger LOG = LoggerFactory.getLogger(ValidatingProcessor.class);
062    private final SchemaReader schemaReader;
063    private ValidatorErrorHandler errorHandler = new DefaultValidationErrorHandler();
064    private final XmlConverter converter = new XmlConverter();
065    private boolean useDom;
066    private boolean useSharedSchema = true;
067    private boolean failOnNullBody = true;
068    private boolean failOnNullHeader = true;
069    private String headerName;
070
071    public ValidatingProcessor() {
072        schemaReader = new SchemaReader();
073    }
074
075    public ValidatingProcessor(SchemaReader schemaReader) {
076        // schema reader can be a singelton per schema, therefore make reuse, see ValidatorEndpoint and ValidatorProducer
077        this.schemaReader = schemaReader;
078    }
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        schemaReader.loadSchema();
186    }
187
188    // Properties
189    // -----------------------------------------------------------------------
190
191    public Schema getSchema() throws IOException, SAXException {
192        return schemaReader.getSchema();
193    }
194
195    public void setSchema(Schema schema) {
196        schemaReader.setSchema(schema);
197    }
198
199    public String getSchemaLanguage() {
200        return schemaReader.getSchemaLanguage();
201    }
202
203    public void setSchemaLanguage(String schemaLanguage) {
204        schemaReader.setSchemaLanguage(schemaLanguage);
205    }
206
207    public Source getSchemaSource() throws IOException {
208        return schemaReader.getSchemaSource();
209    }
210
211    public void setSchemaSource(Source schemaSource) {
212        schemaReader.setSchemaSource(schemaSource);
213    }
214
215    public URL getSchemaUrl() {
216        return schemaReader.getSchemaUrl();
217    }
218
219    public void setSchemaUrl(URL schemaUrl) {
220        schemaReader.setSchemaUrl(schemaUrl);
221    }
222
223    public File getSchemaFile() {
224        return schemaReader.getSchemaFile();
225    }
226
227    public void setSchemaFile(File schemaFile) {
228        schemaReader.setSchemaFile(schemaFile);
229    }
230
231    public byte[] getSchemaAsByteArray() {
232        return schemaReader.getSchemaAsByteArray();
233    }
234
235    public void setSchemaAsByteArray(byte[] schemaAsByteArray) {
236        schemaReader.setSchemaAsByteArray(schemaAsByteArray);
237    }
238
239    public SchemaFactory getSchemaFactory() {
240        return schemaReader.getSchemaFactory();
241    }
242
243    public void setSchemaFactory(SchemaFactory schemaFactory) {
244        schemaReader.setSchemaFactory(schemaFactory);
245    }
246
247    public ValidatorErrorHandler getErrorHandler() {
248        return errorHandler;
249    }
250
251    public void setErrorHandler(ValidatorErrorHandler errorHandler) {
252        this.errorHandler = errorHandler;
253    }
254
255    @Deprecated
256    public boolean isUseDom() {
257        return useDom;
258    }
259
260    /**
261     * Sets whether DOMSource and DOMResult should be used.
262     *
263     * @param useDom true to use DOM otherwise
264     */
265    @Deprecated
266    public void setUseDom(boolean useDom) {
267        this.useDom = useDom;
268    }
269
270    public boolean isUseSharedSchema() {
271        return useSharedSchema;
272    }
273
274    public void setUseSharedSchema(boolean useSharedSchema) {
275        this.useSharedSchema = useSharedSchema;
276    }
277
278    public LSResourceResolver getResourceResolver() {
279        return schemaReader.getResourceResolver();
280    }
281
282    public void setResourceResolver(LSResourceResolver resourceResolver) {
283        schemaReader.setResourceResolver(resourceResolver);
284    }
285
286    public boolean isFailOnNullBody() {
287        return failOnNullBody;
288    }
289
290    public void setFailOnNullBody(boolean failOnNullBody) {
291        this.failOnNullBody = failOnNullBody;
292    }
293
294    public boolean isFailOnNullHeader() {
295        return failOnNullHeader;
296    }
297
298    public void setFailOnNullHeader(boolean failOnNullHeader) {
299        this.failOnNullHeader = failOnNullHeader;
300    }
301
302    public String getHeaderName() {
303        return headerName;
304    }
305
306    public void setHeaderName(String headerName) {
307        this.headerName = headerName;
308    }
309
310    // Implementation methods
311    // -----------------------------------------------------------------------
312
313    protected SchemaFactory createSchemaFactory() {
314        return schemaReader.createSchemaFactory();
315    }
316
317    protected Source createSchemaSource() throws IOException {
318        return schemaReader.createSchemaSource();
319    }
320
321    protected Schema createSchema() throws SAXException, IOException {
322        return schemaReader.createSchema();
323    }
324
325    /**
326     * Checks whether we need an {@link InputStream} to access the message body or header.
327     * <p/>
328     * Depending on the content in the message body or header, we may not need to convert
329     * to {@link InputStream}.
330     *
331     * @param exchange the current exchange
332     * @return <tt>true</tt> to convert to {@link InputStream} beforehand converting to {@link Source} afterwards.
333     */
334    protected boolean isInputStreamNeeded(Exchange exchange) {
335        Object content = getContentToValidate(exchange);
336        if (content == null) {
337            return false;
338        }
339
340        if (content instanceof InputStream) {
341            return true;
342        } else if (content instanceof Source) {
343            return false;
344        } else if (content instanceof String) {
345            return false;
346        } else if (content instanceof byte[]) {
347            return false;
348        } else if (content instanceof Node) {
349            return false;
350        } else if (exchange.getContext().getTypeConverterRegistry().lookup(Source.class, content.getClass()) != null) {
351            //there is a direct and hopefully optimized converter to Source
352            return false;
353        }
354        // yes an input stream is needed
355        return true;
356    }
357
358    /**
359     * Converts the inbound body or header to a {@link Source}, if it is <b>not</b> already a {@link Source}.
360     * <p/>
361     * This implementation will prefer to source in the following order:
362     * <ul>
363     * <li>DOM - DOM if explicit configured to use DOM</li>
364     * <li>SAX - SAX as 2nd choice</li>
365     * <li>Stream - Stream as 3rd choice</li>
366     * <li>DOM - DOM as 4th choice</li>
367     * </ul>
368     */
369    protected Source getSource(Exchange exchange, Object content) {
370        if (isUseDom()) {
371            // force DOM
372            return exchange.getContext().getTypeConverter().tryConvertTo(DOMSource.class, exchange, content);
373        }
374
375        // body or header may already be a source
376        if (content instanceof Source) {
377            return (Source) content;
378        }
379        Source source = null;
380        if (content instanceof InputStream) {
381            return new StreamSource((InputStream) content);
382        }
383        if (content != null) {
384            TypeConverter tc = exchange.getContext().getTypeConverterRegistry().lookup(Source.class, content.getClass());
385            if (tc != null) {
386                source = tc.convertTo(Source.class, exchange, content);
387            }
388        }
389
390        if (source == null) {
391            // then try SAX
392            source = exchange.getContext().getTypeConverter().tryConvertTo(SAXSource.class, exchange, content);
393        }
394        if (source == null) {
395            // then try stream
396            source = exchange.getContext().getTypeConverter().tryConvertTo(StreamSource.class, exchange, content);
397        }
398        if (source == null) {
399            // and fallback to DOM
400            source = exchange.getContext().getTypeConverter().tryConvertTo(DOMSource.class, exchange, content);
401        }
402        if (source == null) {
403            if (isFailOnNullBody()) {
404                throw new ExpectedBodyTypeException(exchange, Source.class);
405            } else {
406                try {
407                    source = converter.toDOMSource(converter.createDocument());
408                } catch (ParserConfigurationException e) {
409                    throw new RuntimeTransformException(e);
410                }
411            }
412        }
413        return source;
414    }
415
416}