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.builder.xml; 018 019import java.io.File; 020import java.io.InputStream; 021import java.util.HashSet; 022import java.util.LinkedHashMap; 023import java.util.List; 024import java.util.Map; 025import java.util.Properties; 026import java.util.Queue; 027import java.util.concurrent.ConcurrentHashMap; 028import java.util.concurrent.ConcurrentLinkedQueue; 029import javax.xml.namespace.QName; 030import javax.xml.transform.dom.DOMSource; 031import javax.xml.xpath.XPath; 032import javax.xml.xpath.XPathConstants; 033import javax.xml.xpath.XPathExpression; 034import javax.xml.xpath.XPathExpressionException; 035import javax.xml.xpath.XPathFactory; 036import javax.xml.xpath.XPathFactoryConfigurationException; 037import javax.xml.xpath.XPathFunction; 038import javax.xml.xpath.XPathFunctionException; 039import javax.xml.xpath.XPathFunctionResolver; 040 041import org.w3c.dom.Document; 042import org.w3c.dom.Node; 043import org.w3c.dom.NodeList; 044import org.xml.sax.InputSource; 045 046import org.apache.camel.CamelContext; 047import org.apache.camel.Exchange; 048import org.apache.camel.Expression; 049import org.apache.camel.NoTypeConversionAvailableException; 050import org.apache.camel.Predicate; 051import org.apache.camel.RuntimeExpressionException; 052import org.apache.camel.WrappedFile; 053import org.apache.camel.impl.DefaultExchange; 054import org.apache.camel.spi.Language; 055import org.apache.camel.spi.NamespaceAware; 056import org.apache.camel.support.ServiceSupport; 057import org.apache.camel.util.ExchangeHelper; 058import org.apache.camel.util.IOHelper; 059import org.apache.camel.util.MessageHelper; 060import org.apache.camel.util.ObjectHelper; 061import org.slf4j.Logger; 062import org.slf4j.LoggerFactory; 063 064import static org.apache.camel.builder.xml.Namespaces.DEFAULT_NAMESPACE; 065import static org.apache.camel.builder.xml.Namespaces.FUNCTION_NAMESPACE; 066import static org.apache.camel.builder.xml.Namespaces.IN_NAMESPACE; 067import static org.apache.camel.builder.xml.Namespaces.OUT_NAMESPACE; 068import static org.apache.camel.builder.xml.Namespaces.isMatchingNamespaceOrEmptyNamespace; 069 070/** 071 * Creates an XPath expression builder which creates a nodeset result by default. 072 * If you want to evaluate a String expression then call {@link #stringResult()} 073 * <p/> 074 * An XPath object is not thread-safe and not reentrant. In other words, it is the application's responsibility to make 075 * sure that one XPath object is not used from more than one thread at any given time, and while the evaluate method 076 * is invoked, applications may not recursively call the evaluate method. 077 * <p/> 078 * This implementation is thread safe by using thread locals and pooling to allow concurrency. 079 * <p/> 080 * <b>Important:</b> After configuring the {@link XPathBuilder} its advised to invoke {@link #start()} 081 * to prepare the builder before using; though the builder will auto-start on first use. 082 * 083 * @see XPathConstants#NODESET 084 */ 085public class XPathBuilder extends ServiceSupport implements Expression, Predicate, NamespaceAware { 086 private static final Logger LOG = LoggerFactory.getLogger(XPathBuilder.class); 087 private static final String SAXON_OBJECT_MODEL_URI = "http://saxon.sf.net/jaxp/xpath/om"; 088 private static final String SAXON_FACTORY_CLASS_NAME = "net.sf.saxon.xpath.XPathFactoryImpl"; 089 private static final String OBTAIN_ALL_NS_XPATH = "//*/namespace::*"; 090 091 private static volatile XPathFactory defaultXPathFactory; 092 093 private final Queue<XPathExpression> pool = new ConcurrentLinkedQueue<XPathExpression>(); 094 private final Queue<XPathExpression> poolLogNamespaces = new ConcurrentLinkedQueue<XPathExpression>(); 095 private final String text; 096 private final ThreadLocal<Exchange> exchange = new ThreadLocal<Exchange>(); 097 private final MessageVariableResolver variableResolver = new MessageVariableResolver(exchange); 098 private final Map<String, String> namespaces = new ConcurrentHashMap<String, String>(); 099 private volatile XPathFactory xpathFactory; 100 private volatile Class<?> documentType = Document.class; 101 // For some reason the default expression of "a/b" on a document such as 102 // <a><b>1</b><b>2</b></a> 103 // will evaluate as just "1" by default which is bizarre. So by default 104 // let's assume XPath expressions result in nodesets. 105 private volatile Class<?> resultType; 106 private volatile QName resultQName = XPathConstants.NODESET; 107 private volatile String objectModelUri; 108 private volatile String factoryClassName; 109 private volatile DefaultNamespaceContext namespaceContext; 110 private volatile boolean logNamespaces; 111 private volatile XPathFunctionResolver functionResolver; 112 private volatile XPathFunction bodyFunction; 113 private volatile XPathFunction headerFunction; 114 private volatile XPathFunction outBodyFunction; 115 private volatile XPathFunction outHeaderFunction; 116 private volatile XPathFunction propertiesFunction; 117 private volatile XPathFunction simpleFunction; 118 /** 119 * The name of the header we want to apply the XPath expression to, which when set will cause 120 * the xpath to be evaluated on the required header, otherwise it will be applied to the body 121 */ 122 private volatile String headerName; 123 124 /** 125 * @param text The XPath expression 126 */ 127 public XPathBuilder(String text) { 128 this.text = text; 129 } 130 131 /** 132 * @param text The XPath expression 133 * @return A new XPathBuilder object 134 */ 135 public static XPathBuilder xpath(String text) { 136 return new XPathBuilder(text); 137 } 138 139 /** 140 * @param text The XPath expression 141 * @param resultType The result type that the XPath expression will return. 142 * @return A new XPathBuilder object 143 */ 144 public static XPathBuilder xpath(String text, Class<?> resultType) { 145 XPathBuilder builder = new XPathBuilder(text); 146 builder.setResultType(resultType); 147 return builder; 148 } 149 150 @Override 151 public String toString() { 152 return "XPath: " + text; 153 } 154 155 public boolean matches(Exchange exchange) { 156 try { 157 Object booleanResult = evaluateAs(exchange, XPathConstants.BOOLEAN); 158 return exchange.getContext().getTypeConverter().convertTo(Boolean.class, booleanResult); 159 } finally { 160 // remove the thread local after usage 161 this.exchange.remove(); 162 } 163 } 164 165 public <T> T evaluate(Exchange exchange, Class<T> type) { 166 try { 167 Object result = evaluate(exchange); 168 return exchange.getContext().getTypeConverter().convertTo(type, exchange, result); 169 } finally { 170 // remove the thread local after usage 171 this.exchange.remove(); 172 } 173 } 174 175 /** 176 * Matches the given xpath using the provided body. 177 * 178 * @param context the camel context 179 * @param body the body 180 * @return <tt>true</tt> if matches, <tt>false</tt> otherwise 181 */ 182 public boolean matches(CamelContext context, Object body) { 183 ObjectHelper.notNull(context, "CamelContext"); 184 185 // create a dummy Exchange to use during matching 186 Exchange dummy = new DefaultExchange(context); 187 dummy.getIn().setBody(body); 188 189 try { 190 return matches(dummy); 191 } finally { 192 // remove the thread local after usage 193 exchange.remove(); 194 } 195 } 196 197 /** 198 * Evaluates the given xpath using the provided body. 199 * <p/> 200 * The evaluation uses by default {@link javax.xml.xpath.XPathConstants#NODESET} as the type 201 * used during xpath evaluation. The output from xpath is then afterwards type converted 202 * using Camel's type converter to the given type. 203 * <p/> 204 * If you want to evaluate xpath using a different type, then call {@link #setResultType(Class)} 205 * prior to calling this evaluate method. 206 * 207 * @param context the camel context 208 * @param body the body 209 * @param type the type to return 210 * @return result of the evaluation 211 */ 212 public <T> T evaluate(CamelContext context, Object body, Class<T> type) { 213 ObjectHelper.notNull(context, "CamelContext"); 214 215 // create a dummy Exchange to use during evaluation 216 Exchange dummy = new DefaultExchange(context); 217 dummy.getIn().setBody(body); 218 219 try { 220 return evaluate(dummy, type); 221 } finally { 222 // remove the thread local after usage 223 exchange.remove(); 224 } 225 } 226 227 /** 228 * Evaluates the given xpath using the provided body as a String return type. 229 * 230 * @param context the camel context 231 * @param body the body 232 * @return result of the evaluation 233 */ 234 public String evaluate(CamelContext context, Object body) { 235 ObjectHelper.notNull(context, "CamelContext"); 236 237 // create a dummy Exchange to use during evaluation 238 Exchange dummy = new DefaultExchange(context); 239 dummy.getIn().setBody(body); 240 241 setResultQName(XPathConstants.STRING); 242 setResultType(String.class); 243 try { 244 return evaluate(dummy, String.class); 245 } finally { 246 // remove the thread local after usage 247 this.exchange.remove(); 248 } 249 } 250 251 // Builder methods 252 // ------------------------------------------------------------------------- 253 254 /** 255 * Sets the expression result type to {@link XPathConstants#BOOLEAN} 256 * 257 * @return the current builder 258 */ 259 public XPathBuilder booleanResult() { 260 resultQName = XPathConstants.BOOLEAN; 261 return this; 262 } 263 264 /** 265 * Sets the expression result type to {@link XPathConstants#NODE} 266 * 267 * @return the current builder 268 */ 269 public XPathBuilder nodeResult() { 270 resultQName = XPathConstants.NODE; 271 return this; 272 } 273 274 /** 275 * Sets the expression result type to {@link XPathConstants#NODESET} 276 * 277 * @return the current builder 278 */ 279 public XPathBuilder nodeSetResult() { 280 resultQName = XPathConstants.NODESET; 281 return this; 282 } 283 284 /** 285 * Sets the expression result type to {@link XPathConstants#NUMBER} 286 * 287 * @return the current builder 288 */ 289 public XPathBuilder numberResult() { 290 resultQName = XPathConstants.NUMBER; 291 return this; 292 } 293 294 /** 295 * Sets the expression result type to {@link XPathConstants#STRING} 296 * 297 * @return the current builder 298 */ 299 public XPathBuilder stringResult() { 300 resultQName = XPathConstants.STRING; 301 return this; 302 } 303 304 /** 305 * Sets the expression result type to the given {@code resultType} 306 * 307 * @return the current builder 308 */ 309 public XPathBuilder resultType(Class<?> resultType) { 310 setResultType(resultType); 311 return this; 312 } 313 314 /** 315 * Sets the object model URI to use 316 * 317 * @return the current builder 318 */ 319 public XPathBuilder objectModel(String uri) { 320 // Careful! Setting the Object Model URI this way will set the *Default* XPath Factory, which since is a static field, 321 // will set the XPath Factory system-wide. Decide what to do, as changing this behaviour can break compatibility. Provided the setObjectModel which changes 322 // this instance's XPath Factory rather than the static field 323 this.objectModelUri = uri; 324 return this; 325 } 326 327 328 /** 329 * Sets the factory class name to use 330 * 331 * @return the current builder 332 */ 333 public XPathBuilder factoryClassName(String factoryClassName) { 334 this.factoryClassName = factoryClassName; 335 return this; 336 } 337 338 339 /** 340 * Configures to use Saxon as the XPathFactory which allows you to use XPath 2.0 functions 341 * which may not be part of the build in JDK XPath parser. 342 * 343 * @return the current builder 344 */ 345 public XPathBuilder saxon() { 346 this.objectModelUri = SAXON_OBJECT_MODEL_URI; 347 this.factoryClassName = SAXON_FACTORY_CLASS_NAME; 348 return this; 349 } 350 351 /** 352 * Sets the {@link XPathFunctionResolver} instance to use on these XPath 353 * expressions 354 * 355 * @return the current builder 356 */ 357 public XPathBuilder functionResolver(XPathFunctionResolver functionResolver) { 358 this.functionResolver = functionResolver; 359 return this; 360 } 361 362 /** 363 * Registers the namespace prefix and URI with the builder so that the 364 * prefix can be used in XPath expressions 365 * 366 * @param prefix is the namespace prefix that can be used in the XPath 367 * expressions 368 * @param uri is the namespace URI to which the prefix refers 369 * @return the current builder 370 */ 371 public XPathBuilder namespace(String prefix, String uri) { 372 namespaces.put(prefix, uri); 373 return this; 374 } 375 376 /** 377 * Registers namespaces with the builder so that the registered 378 * prefixes can be used in XPath expressions 379 * 380 * @param namespaces is namespaces object that should be used in the 381 * XPath expression 382 * @return the current builder 383 */ 384 public XPathBuilder namespaces(Namespaces namespaces) { 385 namespaces.configure(this); 386 return this; 387 } 388 389 /** 390 * Registers a variable (in the global namespace) which can be referred to 391 * from XPath expressions 392 * 393 * @param name name of variable 394 * @param value value of variable 395 * @return the current builder 396 */ 397 public XPathBuilder variable(String name, Object value) { 398 getVariableResolver().addVariable(name, value); 399 return this; 400 } 401 402 /** 403 * Configures the document type to use. 404 * <p/> 405 * The document type controls which kind of Class Camel should convert the payload 406 * to before doing the xpath evaluation. 407 * <p/> 408 * For example you can set it to {@link InputSource} to use SAX streams. 409 * By default Camel uses {@link Document} as the type. 410 * 411 * @param documentType the document type 412 * @return the current builder 413 */ 414 public XPathBuilder documentType(Class<?> documentType) { 415 setDocumentType(documentType); 416 return this; 417 } 418 419 /** 420 * Configures to use the provided XPath factory. 421 * <p/> 422 * Can be used to use Saxon instead of the build in factory from the JDK. 423 * 424 * @param xpathFactory the xpath factory to use 425 * @return the current builder. 426 */ 427 public XPathBuilder factory(XPathFactory xpathFactory) { 428 setXPathFactory(xpathFactory); 429 return this; 430 } 431 432 /** 433 * Activates trace logging of all discovered namespaces in the message - to simplify debugging namespace-related issues 434 * <p/> 435 * Namespaces are printed in Hashmap style <code>{xmlns:prefix=[namespaceURI], xmlns:prefix=[namespaceURI]}</code>. 436 * <p/> 437 * The implicit XML namespace is omitted (http://www.w3.org/XML/1998/namespace). 438 * XML allows for namespace prefixes to be redefined/overridden due to hierarchical scoping, i.e. prefix abc can be mapped to http://abc.com, 439 * and deeper in the document it can be mapped to http://def.com. When two prefixes are detected which are equal but are mapped to different 440 * namespace URIs, Camel will show all namespaces URIs it is mapped to in an array-style. 441 * <p/> 442 * This feature is disabled by default. 443 * 444 * @return the current builder. 445 */ 446 public XPathBuilder logNamespaces() { 447 setLogNamespaces(true); 448 return this; 449 } 450 451 // Properties 452 // ------------------------------------------------------------------------- 453 454 /** 455 * Gets the xpath factory, can be <tt>null</tt> if no custom factory has been assigned. 456 * <p/> 457 * A default factory will be assigned (if no custom assigned) when either starting this builder 458 * or on first evaluation. 459 * 460 * @return the factory, or <tt>null</tt> if this builder has not been started/used before. 461 */ 462 public XPathFactory getXPathFactory() { 463 return xpathFactory; 464 } 465 466 public void setXPathFactory(XPathFactory xpathFactory) { 467 this.xpathFactory = xpathFactory; 468 } 469 470 public Class<?> getDocumentType() { 471 return documentType; 472 } 473 474 public void setDocumentType(Class<?> documentType) { 475 this.documentType = documentType; 476 } 477 478 public String getText() { 479 return text; 480 } 481 482 public QName getResultQName() { 483 return resultQName; 484 } 485 486 public void setResultQName(QName resultQName) { 487 this.resultQName = resultQName; 488 } 489 490 public String getHeaderName() { 491 return headerName; 492 } 493 494 public void setHeaderName(String headerName) { 495 this.headerName = headerName; 496 } 497 498 /** 499 * Gets the namespace context, can be <tt>null</tt> if no custom context has been assigned. 500 * <p/> 501 * A default context will be assigned (if no custom assigned) when either starting this builder 502 * or on first evaluation. 503 * 504 * @return the context, or <tt>null</tt> if this builder has not been started/used before. 505 */ 506 public DefaultNamespaceContext getNamespaceContext() { 507 return namespaceContext; 508 } 509 510 public void setNamespaceContext(DefaultNamespaceContext namespaceContext) { 511 this.namespaceContext = namespaceContext; 512 } 513 514 public XPathFunctionResolver getFunctionResolver() { 515 return functionResolver; 516 } 517 518 public void setFunctionResolver(XPathFunctionResolver functionResolver) { 519 this.functionResolver = functionResolver; 520 } 521 522 public void setNamespaces(Map<String, String> namespaces) { 523 this.namespaces.clear(); 524 this.namespaces.putAll(namespaces); 525 } 526 527 /** 528 * Gets the {@link XPathFunction} for getting the input message body. 529 * <p/> 530 * A default function will be assigned (if no custom assigned) when either starting this builder 531 * or on first evaluation. 532 * 533 * @return the function, or <tt>null</tt> if this builder has not been started/used before. 534 */ 535 public XPathFunction getBodyFunction() { 536 return bodyFunction; 537 } 538 539 private XPathFunction createBodyFunction() { 540 return new XPathFunction() { 541 @SuppressWarnings("rawtypes") 542 public Object evaluate(List list) throws XPathFunctionException { 543 return exchange.get().getIn().getBody(); 544 } 545 }; 546 } 547 548 public void setBodyFunction(XPathFunction bodyFunction) { 549 this.bodyFunction = bodyFunction; 550 } 551 552 /** 553 * Gets the {@link XPathFunction} for getting the input message header. 554 * <p/> 555 * A default function will be assigned (if no custom assigned) when either starting this builder 556 * or on first evaluation. 557 * 558 * @return the function, or <tt>null</tt> if this builder has not been started/used before. 559 */ 560 public XPathFunction getHeaderFunction() { 561 return headerFunction; 562 } 563 564 private XPathFunction createHeaderFunction() { 565 return new XPathFunction() { 566 @SuppressWarnings("rawtypes") 567 public Object evaluate(List list) throws XPathFunctionException { 568 if (!list.isEmpty()) { 569 Object value = list.get(0); 570 if (value != null) { 571 String text = exchange.get().getContext().getTypeConverter().convertTo(String.class, value); 572 return exchange.get().getIn().getHeader(text); 573 } 574 } 575 return null; 576 } 577 }; 578 } 579 580 public void setHeaderFunction(XPathFunction headerFunction) { 581 this.headerFunction = headerFunction; 582 } 583 584 /** 585 * Gets the {@link XPathFunction} for getting the output message body. 586 * <p/> 587 * A default function will be assigned (if no custom assigned) when either starting this builder 588 * or on first evaluation. 589 * 590 * @return the function, or <tt>null</tt> if this builder has not been started/used before. 591 */ 592 public XPathFunction getOutBodyFunction() { 593 return outBodyFunction; 594 } 595 596 private XPathFunction createOutBodyFunction() { 597 return new XPathFunction() { 598 @SuppressWarnings("rawtypes") 599 public Object evaluate(List list) throws XPathFunctionException { 600 if (exchange.get() != null && exchange.get().hasOut()) { 601 return exchange.get().getOut().getBody(); 602 } 603 return null; 604 } 605 }; 606 } 607 608 public void setOutBodyFunction(XPathFunction outBodyFunction) { 609 this.outBodyFunction = outBodyFunction; 610 } 611 612 /** 613 * Gets the {@link XPathFunction} for getting the output message header. 614 * <p/> 615 * A default function will be assigned (if no custom assigned) when either starting this builder 616 * or on first evaluation. 617 * 618 * @return the function, or <tt>null</tt> if this builder has not been started/used before. 619 */ 620 public XPathFunction getOutHeaderFunction() { 621 return outHeaderFunction; 622 } 623 624 private XPathFunction createOutHeaderFunction() { 625 return new XPathFunction() { 626 @SuppressWarnings("rawtypes") 627 public Object evaluate(List list) throws XPathFunctionException { 628 if (exchange.get() != null && !list.isEmpty()) { 629 Object value = list.get(0); 630 if (value != null) { 631 String text = exchange.get().getContext().getTypeConverter().convertTo(String.class, value); 632 return exchange.get().getOut().getHeader(text); 633 } 634 } 635 return null; 636 } 637 }; 638 } 639 640 public void setOutHeaderFunction(XPathFunction outHeaderFunction) { 641 this.outHeaderFunction = outHeaderFunction; 642 } 643 644 /** 645 * Gets the {@link XPathFunction} for getting the exchange properties. 646 * <p/> 647 * A default function will be assigned (if no custom assigned) when either starting this builder 648 * or on first evaluation. 649 * 650 * @return the function, or <tt>null</tt> if this builder has not been started/used before. 651 */ 652 public XPathFunction getPropertiesFunction() { 653 return propertiesFunction; 654 } 655 656 private XPathFunction createPropertiesFunction() { 657 return new XPathFunction() { 658 @SuppressWarnings("rawtypes") 659 public Object evaluate(List list) throws XPathFunctionException { 660 if (!list.isEmpty()) { 661 Object value = list.get(0); 662 if (value != null) { 663 String text = exchange.get().getContext().getTypeConverter().convertTo(String.class, value); 664 try { 665 // use the property placeholder resolver to lookup the property for us 666 Object answer = exchange.get().getContext().resolvePropertyPlaceholders("{{" + text + "}}"); 667 return answer; 668 } catch (Exception e) { 669 throw new XPathFunctionException(e); 670 } 671 } 672 } 673 return null; 674 } 675 }; 676 } 677 678 public void setPropertiesFunction(XPathFunction propertiesFunction) { 679 this.propertiesFunction = propertiesFunction; 680 } 681 682 /** 683 * Gets the {@link XPathFunction} for executing <a href="http://camel.apache.org/simple">simple</a> 684 * language as xpath function. 685 * <p/> 686 * A default function will be assigned (if no custom assigned) when either starting this builder 687 * or on first evaluation. 688 * 689 * @return the function, or <tt>null</tt> if this builder has not been started/used before. 690 */ 691 public XPathFunction getSimpleFunction() { 692 return simpleFunction; 693 } 694 695 private XPathFunction createSimpleFunction() { 696 return new XPathFunction() { 697 @SuppressWarnings("rawtypes") 698 public Object evaluate(List list) throws XPathFunctionException { 699 if (!list.isEmpty()) { 700 Object value = list.get(0); 701 if (value != null) { 702 String text = exchange.get().getContext().getTypeConverter().convertTo(String.class, value); 703 Language simple = exchange.get().getContext().resolveLanguage("simple"); 704 Expression exp = simple.createExpression(text); 705 Object answer = exp.evaluate(exchange.get(), Object.class); 706 return answer; 707 } 708 } 709 return null; 710 } 711 }; 712 } 713 714 public void setSimpleFunction(XPathFunction simpleFunction) { 715 this.simpleFunction = simpleFunction; 716 } 717 718 public Class<?> getResultType() { 719 return resultType; 720 } 721 722 public void setResultType(Class<?> resultType) { 723 this.resultType = resultType; 724 if (Number.class.isAssignableFrom(resultType)) { 725 numberResult(); 726 } else if (String.class.isAssignableFrom(resultType)) { 727 stringResult(); 728 } else if (Boolean.class.isAssignableFrom(resultType)) { 729 booleanResult(); 730 } else if (Node.class.isAssignableFrom(resultType)) { 731 nodeResult(); 732 } else if (NodeList.class.isAssignableFrom(resultType)) { 733 nodeSetResult(); 734 } 735 } 736 737 public void setLogNamespaces(boolean logNamespaces) { 738 this.logNamespaces = logNamespaces; 739 } 740 741 public boolean isLogNamespaces() { 742 return logNamespaces; 743 } 744 745 /** 746 * Enables Saxon on this particular XPath expression, as {@link #saxon()} sets the default static XPathFactory which may have already been initialised 747 * by previous XPath expressions 748 */ 749 public void enableSaxon() { 750 this.setObjectModelUri(SAXON_OBJECT_MODEL_URI); 751 this.setFactoryClassName(SAXON_FACTORY_CLASS_NAME); 752 753 } 754 755 public String getObjectModelUri() { 756 return objectModelUri; 757 } 758 759 public void setObjectModelUri(String objectModelUri) { 760 this.objectModelUri = objectModelUri; 761 } 762 763 public String getFactoryClassName() { 764 return factoryClassName; 765 } 766 767 public void setFactoryClassName(String factoryClassName) { 768 this.factoryClassName = factoryClassName; 769 } 770 771 // Implementation methods 772 // ------------------------------------------------------------------------- 773 774 protected Object evaluate(Exchange exchange) { 775 Object answer = evaluateAs(exchange, resultQName); 776 if (resultType != null) { 777 return ExchangeHelper.convertToType(exchange, resultType, answer); 778 } 779 return answer; 780 } 781 782 /** 783 * Evaluates the expression as the given result type 784 */ 785 protected Object evaluateAs(Exchange exchange, QName resultQName) { 786 // pool a pre compiled expression from pool 787 XPathExpression xpathExpression = pool.poll(); 788 if (xpathExpression == null) { 789 LOG.trace("Creating new XPathExpression as none was available from pool"); 790 // no avail in pool then create one 791 try { 792 xpathExpression = createXPathExpression(); 793 } catch (XPathExpressionException e) { 794 throw new InvalidXPathExpression(getText(), e); 795 } catch (Exception e) { 796 throw new RuntimeExpressionException("Cannot create xpath expression", e); 797 } 798 } else { 799 LOG.trace("Acquired XPathExpression from pool"); 800 } 801 try { 802 if (logNamespaces && LOG.isInfoEnabled()) { 803 logNamespaces(exchange); 804 } 805 return doInEvaluateAs(xpathExpression, exchange, resultQName); 806 } finally { 807 // release it back to the pool 808 pool.add(xpathExpression); 809 LOG.trace("Released XPathExpression back to pool"); 810 } 811 } 812 813 private void logNamespaces(Exchange exchange) { 814 InputStream is = null; 815 NodeList answer = null; 816 XPathExpression xpathExpression = null; 817 818 try { 819 xpathExpression = poolLogNamespaces.poll(); 820 if (xpathExpression == null) { 821 xpathExpression = createTraceNamespaceExpression(); 822 } 823 824 // prepare the input 825 Object document; 826 if (isInputStreamNeeded(exchange)) { 827 is = exchange.getIn().getBody(InputStream.class); 828 document = getDocument(exchange, is); 829 } else { 830 Object body = exchange.getIn().getBody(); 831 document = getDocument(exchange, body); 832 } 833 // fetch all namespaces 834 if (document instanceof InputSource) { 835 InputSource inputSource = (InputSource) document; 836 answer = (NodeList) xpathExpression.evaluate(inputSource, XPathConstants.NODESET); 837 } else if (document instanceof DOMSource) { 838 DOMSource source = (DOMSource) document; 839 answer = (NodeList) xpathExpression.evaluate(source.getNode(), XPathConstants.NODESET); 840 } else { 841 answer = (NodeList) xpathExpression.evaluate(document, XPathConstants.NODESET); 842 } 843 } catch (Exception e) { 844 LOG.warn("Unable to trace discovered namespaces in XPath expression", e); 845 } finally { 846 // IOHelper can handle if is is null 847 IOHelper.close(is); 848 poolLogNamespaces.add(xpathExpression); 849 } 850 851 if (answer != null) { 852 logDiscoveredNamespaces(answer); 853 } 854 } 855 856 private void logDiscoveredNamespaces(NodeList namespaces) { 857 Map<String, HashSet<String>> map = new LinkedHashMap<String, HashSet<String>>(); 858 for (int i = 0; i < namespaces.getLength(); i++) { 859 Node n = namespaces.item(i); 860 if (n.getNodeName().equals("xmlns:xml")) { 861 // skip the implicit XML namespace as it provides no value 862 continue; 863 } 864 865 String prefix = namespaces.item(i).getNodeName(); 866 if (prefix.equals("xmlns")) { 867 prefix = "DEFAULT"; 868 } 869 870 // add to map 871 if (!map.containsKey(prefix)) { 872 map.put(prefix, new HashSet<String>()); 873 } 874 map.get(prefix).add(namespaces.item(i).getNodeValue()); 875 } 876 877 LOG.info("Namespaces discovered in message: {}.", map); 878 } 879 880 protected Object doInEvaluateAs(XPathExpression xpathExpression, Exchange exchange, QName resultQName) { 881 LOG.trace("Evaluating exchange: {} as: {}", exchange, resultQName); 882 883 Object answer; 884 885 // set exchange and variable resolver as thread locals for concurrency 886 this.exchange.set(exchange); 887 888 // the underlying input stream, which we need to close to avoid locking files or other resources 889 InputStream is = null; 890 try { 891 Object document; 892 893 // Check if we need to apply the XPath expression to a header 894 if (ObjectHelper.isNotEmpty(getHeaderName())) { 895 String headerName = getHeaderName(); 896 // only convert to input stream if really needed 897 if (isInputStreamNeeded(exchange, headerName)) { 898 is = exchange.getIn().getHeader(headerName, InputStream.class); 899 document = getDocument(exchange, is); 900 } else { 901 Object headerObject = exchange.getIn().getHeader(getHeaderName()); 902 document = getDocument(exchange, headerObject); 903 } 904 } else { 905 // only convert to input stream if really needed 906 if (isInputStreamNeeded(exchange)) { 907 is = exchange.getIn().getBody(InputStream.class); 908 document = getDocument(exchange, is); 909 } else { 910 Object body = exchange.getIn().getBody(); 911 document = getDocument(exchange, body); 912 } 913 } 914 915 if (resultQName != null) { 916 if (document instanceof InputSource) { 917 InputSource inputSource = (InputSource) document; 918 answer = xpathExpression.evaluate(inputSource, resultQName); 919 } else if (document instanceof DOMSource) { 920 DOMSource source = (DOMSource) document; 921 answer = xpathExpression.evaluate(source.getNode(), resultQName); 922 } else { 923 answer = xpathExpression.evaluate(document, resultQName); 924 } 925 } else { 926 if (document instanceof InputSource) { 927 InputSource inputSource = (InputSource) document; 928 answer = xpathExpression.evaluate(inputSource); 929 } else if (document instanceof DOMSource) { 930 DOMSource source = (DOMSource) document; 931 answer = xpathExpression.evaluate(source.getNode()); 932 } else { 933 answer = xpathExpression.evaluate(document); 934 } 935 } 936 } catch (XPathExpressionException e) { 937 String message = getText(); 938 if (ObjectHelper.isNotEmpty(getHeaderName())) { 939 message = message + " with headerName " + getHeaderName(); 940 } 941 throw new InvalidXPathExpression(message, e); 942 } finally { 943 // IOHelper can handle if is is null 944 IOHelper.close(is); 945 } 946 947 if (LOG.isTraceEnabled()) { 948 LOG.trace("Done evaluating exchange: {} as: {} with result: {}", new Object[]{exchange, resultQName, answer}); 949 } 950 return answer; 951 } 952 953 /** 954 * Creates a new xpath expression as there we no available in the pool. 955 * <p/> 956 * This implementation must be synchronized to ensure thread safety, as this XPathBuilder instance may not have been 957 * started prior to being used. 958 */ 959 protected synchronized XPathExpression createXPathExpression() throws XPathExpressionException, XPathFactoryConfigurationException { 960 // ensure we are started 961 try { 962 start(); 963 } catch (Exception e) { 964 throw new RuntimeExpressionException("Error starting XPathBuilder", e); 965 } 966 967 // XPathFactory is not thread safe 968 XPath xPath = getXPathFactory().newXPath(); 969 970 if (!logNamespaces && LOG.isTraceEnabled()) { 971 LOG.trace("Creating new XPath expression in pool. Namespaces on XPath expression: {}", getNamespaceContext().toString()); 972 } else if (logNamespaces && LOG.isInfoEnabled()) { 973 LOG.info("Creating new XPath expression in pool. Namespaces on XPath expression: {}", getNamespaceContext().toString()); 974 } 975 xPath.setNamespaceContext(getNamespaceContext()); 976 xPath.setXPathVariableResolver(getVariableResolver()); 977 978 XPathFunctionResolver parentResolver = getFunctionResolver(); 979 if (parentResolver == null) { 980 parentResolver = xPath.getXPathFunctionResolver(); 981 } 982 xPath.setXPathFunctionResolver(createDefaultFunctionResolver(parentResolver)); 983 return xPath.compile(text); 984 } 985 986 protected synchronized XPathExpression createTraceNamespaceExpression() throws XPathFactoryConfigurationException, XPathExpressionException { 987 // XPathFactory is not thread safe 988 XPath xPath = getXPathFactory().newXPath(); 989 return xPath.compile(OBTAIN_ALL_NS_XPATH); 990 } 991 992 protected DefaultNamespaceContext createNamespaceContext(XPathFactory factory) { 993 DefaultNamespaceContext context = new DefaultNamespaceContext(factory); 994 populateDefaultNamespaces(context); 995 return context; 996 } 997 998 /** 999 * Populate a number of standard prefixes if they are not already there 1000 */ 1001 protected void populateDefaultNamespaces(DefaultNamespaceContext context) { 1002 setNamespaceIfNotPresent(context, "in", IN_NAMESPACE); 1003 setNamespaceIfNotPresent(context, "out", OUT_NAMESPACE); 1004 setNamespaceIfNotPresent(context, "env", Namespaces.ENVIRONMENT_VARIABLES); 1005 setNamespaceIfNotPresent(context, "system", Namespaces.SYSTEM_PROPERTIES_NAMESPACE); 1006 setNamespaceIfNotPresent(context, "function", Namespaces.FUNCTION_NAMESPACE); 1007 } 1008 1009 protected void setNamespaceIfNotPresent(DefaultNamespaceContext context, String prefix, String uri) { 1010 if (context != null) { 1011 String current = context.getNamespaceURI(prefix); 1012 if (current == null) { 1013 context.add(prefix, uri); 1014 } 1015 } 1016 } 1017 1018 protected XPathFunctionResolver createDefaultFunctionResolver(final XPathFunctionResolver parent) { 1019 return new XPathFunctionResolver() { 1020 public XPathFunction resolveFunction(QName qName, int argumentCount) { 1021 XPathFunction answer = null; 1022 if (parent != null) { 1023 answer = parent.resolveFunction(qName, argumentCount); 1024 } 1025 if (answer == null) { 1026 if (isMatchingNamespaceOrEmptyNamespace(qName.getNamespaceURI(), IN_NAMESPACE) 1027 || isMatchingNamespaceOrEmptyNamespace(qName.getNamespaceURI(), DEFAULT_NAMESPACE)) { 1028 String localPart = qName.getLocalPart(); 1029 if (localPart.equals("body") && argumentCount == 0) { 1030 return getBodyFunction(); 1031 } 1032 if (localPart.equals("header") && argumentCount == 1) { 1033 return getHeaderFunction(); 1034 } 1035 } 1036 if (isMatchingNamespaceOrEmptyNamespace(qName.getNamespaceURI(), OUT_NAMESPACE)) { 1037 String localPart = qName.getLocalPart(); 1038 if (localPart.equals("body") && argumentCount == 0) { 1039 return getOutBodyFunction(); 1040 } 1041 if (localPart.equals("header") && argumentCount == 1) { 1042 return getOutHeaderFunction(); 1043 } 1044 } 1045 if (isMatchingNamespaceOrEmptyNamespace(qName.getNamespaceURI(), FUNCTION_NAMESPACE)) { 1046 String localPart = qName.getLocalPart(); 1047 if (localPart.equals("properties") && argumentCount == 1) { 1048 return getPropertiesFunction(); 1049 } 1050 if (localPart.equals("simple") && argumentCount == 1) { 1051 return getSimpleFunction(); 1052 } 1053 } 1054 } 1055 return answer; 1056 } 1057 }; 1058 } 1059 1060 /** 1061 * Checks whether we need an {@link InputStream} to access the message body. 1062 * <p/> 1063 * Depending on the content in the message body, we may not need to convert 1064 * to {@link InputStream}. 1065 * 1066 * @param exchange the current exchange 1067 * @return <tt>true</tt> to convert to {@link InputStream} beforehand converting afterwards. 1068 */ 1069 protected boolean isInputStreamNeeded(Exchange exchange) { 1070 Object body = exchange.getIn().getBody(); 1071 return isInputStreamNeededForObject(exchange, body); 1072 } 1073 1074 /** 1075 * Checks whether we need an {@link InputStream} to access the message header. 1076 * <p/> 1077 * Depending on the content in the message header, we may not need to convert 1078 * to {@link InputStream}. 1079 * 1080 * @param exchange the current exchange 1081 * @return <tt>true</tt> to convert to {@link InputStream} beforehand converting afterwards. 1082 */ 1083 protected boolean isInputStreamNeeded(Exchange exchange, String headerName) { 1084 Object header = exchange.getIn().getHeader(headerName); 1085 return isInputStreamNeededForObject(exchange, header); 1086 } 1087 1088 /** 1089 * Checks whether we need an {@link InputStream} to access this object 1090 * <p/> 1091 * Depending on the content in the object, we may not need to convert 1092 * to {@link InputStream}. 1093 * 1094 * @param exchange the current exchange 1095 * @return <tt>true</tt> to convert to {@link InputStream} beforehand converting afterwards. 1096 */ 1097 protected boolean isInputStreamNeededForObject(Exchange exchange, Object obj) { 1098 if (obj == null) { 1099 return false; 1100 } 1101 1102 if (obj instanceof WrappedFile) { 1103 obj = ((WrappedFile<?>) obj).getFile(); 1104 } 1105 if (obj instanceof File) { 1106 // input stream is needed for File to avoid locking the file in case of errors etc 1107 return true; 1108 } 1109 1110 // input stream is not needed otherwise 1111 return false; 1112 } 1113 1114 /** 1115 * Strategy method to extract the document from the exchange. 1116 */ 1117 protected Object getDocument(Exchange exchange, Object body) { 1118 try { 1119 return doGetDocument(exchange, body); 1120 } catch (Exception e) { 1121 throw ObjectHelper.wrapRuntimeCamelException(e); 1122 } finally { 1123 // call the reset if the in message body is StreamCache 1124 MessageHelper.resetStreamCache(exchange.getIn()); 1125 } 1126 } 1127 1128 protected Object doGetDocument(Exchange exchange, Object body) throws Exception { 1129 if (body == null) { 1130 return null; 1131 } 1132 1133 Object answer = null; 1134 1135 Class<?> type = getDocumentType(); 1136 Exception cause = null; 1137 if (type != null) { 1138 // try to get the body as the desired type 1139 try { 1140 answer = exchange.getContext().getTypeConverter().convertTo(type, exchange, body); 1141 } catch (Exception e) { 1142 // we want to store the caused exception, if we could not convert 1143 cause = e; 1144 } 1145 } 1146 1147 if (type == null && answer == null) { 1148 // fallback to get the body as is 1149 answer = body; 1150 } else if (answer == null) { 1151 // there was a type, and we could not convert to it, then fail 1152 if (cause != null) { 1153 throw cause; 1154 } else { 1155 throw new NoTypeConversionAvailableException(body, type); 1156 } 1157 } 1158 1159 return answer; 1160 } 1161 1162 private MessageVariableResolver getVariableResolver() { 1163 return variableResolver; 1164 } 1165 1166 @Override 1167 public void doStart() throws Exception { 1168 if (xpathFactory == null) { 1169 xpathFactory = createXPathFactory(); 1170 } 1171 if (namespaceContext == null) { 1172 namespaceContext = createNamespaceContext(xpathFactory); 1173 } 1174 for (Map.Entry<String, String> entry : namespaces.entrySet()) { 1175 namespaceContext.add(entry.getKey(), entry.getValue()); 1176 } 1177 1178 // create default functions if no custom assigned 1179 if (bodyFunction == null) { 1180 bodyFunction = createBodyFunction(); 1181 } 1182 if (headerFunction == null) { 1183 headerFunction = createHeaderFunction(); 1184 } 1185 if (outBodyFunction == null) { 1186 outBodyFunction = createOutBodyFunction(); 1187 } 1188 if (outHeaderFunction == null) { 1189 outHeaderFunction = createOutHeaderFunction(); 1190 } 1191 if (propertiesFunction == null) { 1192 propertiesFunction = createPropertiesFunction(); 1193 } 1194 if (simpleFunction == null) { 1195 simpleFunction = createSimpleFunction(); 1196 } 1197 } 1198 1199 @Override 1200 public void doStop() throws Exception { 1201 pool.clear(); 1202 poolLogNamespaces.clear(); 1203 } 1204 1205 protected synchronized XPathFactory createXPathFactory() throws XPathFactoryConfigurationException { 1206 if (objectModelUri != null) { 1207 String xpathFactoryClassName = factoryClassName; 1208 if (objectModelUri.equals(SAXON_OBJECT_MODEL_URI) && ObjectHelper.isEmpty(xpathFactoryClassName)) { 1209 xpathFactoryClassName = SAXON_FACTORY_CLASS_NAME; 1210 } 1211 1212 xpathFactory = ObjectHelper.isEmpty(xpathFactoryClassName) 1213 ? XPathFactory.newInstance(objectModelUri) 1214 : XPathFactory.newInstance(objectModelUri, xpathFactoryClassName, null); 1215 1216 LOG.info("Using objectModelUri " + objectModelUri + " when created XPathFactory {}", xpathFactory); 1217 return xpathFactory; 1218 } 1219 1220 if (defaultXPathFactory == null) { 1221 defaultXPathFactory = createDefaultXPathFactory(); 1222 } 1223 return defaultXPathFactory; 1224 } 1225 1226 protected static XPathFactory createDefaultXPathFactory() throws XPathFactoryConfigurationException { 1227 XPathFactory factory = null; 1228 1229 // read system property and see if there is a factory set 1230 Properties properties = System.getProperties(); 1231 for (Map.Entry<Object, Object> prop : properties.entrySet()) { 1232 String key = (String) prop.getKey(); 1233 if (key.startsWith(XPathFactory.DEFAULT_PROPERTY_NAME)) { 1234 String uri = ObjectHelper.after(key, ":"); 1235 if (uri != null) { 1236 factory = XPathFactory.newInstance(uri); 1237 LOG.info("Using system property {} with value {} when created default XPathFactory {}", new Object[]{key, uri, factory}); 1238 } 1239 } 1240 } 1241 1242 if (factory == null) { 1243 factory = XPathFactory.newInstance(); 1244 LOG.info("Created default XPathFactory {}", factory); 1245 } 1246 1247 return factory; 1248 } 1249 1250}