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