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