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