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