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