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