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