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}