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