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