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