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