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