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: 640438 $
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 }