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 }