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