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