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.StringReader; 020 import java.util.List; 021 import java.util.Map; 022 023 import javax.xml.namespace.QName; 024 import javax.xml.transform.dom.DOMSource; 025 import javax.xml.xpath.XPath; 026 import javax.xml.xpath.XPathConstants; 027 import javax.xml.xpath.XPathExpression; 028 import javax.xml.xpath.XPathExpressionException; 029 import javax.xml.xpath.XPathFactory; 030 import javax.xml.xpath.XPathFactoryConfigurationException; 031 import javax.xml.xpath.XPathFunction; 032 import javax.xml.xpath.XPathFunctionException; 033 import javax.xml.xpath.XPathFunctionResolver; 034 035 import org.w3c.dom.Document; 036 import org.w3c.dom.Node; 037 import org.w3c.dom.NodeList; 038 039 import org.xml.sax.InputSource; 040 041 import org.apache.camel.Exchange; 042 import org.apache.camel.Expression; 043 import org.apache.camel.Message; 044 import org.apache.camel.Predicate; 045 import org.apache.camel.RuntimeExpressionException; 046 import org.apache.camel.spi.NamespaceAware; 047 import org.apache.camel.util.ExchangeHelper; 048 import org.apache.camel.util.MessageHelper; 049 050 import static org.apache.camel.builder.xml.Namespaces.DEFAULT_NAMESPACE; 051 import static org.apache.camel.builder.xml.Namespaces.IN_NAMESPACE; 052 import static org.apache.camel.builder.xml.Namespaces.OUT_NAMESPACE; 053 import static org.apache.camel.builder.xml.Namespaces.isMatchingNamespaceOrEmptyNamespace; 054 055 /** 056 * Creates an XPath expression builder which creates a nodeset result by default. 057 * If you want to evaluate a String expression then call {@link #stringResult()} 058 * 059 * @see XPathConstants#NODESET 060 * 061 * @version $Revision: 769448 $ 062 */ 063 public class XPathBuilder implements Expression, Predicate, NamespaceAware { 064 private final String text; 065 private XPathFactory xpathFactory; 066 private Class<?> documentType = Document.class; 067 // For some reason the default expression of "a/b" on a document such as 068 // <a><b>1</b><b>2</b></a> 069 // will evaluate as just "1" by default which is bizarre. So by default 070 // lets assume XPath expressions result in nodesets. 071 private Class<?> resultType; 072 private QName resultQName = XPathConstants.NODESET; 073 private String objectModelUri; 074 private DefaultNamespaceContext namespaceContext; 075 private XPathFunctionResolver functionResolver; 076 private XPathExpression expression; 077 private MessageVariableResolver variableResolver = new MessageVariableResolver(); 078 private Exchange exchange; 079 private XPathFunction bodyFunction; 080 private XPathFunction headerFunction; 081 private XPathFunction outBodyFunction; 082 private XPathFunction outHeaderFunction; 083 084 public XPathBuilder(String text) { 085 this.text = text; 086 } 087 088 public static XPathBuilder xpath(String text) { 089 return new XPathBuilder(text); 090 } 091 092 public static XPathBuilder xpath(String text, Class resultType) { 093 XPathBuilder builder = new XPathBuilder(text); 094 builder.setResultType(resultType); 095 return builder; 096 } 097 098 @Override 099 public String toString() { 100 return "XPath: " + text; 101 } 102 103 public boolean matches(Exchange exchange) { 104 Object booleanResult = evaluateAs(exchange, XPathConstants.BOOLEAN); 105 return exchange.getContext().getTypeConverter().convertTo(Boolean.class, booleanResult); 106 } 107 108 public void assertMatches(String text, Exchange exchange) throws AssertionError { 109 Object booleanResult = evaluateAs(exchange, XPathConstants.BOOLEAN); 110 Boolean answer = exchange.getContext().getTypeConverter().convertTo(Boolean.class, booleanResult); 111 if (answer == null) { 112 throw new AssertionError(this + " failed on " + exchange + " as returned <" + booleanResult + ">"); 113 } 114 } 115 116 public Object evaluate(Exchange exchange) { 117 Object answer = evaluateAs(exchange, resultQName); 118 if (resultType != null) { 119 return ExchangeHelper.convertToType(exchange, resultType, answer); 120 } 121 return answer; 122 } 123 124 public <T> T evaluate(Exchange exchange, Class<T> type) { 125 Object result = evaluate(exchange); 126 return exchange.getContext().getTypeConverter().convertTo(type, result); 127 } 128 129 // Builder methods 130 // ------------------------------------------------------------------------- 131 132 /** 133 * Sets the expression result type to boolean 134 * 135 * @return the current builder 136 */ 137 public XPathBuilder booleanResult() { 138 resultQName = XPathConstants.BOOLEAN; 139 return this; 140 } 141 142 /** 143 * Sets the expression result type to boolean 144 * 145 * @return the current builder 146 */ 147 public XPathBuilder nodeResult() { 148 resultQName = XPathConstants.NODE; 149 return this; 150 } 151 152 /** 153 * Sets the expression result type to boolean 154 * 155 * @return the current builder 156 */ 157 public XPathBuilder nodeSetResult() { 158 resultQName = XPathConstants.NODESET; 159 return this; 160 } 161 162 /** 163 * Sets the expression result type to boolean 164 * 165 * @return the current builder 166 */ 167 public XPathBuilder numberResult() { 168 resultQName = XPathConstants.NUMBER; 169 return this; 170 } 171 172 /** 173 * Sets the expression result type to boolean 174 * 175 * @return the current builder 176 */ 177 public XPathBuilder stringResult() { 178 resultQName = XPathConstants.STRING; 179 return this; 180 } 181 182 /** 183 * Sets the expression result type to boolean 184 * 185 * @return the current builder 186 */ 187 public XPathBuilder resultType(Class<?> resultType) { 188 setResultType(resultType); 189 return this; 190 } 191 192 /** 193 * Sets the object model URI to use 194 * 195 * @return the current builder 196 */ 197 public XPathBuilder objectModel(String uri) { 198 this.objectModelUri = uri; 199 return this; 200 } 201 202 /** 203 * Sets the {@link XPathFunctionResolver} instance to use on these XPath 204 * expressions 205 * 206 * @return the current builder 207 */ 208 public XPathBuilder functionResolver(XPathFunctionResolver functionResolver) { 209 this.functionResolver = functionResolver; 210 return this; 211 } 212 213 /** 214 * Registers the namespace prefix and URI with the builder so that the 215 * prefix can be used in XPath expressions 216 * 217 * @param prefix is the namespace prefix that can be used in the XPath 218 * expressions 219 * @param uri is the namespace URI to which the prefix refers 220 * @return the current builder 221 */ 222 public XPathBuilder namespace(String prefix, String uri) { 223 getNamespaceContext().add(prefix, uri); 224 return this; 225 } 226 227 /** 228 * Registers namespaces with the builder so that the registered 229 * prefixes can be used in XPath expressions 230 * 231 * @param namespaces is namespaces object that should be used in the 232 * XPath expression 233 * @return the current builder 234 */ 235 public XPathBuilder namespaces(Namespaces namespaces) { 236 namespaces.configure(this); 237 return this; 238 } 239 240 /** 241 * Registers a variable (in the global namespace) which can be referred to 242 * from XPath expressions 243 */ 244 public XPathBuilder variable(String name, Object value) { 245 variableResolver.addVariable(name, value); 246 return this; 247 } 248 249 // Properties 250 // ------------------------------------------------------------------------- 251 public XPathFactory getXPathFactory() throws XPathFactoryConfigurationException { 252 if (xpathFactory == null) { 253 if (objectModelUri != null) { 254 xpathFactory = XPathFactory.newInstance(objectModelUri); 255 } 256 xpathFactory = XPathFactory.newInstance(); 257 } 258 return xpathFactory; 259 } 260 261 public void setXPathFactory(XPathFactory xpathFactory) { 262 this.xpathFactory = xpathFactory; 263 } 264 265 public Class getDocumentType() { 266 return documentType; 267 } 268 269 public void setDocumentType(Class documentType) { 270 this.documentType = documentType; 271 } 272 273 public String getText() { 274 return text; 275 } 276 277 public QName getResultQName() { 278 return resultQName; 279 } 280 281 public void setResultQName(QName resultQName) { 282 this.resultQName = resultQName; 283 } 284 285 public DefaultNamespaceContext getNamespaceContext() { 286 if (namespaceContext == null) { 287 try { 288 DefaultNamespaceContext defaultNamespaceContext = new DefaultNamespaceContext(getXPathFactory()); 289 populateDefaultNamespaces(defaultNamespaceContext); 290 namespaceContext = defaultNamespaceContext; 291 } catch (XPathFactoryConfigurationException e) { 292 throw new RuntimeExpressionException(e); 293 } 294 } 295 return namespaceContext; 296 } 297 298 public void setNamespaceContext(DefaultNamespaceContext namespaceContext) { 299 this.namespaceContext = namespaceContext; 300 } 301 302 public XPathFunctionResolver getFunctionResolver() { 303 return functionResolver; 304 } 305 306 public void setFunctionResolver(XPathFunctionResolver functionResolver) { 307 this.functionResolver = functionResolver; 308 } 309 310 public XPathExpression getExpression() throws XPathFactoryConfigurationException, 311 XPathExpressionException { 312 if (expression == null) { 313 expression = createXPathExpression(); 314 } 315 return expression; 316 } 317 318 public void setNamespaces(Map<String, String> namespaces) { 319 getNamespaceContext().setNamespaces(namespaces); 320 } 321 322 public XPathFunction getBodyFunction() { 323 if (bodyFunction == null) { 324 bodyFunction = new XPathFunction() { 325 public Object evaluate(List list) throws XPathFunctionException { 326 if (exchange == null) { 327 return null; 328 } 329 return exchange.getIn().getBody(); 330 } 331 }; 332 } 333 return bodyFunction; 334 } 335 336 public void setBodyFunction(XPathFunction bodyFunction) { 337 this.bodyFunction = bodyFunction; 338 } 339 340 public XPathFunction getHeaderFunction() { 341 if (headerFunction == null) { 342 headerFunction = new XPathFunction() { 343 public Object evaluate(List list) throws XPathFunctionException { 344 if (exchange != null && !list.isEmpty()) { 345 Object value = list.get(0); 346 if (value != null) { 347 return exchange.getIn().getHeader(value.toString()); 348 } 349 } 350 return null; 351 } 352 }; 353 } 354 return headerFunction; 355 } 356 357 public void setHeaderFunction(XPathFunction headerFunction) { 358 this.headerFunction = headerFunction; 359 } 360 361 public XPathFunction getOutBodyFunction() { 362 if (outBodyFunction == null) { 363 outBodyFunction = new XPathFunction() { 364 public Object evaluate(List list) throws XPathFunctionException { 365 if (exchange != null && exchange.hasOut()) { 366 return exchange.getOut().getBody(); 367 } 368 return null; 369 } 370 }; 371 } 372 return outBodyFunction; 373 } 374 375 public void setOutBodyFunction(XPathFunction outBodyFunction) { 376 this.outBodyFunction = outBodyFunction; 377 } 378 379 public XPathFunction getOutHeaderFunction() { 380 if (outHeaderFunction == null) { 381 outHeaderFunction = new XPathFunction() { 382 public Object evaluate(List list) throws XPathFunctionException { 383 if (exchange != null && !list.isEmpty()) { 384 Object value = list.get(0); 385 if (value != null) { 386 return exchange.getOut().getHeader(value.toString()); 387 } 388 } 389 return null; 390 } 391 }; 392 } 393 return outHeaderFunction; 394 } 395 396 public void setOutHeaderFunction(XPathFunction outHeaderFunction) { 397 this.outHeaderFunction = outHeaderFunction; 398 } 399 400 public Class getResultType() { 401 return resultType; 402 } 403 404 public void setResultType(Class resultType) { 405 this.resultType = resultType; 406 if (Number.class.isAssignableFrom(resultType)) { 407 numberResult(); 408 } else if (String.class.isAssignableFrom(resultType)) { 409 stringResult(); 410 } else if (Boolean.class.isAssignableFrom(resultType)) { 411 booleanResult(); 412 } else if (Node.class.isAssignableFrom(resultType)) { 413 nodeResult(); 414 } else if (NodeList.class.isAssignableFrom(resultType)) { 415 nodeSetResult(); 416 } 417 } 418 419 // Implementation methods 420 // ------------------------------------------------------------------------- 421 422 /** 423 * Evaluates the expression as the given result type 424 */ 425 protected synchronized Object evaluateAs(Exchange exchange, QName resultQName) { 426 this.exchange = exchange; 427 variableResolver.setExchange(exchange); 428 try { 429 Object document = getDocument(exchange); 430 if (resultQName != null) { 431 if (document instanceof InputSource) { 432 InputSource inputSource = (InputSource)document; 433 return getExpression().evaluate(inputSource, resultQName); 434 } else if (document instanceof DOMSource) { 435 DOMSource source = (DOMSource) document; 436 return getExpression().evaluate(source.getNode(), resultQName); 437 } else { 438 return getExpression().evaluate(document, resultQName); 439 } 440 } else { 441 if (document instanceof InputSource) { 442 InputSource inputSource = (InputSource)document; 443 return getExpression().evaluate(inputSource); 444 } else if (document instanceof DOMSource) { 445 DOMSource source = (DOMSource)document; 446 return getExpression().evaluate(source.getNode()); 447 } else { 448 return getExpression().evaluate(document); 449 } 450 } 451 } catch (XPathExpressionException e) { 452 throw new InvalidXPathExpression(getText(), e); 453 } catch (XPathFactoryConfigurationException e) { 454 throw new InvalidXPathExpression(getText(), e); 455 } 456 } 457 458 protected XPathExpression createXPathExpression() throws XPathExpressionException, 459 XPathFactoryConfigurationException { 460 XPath xPath = getXPathFactory().newXPath(); 461 462 // lets now clear any factory references to avoid keeping them around 463 xpathFactory = null; 464 465 xPath.setNamespaceContext(getNamespaceContext()); 466 467 xPath.setXPathVariableResolver(variableResolver); 468 469 XPathFunctionResolver parentResolver = getFunctionResolver(); 470 if (parentResolver == null) { 471 parentResolver = xPath.getXPathFunctionResolver(); 472 } 473 xPath.setXPathFunctionResolver(createDefaultFunctionResolver(parentResolver)); 474 return xPath.compile(text); 475 } 476 477 /** 478 * Lets populate a number of standard prefixes if they are not already there 479 */ 480 protected void populateDefaultNamespaces(DefaultNamespaceContext context) { 481 setNamespaceIfNotPresent(context, "in", IN_NAMESPACE); 482 setNamespaceIfNotPresent(context, "out", OUT_NAMESPACE); 483 setNamespaceIfNotPresent(context, "env", Namespaces.ENVIRONMENT_VARIABLES); 484 setNamespaceIfNotPresent(context, "system", Namespaces.SYSTEM_PROPERTIES_NAMESPACE); 485 } 486 487 protected void setNamespaceIfNotPresent(DefaultNamespaceContext context, String prefix, String uri) { 488 if (context != null) { 489 String current = context.getNamespaceURI(prefix); 490 if (current == null) { 491 context.add(prefix, uri); 492 } 493 } 494 } 495 496 protected XPathFunctionResolver createDefaultFunctionResolver(final XPathFunctionResolver parent) { 497 return new XPathFunctionResolver() { 498 public XPathFunction resolveFunction(QName qName, int argumentCount) { 499 XPathFunction answer = null; 500 if (parent != null) { 501 answer = parent.resolveFunction(qName, argumentCount); 502 } 503 if (answer == null) { 504 if (isMatchingNamespaceOrEmptyNamespace(qName.getNamespaceURI(), IN_NAMESPACE) 505 || isMatchingNamespaceOrEmptyNamespace(qName.getNamespaceURI(), DEFAULT_NAMESPACE)) { 506 String localPart = qName.getLocalPart(); 507 if (localPart.equals("body") && argumentCount == 0) { 508 return getBodyFunction(); 509 } 510 if (localPart.equals("header") && argumentCount == 1) { 511 return getHeaderFunction(); 512 } 513 } 514 if (isMatchingNamespaceOrEmptyNamespace(qName.getNamespaceURI(), OUT_NAMESPACE)) { 515 String localPart = qName.getLocalPart(); 516 if (localPart.equals("body") && argumentCount == 0) { 517 return getOutBodyFunction(); 518 } 519 if (localPart.equals("header") && argumentCount == 1) { 520 return getOutHeaderFunction(); 521 } 522 } 523 } 524 return answer; 525 } 526 }; 527 } 528 529 /** 530 * Strategy method to extract the document from the exchange 531 */ 532 @SuppressWarnings("unchecked") 533 protected Object getDocument(Exchange exchange) { 534 Message in = exchange.getIn(); 535 Class type = getDocumentType(); 536 Object answer = null; 537 if (type != null) { 538 answer = in.getBody(type); 539 } 540 if (answer == null) { 541 answer = in.getBody(); 542 } 543 544 // lets try coerce some common types into something JAXP can deal with 545 if (answer instanceof String) { 546 answer = new InputSource(new StringReader(answer.toString())); 547 } 548 549 // call the reset if the in message body is StreamCache 550 MessageHelper.resetStreamCache(exchange.getIn()); 551 return answer; 552 } 553 }