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