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 public Map<String, String> getNamespaces() { 513 return namespaces; 514 } 515 516 /** 517 * Gets the {@link XPathFunction} for getting the input message body. 518 * <p/> 519 * A default function will be assigned (if no custom assigned) when either starting this builder 520 * or on first evaluation. 521 * 522 * @return the function, or <tt>null</tt> if this builder has not been started/used before. 523 */ 524 public XPathFunction getBodyFunction() { 525 return bodyFunction; 526 } 527 528 private XPathFunction createBodyFunction() { 529 return new XPathFunction() { 530 @SuppressWarnings("rawtypes") 531 public Object evaluate(List list) throws XPathFunctionException { 532 return exchange.get().getIn().getBody(); 533 } 534 }; 535 } 536 537 public void setBodyFunction(XPathFunction bodyFunction) { 538 this.bodyFunction = bodyFunction; 539 } 540 541 /** 542 * Gets the {@link XPathFunction} for getting the input message header. 543 * <p/> 544 * A default function will be assigned (if no custom assigned) when either starting this builder 545 * or on first evaluation. 546 * 547 * @return the function, or <tt>null</tt> if this builder has not been started/used before. 548 */ 549 public XPathFunction getHeaderFunction() { 550 return headerFunction; 551 } 552 553 private XPathFunction createHeaderFunction() { 554 return new XPathFunction() { 555 @SuppressWarnings("rawtypes") 556 public Object evaluate(List list) throws XPathFunctionException { 557 if (!list.isEmpty()) { 558 Object value = list.get(0); 559 if (value != null) { 560 String text = exchange.get().getContext().getTypeConverter().convertTo(String.class, value); 561 return exchange.get().getIn().getHeader(text); 562 } 563 } 564 return null; 565 } 566 }; 567 } 568 569 public void setHeaderFunction(XPathFunction headerFunction) { 570 this.headerFunction = headerFunction; 571 } 572 573 /** 574 * Gets the {@link XPathFunction} for getting the output message body. 575 * <p/> 576 * A default function will be assigned (if no custom assigned) when either starting this builder 577 * or on first evaluation. 578 * 579 * @return the function, or <tt>null</tt> if this builder has not been started/used before. 580 */ 581 public XPathFunction getOutBodyFunction() { 582 return outBodyFunction; 583 } 584 585 private XPathFunction createOutBodyFunction() { 586 return new XPathFunction() { 587 @SuppressWarnings("rawtypes") 588 public Object evaluate(List list) throws XPathFunctionException { 589 if (exchange.get() != null && exchange.get().hasOut()) { 590 return exchange.get().getOut().getBody(); 591 } 592 return null; 593 } 594 }; 595 } 596 597 public void setOutBodyFunction(XPathFunction outBodyFunction) { 598 this.outBodyFunction = outBodyFunction; 599 } 600 601 /** 602 * Gets the {@link XPathFunction} for getting the output message header. 603 * <p/> 604 * A default function will be assigned (if no custom assigned) when either starting this builder 605 * or on first evaluation. 606 * 607 * @return the function, or <tt>null</tt> if this builder has not been started/used before. 608 */ 609 public XPathFunction getOutHeaderFunction() { 610 return outHeaderFunction; 611 } 612 613 private XPathFunction createOutHeaderFunction() { 614 return new XPathFunction() { 615 @SuppressWarnings("rawtypes") 616 public Object evaluate(List list) throws XPathFunctionException { 617 if (exchange.get() != null && !list.isEmpty()) { 618 Object value = list.get(0); 619 if (value != null) { 620 String text = exchange.get().getContext().getTypeConverter().convertTo(String.class, value); 621 return exchange.get().getOut().getHeader(text); 622 } 623 } 624 return null; 625 } 626 }; 627 } 628 629 public void setOutHeaderFunction(XPathFunction outHeaderFunction) { 630 this.outHeaderFunction = outHeaderFunction; 631 } 632 633 /** 634 * Gets the {@link XPathFunction} for getting the exchange properties. 635 * <p/> 636 * A default function will be assigned (if no custom assigned) when either starting this builder 637 * or on first evaluation. 638 * 639 * @return the function, or <tt>null</tt> if this builder has not been started/used before. 640 */ 641 public XPathFunction getPropertiesFunction() { 642 return propertiesFunction; 643 } 644 645 private XPathFunction createPropertiesFunction() { 646 return new XPathFunction() { 647 @SuppressWarnings("rawtypes") 648 public Object evaluate(List list) throws XPathFunctionException { 649 if (!list.isEmpty()) { 650 Object value = list.get(0); 651 if (value != null) { 652 String text = exchange.get().getContext().getTypeConverter().convertTo(String.class, value); 653 try { 654 // use the property placeholder resolver to lookup the property for us 655 Object answer = exchange.get().getContext().resolvePropertyPlaceholders("{{" + text + "}}"); 656 return answer; 657 } catch (Exception e) { 658 throw new XPathFunctionException(e); 659 } 660 } 661 } 662 return null; 663 } 664 }; 665 } 666 667 public void setPropertiesFunction(XPathFunction propertiesFunction) { 668 this.propertiesFunction = propertiesFunction; 669 } 670 671 /** 672 * Gets the {@link XPathFunction} for executing <a href="http://camel.apache.org/simple">simple</a> 673 * language as xpath function. 674 * <p/> 675 * A default function will be assigned (if no custom assigned) when either starting this builder 676 * or on first evaluation. 677 * 678 * @return the function, or <tt>null</tt> if this builder has not been started/used before. 679 */ 680 public XPathFunction getSimpleFunction() { 681 return simpleFunction; 682 } 683 684 private XPathFunction createSimpleFunction() { 685 return new XPathFunction() { 686 @SuppressWarnings("rawtypes") 687 public Object evaluate(List list) throws XPathFunctionException { 688 if (!list.isEmpty()) { 689 Object value = list.get(0); 690 if (value != null) { 691 String text = exchange.get().getContext().getTypeConverter().convertTo(String.class, value); 692 Language simple = exchange.get().getContext().resolveLanguage("simple"); 693 Expression exp = simple.createExpression(text); 694 Object answer = exp.evaluate(exchange.get(), Object.class); 695 return answer; 696 } 697 } 698 return null; 699 } 700 }; 701 } 702 703 public void setSimpleFunction(XPathFunction simpleFunction) { 704 this.simpleFunction = simpleFunction; 705 } 706 707 public Class<?> getResultType() { 708 return resultType; 709 } 710 711 public void setResultType(Class<?> resultType) { 712 this.resultType = resultType; 713 if (Number.class.isAssignableFrom(resultType)) { 714 numberResult(); 715 } else if (String.class.isAssignableFrom(resultType)) { 716 stringResult(); 717 } else if (Boolean.class.isAssignableFrom(resultType)) { 718 booleanResult(); 719 } else if (Node.class.isAssignableFrom(resultType)) { 720 nodeResult(); 721 } else if (NodeList.class.isAssignableFrom(resultType)) { 722 nodeSetResult(); 723 } 724 } 725 726 public void setLogNamespaces(boolean logNamespaces) { 727 this.logNamespaces = logNamespaces; 728 } 729 730 public boolean isLogNamespaces() { 731 return logNamespaces; 732 } 733 734 public String getObjectModelUri() { 735 return objectModelUri; 736 } 737 738 /** 739 * Enables Saxon on this particular XPath expression, as {@link #saxon()} sets the default static XPathFactory which may have already been initialised 740 * by previous XPath expressions 741 */ 742 public void enableSaxon() { 743 this.setObjectModelUri(SAXON_OBJECT_MODEL_URI); 744 } 745 746 public void setObjectModelUri(String objectModelUri) { 747 this.objectModelUri = objectModelUri; 748 } 749 750 // Implementation methods 751 // ------------------------------------------------------------------------- 752 753 protected Object evaluate(Exchange exchange) { 754 Object answer = evaluateAs(exchange, resultQName); 755 if (resultType != null) { 756 return ExchangeHelper.convertToType(exchange, resultType, answer); 757 } 758 return answer; 759 } 760 761 /** 762 * Evaluates the expression as the given result type 763 */ 764 protected Object evaluateAs(Exchange exchange, QName resultQName) { 765 // pool a pre compiled expression from pool 766 XPathExpression xpathExpression = pool.poll(); 767 if (xpathExpression == null) { 768 LOG.trace("Creating new XPathExpression as none was available from pool"); 769 // no avail in pool then create one 770 try { 771 xpathExpression = createXPathExpression(); 772 } catch (XPathExpressionException e) { 773 throw new InvalidXPathExpression(getText(), e); 774 } catch (Exception e) { 775 throw new RuntimeExpressionException("Cannot create xpath expression", e); 776 } 777 } else { 778 LOG.trace("Acquired XPathExpression from pool"); 779 } 780 try { 781 if (logNamespaces && LOG.isInfoEnabled()) { 782 logNamespaces(exchange); 783 } 784 return doInEvaluateAs(xpathExpression, exchange, resultQName); 785 } finally { 786 // release it back to the pool 787 pool.add(xpathExpression); 788 LOG.trace("Released XPathExpression back to pool"); 789 } 790 } 791 792 private void logNamespaces(Exchange exchange) { 793 InputStream is = null; 794 NodeList answer = null; 795 XPathExpression xpathExpression = null; 796 797 try { 798 xpathExpression = poolLogNamespaces.poll(); 799 if (xpathExpression == null) { 800 xpathExpression = createTraceNamespaceExpression(); 801 } 802 803 // prepare the input 804 Object document; 805 if (isInputStreamNeeded(exchange)) { 806 is = exchange.getIn().getBody(InputStream.class); 807 document = getDocument(exchange, is); 808 } else { 809 Object body = exchange.getIn().getBody(); 810 document = getDocument(exchange, body); 811 } 812 // fetch all namespaces 813 if (document instanceof InputSource) { 814 InputSource inputSource = (InputSource) document; 815 answer = (NodeList) xpathExpression.evaluate(inputSource, XPathConstants.NODESET); 816 } else if (document instanceof DOMSource) { 817 DOMSource source = (DOMSource) document; 818 answer = (NodeList) xpathExpression.evaluate(source.getNode(), XPathConstants.NODESET); 819 } else { 820 answer = (NodeList) xpathExpression.evaluate(document, XPathConstants.NODESET); 821 } 822 } catch (Exception e) { 823 LOG.warn("Unable to trace discovered namespaces in XPath expression", e); 824 } finally { 825 // IOHelper can handle if is is null 826 IOHelper.close(is); 827 poolLogNamespaces.add(xpathExpression); 828 } 829 830 if (answer != null) { 831 logDiscoveredNamespaces(answer); 832 } 833 } 834 835 private void logDiscoveredNamespaces(NodeList namespaces) { 836 Map<String, HashSet<String>> map = new LinkedHashMap<String, HashSet<String>>(); 837 for (int i = 0; i < namespaces.getLength(); i++) { 838 Node n = namespaces.item(i); 839 if (n.getNodeName().equals("xmlns:xml")) { 840 // skip the implicit XML namespace as it provides no value 841 continue; 842 } 843 844 String prefix = namespaces.item(i).getNodeName(); 845 if (prefix.equals("xmlns")) { 846 prefix = "DEFAULT"; 847 } 848 849 // add to map 850 if (!map.containsKey(prefix)) { 851 map.put(prefix, new HashSet<String>()); 852 } 853 map.get(prefix).add(namespaces.item(i).getNodeValue()); 854 } 855 856 LOG.info("Namespaces discovered in message: {}.", map); 857 } 858 859 protected Object doInEvaluateAs(XPathExpression xpathExpression, Exchange exchange, QName resultQName) { 860 LOG.trace("Evaluating exchange: {} as: {}", exchange, resultQName); 861 862 Object answer; 863 864 // set exchange and variable resolver as thread locals for concurrency 865 this.exchange.set(exchange); 866 867 // the underlying input stream, which we need to close to avoid locking files or other resources 868 InputStream is = null; 869 try { 870 Object document; 871 872 // Check if we need to apply the XPath expression to a header 873 if (ObjectHelper.isNotEmpty(getHeaderName())) { 874 String headerName = getHeaderName(); 875 // only convert to input stream if really needed 876 if (isInputStreamNeeded(exchange, headerName)) { 877 is = exchange.getIn().getHeader(headerName, InputStream.class); 878 document = getDocument(exchange, is); 879 } else { 880 Object headerObject = exchange.getIn().getHeader(getHeaderName()); 881 document = getDocument(exchange, headerObject); 882 } 883 } else { 884 // only convert to input stream if really needed 885 if (isInputStreamNeeded(exchange)) { 886 is = exchange.getIn().getBody(InputStream.class); 887 document = getDocument(exchange, is); 888 } else { 889 Object body = exchange.getIn().getBody(); 890 document = getDocument(exchange, body); 891 } 892 } 893 894 if (resultQName != null) { 895 if (document instanceof InputSource) { 896 InputSource inputSource = (InputSource) document; 897 answer = xpathExpression.evaluate(inputSource, resultQName); 898 } else if (document instanceof DOMSource) { 899 DOMSource source = (DOMSource) document; 900 answer = xpathExpression.evaluate(source.getNode(), resultQName); 901 } else { 902 answer = xpathExpression.evaluate(document, resultQName); 903 } 904 } else { 905 if (document instanceof InputSource) { 906 InputSource inputSource = (InputSource) document; 907 answer = xpathExpression.evaluate(inputSource); 908 } else if (document instanceof DOMSource) { 909 DOMSource source = (DOMSource) document; 910 answer = xpathExpression.evaluate(source.getNode()); 911 } else { 912 answer = xpathExpression.evaluate(document); 913 } 914 } 915 } catch (XPathExpressionException e) { 916 String message = getText(); 917 if (ObjectHelper.isNotEmpty(getHeaderName())) { 918 message = message + " with headerName " + getHeaderName(); 919 } 920 throw new InvalidXPathExpression(message, e); 921 } finally { 922 // IOHelper can handle if is is null 923 IOHelper.close(is); 924 } 925 926 if (LOG.isTraceEnabled()) { 927 LOG.trace("Done evaluating exchange: {} as: {} with result: {}", new Object[]{exchange, resultQName, answer}); 928 } 929 return answer; 930 } 931 932 /** 933 * Creates a new xpath expression as there we no available in the pool. 934 * <p/> 935 * This implementation must be synchronized to ensure thread safety, as this XPathBuilder instance may not have been 936 * started prior to being used. 937 */ 938 protected synchronized XPathExpression createXPathExpression() throws XPathExpressionException, XPathFactoryConfigurationException { 939 // ensure we are started 940 try { 941 start(); 942 } catch (Exception e) { 943 throw new RuntimeExpressionException("Error starting XPathBuilder", e); 944 } 945 946 // XPathFactory is not thread safe 947 XPath xPath = getXPathFactory().newXPath(); 948 949 if (!logNamespaces && LOG.isTraceEnabled()) { 950 LOG.trace("Creating new XPath expression in pool. Namespaces on XPath expression: {}", getNamespaceContext().toString()); 951 } else if (logNamespaces && LOG.isInfoEnabled()) { 952 LOG.info("Creating new XPath expression in pool. Namespaces on XPath expression: {}", getNamespaceContext().toString()); 953 } 954 xPath.setNamespaceContext(getNamespaceContext()); 955 xPath.setXPathVariableResolver(getVariableResolver()); 956 957 XPathFunctionResolver parentResolver = getFunctionResolver(); 958 if (parentResolver == null) { 959 parentResolver = xPath.getXPathFunctionResolver(); 960 } 961 xPath.setXPathFunctionResolver(createDefaultFunctionResolver(parentResolver)); 962 return xPath.compile(text); 963 } 964 965 protected synchronized XPathExpression createTraceNamespaceExpression() throws XPathFactoryConfigurationException, XPathExpressionException { 966 // XPathFactory is not thread safe 967 XPath xPath = getXPathFactory().newXPath(); 968 return xPath.compile(OBTAIN_ALL_NS_XPATH); 969 } 970 971 protected DefaultNamespaceContext createNamespaceContext(XPathFactory factory) { 972 DefaultNamespaceContext context = new DefaultNamespaceContext(factory); 973 populateDefaultNamespaces(context); 974 return context; 975 } 976 977 /** 978 * Populate a number of standard prefixes if they are not already there 979 */ 980 protected void populateDefaultNamespaces(DefaultNamespaceContext context) { 981 setNamespaceIfNotPresent(context, "in", IN_NAMESPACE); 982 setNamespaceIfNotPresent(context, "out", OUT_NAMESPACE); 983 setNamespaceIfNotPresent(context, "env", Namespaces.ENVIRONMENT_VARIABLES); 984 setNamespaceIfNotPresent(context, "system", Namespaces.SYSTEM_PROPERTIES_NAMESPACE); 985 setNamespaceIfNotPresent(context, "function", Namespaces.FUNCTION_NAMESPACE); 986 } 987 988 protected void setNamespaceIfNotPresent(DefaultNamespaceContext context, String prefix, String uri) { 989 if (context != null) { 990 String current = context.getNamespaceURI(prefix); 991 if (current == null) { 992 context.add(prefix, uri); 993 } 994 } 995 } 996 997 protected XPathFunctionResolver createDefaultFunctionResolver(final XPathFunctionResolver parent) { 998 return new XPathFunctionResolver() { 999 public XPathFunction resolveFunction(QName qName, int argumentCount) { 1000 XPathFunction answer = null; 1001 if (parent != null) { 1002 answer = parent.resolveFunction(qName, argumentCount); 1003 } 1004 if (answer == null) { 1005 if (isMatchingNamespaceOrEmptyNamespace(qName.getNamespaceURI(), IN_NAMESPACE) 1006 || isMatchingNamespaceOrEmptyNamespace(qName.getNamespaceURI(), DEFAULT_NAMESPACE)) { 1007 String localPart = qName.getLocalPart(); 1008 if (localPart.equals("body") && argumentCount == 0) { 1009 return getBodyFunction(); 1010 } 1011 if (localPart.equals("header") && argumentCount == 1) { 1012 return getHeaderFunction(); 1013 } 1014 } 1015 if (isMatchingNamespaceOrEmptyNamespace(qName.getNamespaceURI(), OUT_NAMESPACE)) { 1016 String localPart = qName.getLocalPart(); 1017 if (localPart.equals("body") && argumentCount == 0) { 1018 return getOutBodyFunction(); 1019 } 1020 if (localPart.equals("header") && argumentCount == 1) { 1021 return getOutHeaderFunction(); 1022 } 1023 } 1024 if (isMatchingNamespaceOrEmptyNamespace(qName.getNamespaceURI(), FUNCTION_NAMESPACE)) { 1025 String localPart = qName.getLocalPart(); 1026 if (localPart.equals("properties") && argumentCount == 1) { 1027 return getPropertiesFunction(); 1028 } 1029 if (localPart.equals("simple") && argumentCount == 1) { 1030 return getSimpleFunction(); 1031 } 1032 } 1033 } 1034 return answer; 1035 } 1036 }; 1037 } 1038 1039 /** 1040 * Checks whether we need an {@link InputStream} to access the message body. 1041 * <p/> 1042 * Depending on the content in the message body, we may not need to convert 1043 * to {@link InputStream}. 1044 * 1045 * @param exchange the current exchange 1046 * @return <tt>true</tt> to convert to {@link InputStream} beforehand converting afterwards. 1047 */ 1048 protected boolean isInputStreamNeeded(Exchange exchange) { 1049 Object body = exchange.getIn().getBody(); 1050 return isInputStreamNeededForObject(exchange, body); 1051 } 1052 1053 /** 1054 * Checks whether we need an {@link InputStream} to access the message header. 1055 * <p/> 1056 * Depending on the content in the message header, we may not need to convert 1057 * to {@link InputStream}. 1058 * 1059 * @param exchange the current exchange 1060 * @return <tt>true</tt> to convert to {@link InputStream} beforehand converting afterwards. 1061 */ 1062 protected boolean isInputStreamNeeded(Exchange exchange, String headerName) { 1063 Object header = exchange.getIn().getHeader(headerName); 1064 return isInputStreamNeededForObject(exchange, header); 1065 } 1066 1067 /** 1068 * Checks whether we need an {@link InputStream} to access this object 1069 * <p/> 1070 * Depending on the content in the object, we may not need to convert 1071 * to {@link InputStream}. 1072 * 1073 * @param exchange the current exchange 1074 * @return <tt>true</tt> to convert to {@link InputStream} beforehand converting afterwards. 1075 */ 1076 protected boolean isInputStreamNeededForObject(Exchange exchange, Object obj) { 1077 if (obj == null) { 1078 return false; 1079 } 1080 1081 if (obj instanceof WrappedFile) { 1082 obj = ((WrappedFile<?>) obj).getFile(); 1083 } 1084 if (obj instanceof File) { 1085 // input stream is needed for File to avoid locking the file in case of errors etc 1086 return true; 1087 } 1088 1089 // input stream is not needed otherwise 1090 return false; 1091 } 1092 1093 /** 1094 * Strategy method to extract the document from the exchange. 1095 */ 1096 protected Object getDocument(Exchange exchange, Object body) { 1097 try { 1098 return doGetDocument(exchange, body); 1099 } catch (Exception e) { 1100 throw ObjectHelper.wrapRuntimeCamelException(e); 1101 } finally { 1102 // call the reset if the in message body is StreamCache 1103 MessageHelper.resetStreamCache(exchange.getIn()); 1104 } 1105 } 1106 1107 protected Object doGetDocument(Exchange exchange, Object body) throws Exception { 1108 if (body == null) { 1109 return null; 1110 } 1111 1112 Object answer = null; 1113 1114 Class<?> type = getDocumentType(); 1115 Exception cause = null; 1116 if (type != null) { 1117 // try to get the body as the desired type 1118 try { 1119 answer = exchange.getContext().getTypeConverter().convertTo(type, exchange, body); 1120 } catch (Exception e) { 1121 // we want to store the caused exception, if we could not convert 1122 cause = e; 1123 } 1124 } 1125 1126 if (type == null && answer == null) { 1127 // fallback to get the body as is 1128 answer = body; 1129 } else if (answer == null) { 1130 // there was a type, and we could not convert to it, then fail 1131 if (cause != null) { 1132 throw cause; 1133 } else { 1134 throw new NoTypeConversionAvailableException(body, type); 1135 } 1136 } 1137 1138 return answer; 1139 } 1140 1141 private MessageVariableResolver getVariableResolver() { 1142 return variableResolver; 1143 } 1144 1145 @Override 1146 public void doStart() throws Exception { 1147 if (xpathFactory == null) { 1148 xpathFactory = createXPathFactory(); 1149 } 1150 if (namespaceContext == null) { 1151 namespaceContext = createNamespaceContext(xpathFactory); 1152 } 1153 for (Map.Entry<String, String> entry : namespaces.entrySet()) { 1154 namespaceContext.add(entry.getKey(), entry.getValue()); 1155 } 1156 1157 // create default functions if no custom assigned 1158 if (bodyFunction == null) { 1159 bodyFunction = createBodyFunction(); 1160 } 1161 if (headerFunction == null) { 1162 headerFunction = createHeaderFunction(); 1163 } 1164 if (outBodyFunction == null) { 1165 outBodyFunction = createOutBodyFunction(); 1166 } 1167 if (outHeaderFunction == null) { 1168 outHeaderFunction = createOutHeaderFunction(); 1169 } 1170 if (propertiesFunction == null) { 1171 propertiesFunction = createPropertiesFunction(); 1172 } 1173 if (simpleFunction == null) { 1174 simpleFunction = createSimpleFunction(); 1175 } 1176 } 1177 1178 @Override 1179 public void doStop() throws Exception { 1180 pool.clear(); 1181 poolLogNamespaces.clear(); 1182 } 1183 1184 protected synchronized XPathFactory createXPathFactory() throws XPathFactoryConfigurationException { 1185 if (objectModelUri != null) { 1186 xpathFactory = XPathFactory.newInstance(objectModelUri); 1187 LOG.info("Using objectModelUri " + objectModelUri + " when created XPathFactory {}", xpathFactory); 1188 return xpathFactory; 1189 } 1190 1191 if (defaultXPathFactory == null) { 1192 defaultXPathFactory = createDefaultXPathFactory(); 1193 } 1194 return defaultXPathFactory; 1195 } 1196 1197 protected static XPathFactory createDefaultXPathFactory() throws XPathFactoryConfigurationException { 1198 XPathFactory factory = null; 1199 1200 // read system property and see if there is a factory set 1201 Properties properties = System.getProperties(); 1202 for (Map.Entry<Object, Object> prop : properties.entrySet()) { 1203 String key = (String) prop.getKey(); 1204 if (key.startsWith(XPathFactory.DEFAULT_PROPERTY_NAME)) { 1205 String uri = ObjectHelper.after(key, ":"); 1206 if (uri != null) { 1207 factory = XPathFactory.newInstance(uri); 1208 LOG.info("Using system property {} with value {} when created default XPathFactory {}", new Object[]{key, uri, factory}); 1209 } 1210 } 1211 } 1212 1213 if (factory == null) { 1214 factory = XPathFactory.newInstance(); 1215 LOG.info("Created default XPathFactory {}", factory); 1216 } 1217 1218 return factory; 1219 } 1220 1221}