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 }