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.InputStream;
020 import java.io.StringReader;
021 import java.util.List;
022 import java.util.Map;
023 import java.util.Queue;
024 import java.util.concurrent.ConcurrentLinkedQueue;
025
026 import javax.xml.namespace.QName;
027 import javax.xml.transform.dom.DOMSource;
028 import javax.xml.xpath.XPath;
029 import javax.xml.xpath.XPathConstants;
030 import javax.xml.xpath.XPathExpression;
031 import javax.xml.xpath.XPathExpressionException;
032 import javax.xml.xpath.XPathFactory;
033 import javax.xml.xpath.XPathFactoryConfigurationException;
034 import javax.xml.xpath.XPathFunction;
035 import javax.xml.xpath.XPathFunctionException;
036 import javax.xml.xpath.XPathFunctionResolver;
037
038 import org.w3c.dom.Document;
039 import org.w3c.dom.Node;
040 import org.w3c.dom.NodeList;
041
042 import org.xml.sax.InputSource;
043
044 import org.apache.camel.Exchange;
045 import org.apache.camel.Expression;
046 import org.apache.camel.Message;
047 import org.apache.camel.Predicate;
048 import org.apache.camel.RuntimeExpressionException;
049 import org.apache.camel.Service;
050 import org.apache.camel.component.bean.BeanInvocation;
051 import org.apache.camel.component.file.GenericFile;
052 import org.apache.camel.spi.NamespaceAware;
053 import org.apache.camel.util.ExchangeHelper;
054 import org.apache.camel.util.MessageHelper;
055
056 import org.apache.commons.logging.Log;
057 import org.apache.commons.logging.LogFactory;
058
059 import static org.apache.camel.builder.xml.Namespaces.DEFAULT_NAMESPACE;
060 import static org.apache.camel.builder.xml.Namespaces.IN_NAMESPACE;
061 import static org.apache.camel.builder.xml.Namespaces.OUT_NAMESPACE;
062 import static org.apache.camel.builder.xml.Namespaces.isMatchingNamespaceOrEmptyNamespace;
063
064 /**
065 * Creates an XPath expression builder which creates a nodeset result by default.
066 * If you want to evaluate a String expression then call {@link #stringResult()}
067 * <p/>
068 * An XPath object is not thread-safe and not reentrant. In other words, it is the application's responsibility to make
069 * sure that one XPath object is not used from more than one thread at any given time, and while the evaluate method
070 * is invoked, applications may not recursively call the evaluate method.
071 * <p/>
072 * This implementation is thread safe by using thread locals and pooling to allow concurrency
073 *
074 * @see XPathConstants#NODESET
075 *
076 * @version $Revision: 836037 $
077 */
078 public class XPathBuilder implements Expression, Predicate, NamespaceAware, Service {
079 private static final transient Log LOG = LogFactory.getLog(XPathBuilder.class);
080 private final Queue<XPathExpression> pool = new ConcurrentLinkedQueue<XPathExpression>();
081 private final String text;
082 private final ThreadLocal<MessageVariableResolver> variableResolver = new ThreadLocal<MessageVariableResolver>();
083 private final ThreadLocal<Exchange> exchange = new ThreadLocal<Exchange>();
084
085 private XPathFactory xpathFactory;
086 private Class<?> documentType = Document.class;
087 // For some reason the default expression of "a/b" on a document such as
088 // <a><b>1</b><b>2</b></a>
089 // will evaluate as just "1" by default which is bizarre. So by default
090 // lets assume XPath expressions result in nodesets.
091 private Class<?> resultType;
092 private QName resultQName = XPathConstants.NODESET;
093 private String objectModelUri;
094 private DefaultNamespaceContext namespaceContext;
095 private XPathFunctionResolver functionResolver;
096 private XPathFunction bodyFunction;
097 private XPathFunction headerFunction;
098 private XPathFunction outBodyFunction;
099 private XPathFunction outHeaderFunction;
100
101 public XPathBuilder(String text) {
102 this.text = text;
103 }
104
105 public static XPathBuilder xpath(String text) {
106 return new XPathBuilder(text);
107 }
108
109 public static XPathBuilder xpath(String text, Class<?> resultType) {
110 XPathBuilder builder = new XPathBuilder(text);
111 builder.setResultType(resultType);
112 return builder;
113 }
114
115 @Override
116 public String toString() {
117 return "XPath: " + text;
118 }
119
120 public boolean matches(Exchange exchange) {
121 Object booleanResult = evaluateAs(exchange, XPathConstants.BOOLEAN);
122 return exchange.getContext().getTypeConverter().convertTo(Boolean.class, booleanResult);
123 }
124
125 public <T> T evaluate(Exchange exchange, Class<T> type) {
126 Object result = evaluate(exchange);
127 return exchange.getContext().getTypeConverter().convertTo(type, result);
128 }
129
130 // Builder methods
131 // -------------------------------------------------------------------------
132
133 /**
134 * Sets the expression result type to boolean
135 *
136 * @return the current builder
137 */
138 public XPathBuilder booleanResult() {
139 resultQName = XPathConstants.BOOLEAN;
140 return this;
141 }
142
143 /**
144 * Sets the expression result type to boolean
145 *
146 * @return the current builder
147 */
148 public XPathBuilder nodeResult() {
149 resultQName = XPathConstants.NODE;
150 return this;
151 }
152
153 /**
154 * Sets the expression result type to boolean
155 *
156 * @return the current builder
157 */
158 public XPathBuilder nodeSetResult() {
159 resultQName = XPathConstants.NODESET;
160 return this;
161 }
162
163 /**
164 * Sets the expression result type to boolean
165 *
166 * @return the current builder
167 */
168 public XPathBuilder numberResult() {
169 resultQName = XPathConstants.NUMBER;
170 return this;
171 }
172
173 /**
174 * Sets the expression result type to boolean
175 *
176 * @return the current builder
177 */
178 public XPathBuilder stringResult() {
179 resultQName = XPathConstants.STRING;
180 return this;
181 }
182
183 /**
184 * Sets the expression result type to boolean
185 *
186 * @return the current builder
187 */
188 public XPathBuilder resultType(Class<?> resultType) {
189 setResultType(resultType);
190 return this;
191 }
192
193 /**
194 * Sets the object model URI to use
195 *
196 * @return the current builder
197 */
198 public XPathBuilder objectModel(String uri) {
199 this.objectModelUri = uri;
200 return this;
201 }
202
203 /**
204 * Sets the {@link XPathFunctionResolver} instance to use on these XPath
205 * expressions
206 *
207 * @return the current builder
208 */
209 public XPathBuilder functionResolver(XPathFunctionResolver functionResolver) {
210 this.functionResolver = functionResolver;
211 return this;
212 }
213
214 /**
215 * Registers the namespace prefix and URI with the builder so that the
216 * prefix can be used in XPath expressions
217 *
218 * @param prefix is the namespace prefix that can be used in the XPath
219 * expressions
220 * @param uri is the namespace URI to which the prefix refers
221 * @return the current builder
222 */
223 public XPathBuilder namespace(String prefix, String uri) {
224 getNamespaceContext().add(prefix, uri);
225 return this;
226 }
227
228 /**
229 * Registers namespaces with the builder so that the registered
230 * prefixes can be used in XPath expressions
231 *
232 * @param namespaces is namespaces object that should be used in the
233 * XPath expression
234 * @return the current builder
235 */
236 public XPathBuilder namespaces(Namespaces namespaces) {
237 namespaces.configure(this);
238 return this;
239 }
240
241 /**
242 * Registers a variable (in the global namespace) which can be referred to
243 * from XPath expressions
244 */
245 public XPathBuilder variable(String name, Object value) {
246 getVariableResolver().addVariable(name, value);
247 return this;
248 }
249
250 /**
251 * Configures the document type to use.
252 * <p/>
253 * The document type controls which kind of Class Camel should convert the payload
254 * to before doing the xpath evaluation.
255 * <p/>
256 * For example you can set it to {@link InputSource} to use SAX streams.
257 * By default Camel uses {@link Document} as the type.
258 *
259 * @param documentType the document type
260 * @return the current builder
261 */
262 public XPathBuilder documentType(Class<?> documentType) {
263 setDocumentType(documentType);
264 return this;
265 }
266
267 // Properties
268 // -------------------------------------------------------------------------
269 public XPathFactory getXPathFactory() throws XPathFactoryConfigurationException {
270 if (xpathFactory == null) {
271 if (objectModelUri != null) {
272 xpathFactory = XPathFactory.newInstance(objectModelUri);
273 }
274 xpathFactory = XPathFactory.newInstance();
275 }
276 return xpathFactory;
277 }
278
279 public void setXPathFactory(XPathFactory xpathFactory) {
280 this.xpathFactory = xpathFactory;
281 }
282
283 public Class<?> getDocumentType() {
284 return documentType;
285 }
286
287 public void setDocumentType(Class<?> documentType) {
288 this.documentType = documentType;
289 }
290
291 public String getText() {
292 return text;
293 }
294
295 public QName getResultQName() {
296 return resultQName;
297 }
298
299 public void setResultQName(QName resultQName) {
300 this.resultQName = resultQName;
301 }
302
303 public DefaultNamespaceContext getNamespaceContext() {
304 if (namespaceContext == null) {
305 try {
306 DefaultNamespaceContext defaultNamespaceContext = new DefaultNamespaceContext(getXPathFactory());
307 populateDefaultNamespaces(defaultNamespaceContext);
308 namespaceContext = defaultNamespaceContext;
309 } catch (XPathFactoryConfigurationException e) {
310 throw new RuntimeExpressionException(e);
311 }
312 }
313 return namespaceContext;
314 }
315
316 public void setNamespaceContext(DefaultNamespaceContext namespaceContext) {
317 this.namespaceContext = namespaceContext;
318 }
319
320 public XPathFunctionResolver getFunctionResolver() {
321 return functionResolver;
322 }
323
324 public void setFunctionResolver(XPathFunctionResolver functionResolver) {
325 this.functionResolver = functionResolver;
326 }
327
328 public void setNamespaces(Map<String, String> namespaces) {
329 getNamespaceContext().setNamespaces(namespaces);
330 }
331
332 public XPathFunction getBodyFunction() {
333 if (bodyFunction == null) {
334 bodyFunction = new XPathFunction() {
335 @SuppressWarnings("unchecked")
336 public Object evaluate(List list) throws XPathFunctionException {
337 if (exchange == null) {
338 return null;
339 }
340 return exchange.get().getIn().getBody();
341 }
342 };
343 }
344 return bodyFunction;
345 }
346
347 public void setBodyFunction(XPathFunction bodyFunction) {
348 this.bodyFunction = bodyFunction;
349 }
350
351 public XPathFunction getHeaderFunction() {
352 if (headerFunction == null) {
353 headerFunction = new XPathFunction() {
354 @SuppressWarnings("unchecked")
355 public Object evaluate(List list) throws XPathFunctionException {
356 if (exchange != null && !list.isEmpty()) {
357 Object value = list.get(0);
358 if (value != null) {
359 return exchange.get().getIn().getHeader(value.toString());
360 }
361 }
362 return null;
363 }
364 };
365 }
366 return headerFunction;
367 }
368
369 public void setHeaderFunction(XPathFunction headerFunction) {
370 this.headerFunction = headerFunction;
371 }
372
373 public XPathFunction getOutBodyFunction() {
374 if (outBodyFunction == null) {
375 outBodyFunction = new XPathFunction() {
376 @SuppressWarnings("unchecked")
377 public Object evaluate(List list) throws XPathFunctionException {
378 if (exchange.get() != null && exchange.get().hasOut()) {
379 return exchange.get().getOut().getBody();
380 }
381 return null;
382 }
383 };
384 }
385 return outBodyFunction;
386 }
387
388 public void setOutBodyFunction(XPathFunction outBodyFunction) {
389 this.outBodyFunction = outBodyFunction;
390 }
391
392 public XPathFunction getOutHeaderFunction() {
393 if (outHeaderFunction == null) {
394 outHeaderFunction = new XPathFunction() {
395 @SuppressWarnings("unchecked")
396 public Object evaluate(List list) throws XPathFunctionException {
397 if (exchange.get() != null && !list.isEmpty()) {
398 Object value = list.get(0);
399 if (value != null) {
400 return exchange.get().getOut().getHeader(value.toString());
401 }
402 }
403 return null;
404 }
405 };
406 }
407 return outHeaderFunction;
408 }
409
410 public void setOutHeaderFunction(XPathFunction outHeaderFunction) {
411 this.outHeaderFunction = outHeaderFunction;
412 }
413
414 public Class<?> getResultType() {
415 return resultType;
416 }
417
418 public void setResultType(Class<?> resultType) {
419 this.resultType = resultType;
420 if (Number.class.isAssignableFrom(resultType)) {
421 numberResult();
422 } else if (String.class.isAssignableFrom(resultType)) {
423 stringResult();
424 } else if (Boolean.class.isAssignableFrom(resultType)) {
425 booleanResult();
426 } else if (Node.class.isAssignableFrom(resultType)) {
427 nodeResult();
428 } else if (NodeList.class.isAssignableFrom(resultType)) {
429 nodeSetResult();
430 }
431 }
432
433 // Implementation methods
434 // -------------------------------------------------------------------------
435
436 protected Object evaluate(Exchange exchange) {
437 Object answer = evaluateAs(exchange, resultQName);
438 if (resultType != null) {
439 return ExchangeHelper.convertToType(exchange, resultType, answer);
440 }
441 return answer;
442 }
443
444 /**
445 * Evaluates the expression as the given result type
446 */
447 protected Object evaluateAs(Exchange exchange, QName resultQName) {
448 // pool a pre compiled expression from pool
449 XPathExpression xpathExpression = pool.poll();
450 if (xpathExpression == null) {
451 LOG.trace("Creating new XPathExpression as none was available from pool");
452 // no avail in pool then create one
453 try {
454 xpathExpression = createXPathExpression();
455 } catch (XPathExpressionException e) {
456 throw new InvalidXPathExpression(getText(), e);
457 } catch (Exception e) {
458 throw new RuntimeExpressionException("Cannot create xpath expression", e);
459 }
460 } else {
461 LOG.trace("Acquired XPathExpression from pool");
462 }
463 try {
464 return doInEvaluateAs(xpathExpression, exchange, resultQName);
465 } finally {
466 // release it back to the pool
467 pool.add(xpathExpression);
468 LOG.trace("Released XPathExpression back to pool");
469 }
470 }
471
472 protected Object doInEvaluateAs(XPathExpression xpathExpression, Exchange exchange, QName resultQName) {
473 if (LOG.isTraceEnabled()) {
474 LOG.trace("Evaluating exchange: " + exchange + " as: " + resultQName);
475 }
476
477 Object answer;
478
479 // set exchange and variable resolver as thread locals for concurrency
480 this.exchange.set(exchange);
481
482 try {
483 Object document = getDocument(exchange);
484 if (resultQName != null) {
485 if (document instanceof InputSource) {
486 InputSource inputSource = (InputSource) document;
487 answer = xpathExpression.evaluate(inputSource, resultQName);
488 } else if (document instanceof DOMSource) {
489 DOMSource source = (DOMSource) document;
490 answer = xpathExpression.evaluate(source.getNode(), resultQName);
491 } else {
492 answer = xpathExpression.evaluate(document, resultQName);
493 }
494 } else {
495 if (document instanceof InputSource) {
496 InputSource inputSource = (InputSource) document;
497 answer = xpathExpression.evaluate(inputSource);
498 } else if (document instanceof DOMSource) {
499 DOMSource source = (DOMSource) document;
500 answer = xpathExpression.evaluate(source.getNode());
501 } else {
502 answer = xpathExpression.evaluate(document);
503 }
504 }
505 } catch (XPathExpressionException e) {
506 throw new InvalidXPathExpression(getText(), e);
507 }
508
509 if (LOG.isTraceEnabled()) {
510 LOG.trace("Done evaluating exchange: " + exchange + " as: " + resultQName + " with result: " + answer);
511 }
512 return answer;
513 }
514
515 protected XPathExpression createXPathExpression() throws XPathExpressionException, XPathFactoryConfigurationException {
516 XPath xPath = getXPathFactory().newXPath();
517
518 xPath.setNamespaceContext(getNamespaceContext());
519 xPath.setXPathVariableResolver(getVariableResolver());
520
521 XPathFunctionResolver parentResolver = getFunctionResolver();
522 if (parentResolver == null) {
523 parentResolver = xPath.getXPathFunctionResolver();
524 }
525 xPath.setXPathFunctionResolver(createDefaultFunctionResolver(parentResolver));
526 return xPath.compile(text);
527 }
528
529 /**
530 * Lets populate a number of standard prefixes if they are not already there
531 */
532 protected void populateDefaultNamespaces(DefaultNamespaceContext context) {
533 setNamespaceIfNotPresent(context, "in", IN_NAMESPACE);
534 setNamespaceIfNotPresent(context, "out", OUT_NAMESPACE);
535 setNamespaceIfNotPresent(context, "env", Namespaces.ENVIRONMENT_VARIABLES);
536 setNamespaceIfNotPresent(context, "system", Namespaces.SYSTEM_PROPERTIES_NAMESPACE);
537 }
538
539 protected void setNamespaceIfNotPresent(DefaultNamespaceContext context, String prefix, String uri) {
540 if (context != null) {
541 String current = context.getNamespaceURI(prefix);
542 if (current == null) {
543 context.add(prefix, uri);
544 }
545 }
546 }
547
548 protected XPathFunctionResolver createDefaultFunctionResolver(final XPathFunctionResolver parent) {
549 return new XPathFunctionResolver() {
550 public XPathFunction resolveFunction(QName qName, int argumentCount) {
551 XPathFunction answer = null;
552 if (parent != null) {
553 answer = parent.resolveFunction(qName, argumentCount);
554 }
555 if (answer == null) {
556 if (isMatchingNamespaceOrEmptyNamespace(qName.getNamespaceURI(), IN_NAMESPACE)
557 || isMatchingNamespaceOrEmptyNamespace(qName.getNamespaceURI(), DEFAULT_NAMESPACE)) {
558 String localPart = qName.getLocalPart();
559 if (localPart.equals("body") && argumentCount == 0) {
560 return getBodyFunction();
561 }
562 if (localPart.equals("header") && argumentCount == 1) {
563 return getHeaderFunction();
564 }
565 }
566 if (isMatchingNamespaceOrEmptyNamespace(qName.getNamespaceURI(), OUT_NAMESPACE)) {
567 String localPart = qName.getLocalPart();
568 if (localPart.equals("body") && argumentCount == 0) {
569 return getOutBodyFunction();
570 }
571 if (localPart.equals("header") && argumentCount == 1) {
572 return getOutHeaderFunction();
573 }
574 }
575 }
576 return answer;
577 }
578 };
579 }
580
581 /**
582 * Strategy method to extract the document from the exchange
583 */
584 @SuppressWarnings("unchecked")
585 protected Object getDocument(Exchange exchange) {
586 Message in = exchange.getIn();
587 Class type = getDocumentType();
588 Object answer = null;
589 if (type != null) {
590 answer = in.getBody(type);
591 }
592
593 if (answer == null) {
594 answer = in.getBody();
595 }
596
597 // lets try coerce some common types into something JAXP can deal with
598 if (answer instanceof GenericFile) {
599 // special for files so we can work with them out of the box
600 InputStream is = exchange.getContext().getTypeConverter().convertTo(InputStream.class, answer);
601 answer = new InputSource(is);
602 } else if (answer instanceof BeanInvocation) {
603 // if its a null bean invocation then handle that
604 BeanInvocation bi = exchange.getContext().getTypeConverter().convertTo(BeanInvocation.class, answer);
605 if (bi.getArgs() != null && bi.getArgs().length == 1 && bi.getArgs()[0] == null) {
606 // its a null argument from the bean invocation so use null as answer
607 answer = null;
608 }
609 } else if (answer instanceof String) {
610 answer = new InputSource(new StringReader(answer.toString()));
611 }
612
613 // call the reset if the in message body is StreamCache
614 MessageHelper.resetStreamCache(exchange.getIn());
615 return answer;
616 }
617
618 private MessageVariableResolver getVariableResolver() {
619 MessageVariableResolver resolver = variableResolver.get();
620 if (resolver == null) {
621 resolver = new MessageVariableResolver(exchange);
622 variableResolver.set(resolver);
623 }
624 return resolver;
625 }
626
627 public void start() throws Exception {
628 }
629
630 public void stop() throws Exception {
631 pool.clear();
632 }
633 }