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