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}