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 */
017package org.apache.camel.language.bean;
018
019import java.util.List;
020import java.util.Map;
021
022import org.apache.camel.CamelContext;
023import org.apache.camel.Exchange;
024import org.apache.camel.ExchangePattern;
025import org.apache.camel.Expression;
026import org.apache.camel.ExpressionIllegalSyntaxException;
027import org.apache.camel.Predicate;
028import org.apache.camel.Processor;
029import org.apache.camel.component.bean.BeanHolder;
030import org.apache.camel.component.bean.BeanProcessor;
031import org.apache.camel.component.bean.ConstantBeanHolder;
032import org.apache.camel.component.bean.ConstantTypeBeanHolder;
033import org.apache.camel.component.bean.RegistryBean;
034import org.apache.camel.util.KeyValueHolder;
035import org.apache.camel.util.ObjectHelper;
036import org.apache.camel.util.OgnlHelper;
037import org.apache.camel.util.StringHelper;
038
039/**
040 * Evaluates an expression using a bean method invocation
041 */
042public class BeanExpression implements Expression, Predicate {
043    private final Object bean;
044    private final String beanName;
045    private final Class<?> type;
046    private final String method;
047    private volatile BeanHolder beanHolder;
048
049    public BeanExpression(Object bean, String method) {
050        this.bean = bean;
051        this.method = method;
052        this.beanName = null;
053        this.type = null;
054    }
055
056    public BeanExpression(String beanName, String method) {
057        this.beanName = beanName;
058        this.method = method;
059        this.bean = null;
060        this.type = null;
061    }
062
063    public BeanExpression(Class<?> type, String method) {
064        this.type = type;
065        this.method = method;
066        this.bean = null;
067        this.beanName = null;
068    }
069
070    public BeanExpression(BeanHolder beanHolder, String method) {
071        this.beanHolder = beanHolder;
072        this.method = method;
073        this.bean = null;
074        this.beanName = null;
075        this.type = null;
076    }
077
078    @Override
079    public String toString() {
080        StringBuilder sb = new StringBuilder("BeanExpression[");
081        if (bean != null) {
082            sb.append(bean.toString());
083        } else if (beanName != null) {
084            sb.append(beanName);
085        } else if (type != null) {
086            sb.append(ObjectHelper.className(type));
087        }
088        if (method != null) {
089            sb.append(" method: ").append(method);
090        }
091        sb.append("]");
092        return sb.toString();
093    }
094
095    public Object evaluate(Exchange exchange) {
096
097        // if the bean holder doesn't exist then create it using the context from the exchange
098        if (beanHolder == null) {
099            beanHolder = createBeanHolder(exchange.getContext());
100        }
101
102        // invoking the bean can either be the easy way or using OGNL
103
104        // validate OGNL
105        if (OgnlHelper.isInvalidValidOgnlExpression(method)) {
106            ExpressionIllegalSyntaxException cause = new ExpressionIllegalSyntaxException(method);
107            throw new RuntimeBeanExpressionException(exchange, beanName, method, cause);
108        }
109
110        if (OgnlHelper.isValidOgnlExpression(method)) {
111            // okay the method is an ognl expression
112            OgnlInvokeProcessor ognl = new OgnlInvokeProcessor(beanHolder, method);
113            try {
114                ognl.process(exchange);
115                return ognl.getResult();
116            } catch (Exception e) {
117                throw new RuntimeBeanExpressionException(exchange, beanName, method, e);
118            }
119        } else {
120            // regular non ognl invocation
121            InvokeProcessor invoke = new InvokeProcessor(beanHolder, method);
122            try {
123                invoke.process(exchange);
124                return invoke.getResult();
125            } catch (Exception e) {
126                throw new RuntimeBeanExpressionException(exchange, beanName, method, e);
127            }
128        }
129    }
130
131    public <T> T evaluate(Exchange exchange, Class<T> type) {
132        Object result = evaluate(exchange);
133        if (Object.class == type) {
134            // do not use type converter if type is Object (optimize)
135            return (T) result;
136        } else {
137            return exchange.getContext().getTypeConverter().convertTo(type, exchange, result);
138        }
139    }
140
141    public boolean matches(Exchange exchange) {
142        Object value = evaluate(exchange);
143        return ObjectHelper.evaluateValuePredicate(value);
144    }
145
146    /**
147     * Optimize to create the bean holder once, so we can reuse it for further
148     * evaluation, which is faster.
149     */
150    private synchronized BeanHolder createBeanHolder(CamelContext context) {
151        // either use registry lookup or a constant bean
152        BeanHolder holder;
153        if (bean != null) {
154            holder = new ConstantBeanHolder(bean, context);
155        } else if (beanName != null) {
156            holder = new RegistryBean(context, beanName);
157        } else if (type != null) {
158            holder = new ConstantTypeBeanHolder(type, context);
159        } else {
160            throw new IllegalArgumentException("Either bean, beanName or type should be set on " + this);
161        }
162        return holder;
163    }
164
165    /**
166     * Invokes a given bean holder. The method name is optional.
167     */
168    private final class InvokeProcessor implements Processor {
169
170        private BeanHolder beanHolder;
171        private String methodName;
172        private Object result;
173
174        private InvokeProcessor(BeanHolder beanHolder, String methodName) {
175            this.beanHolder = beanHolder;
176            this.methodName = methodName;
177        }
178
179        public void process(Exchange exchange) throws Exception {
180            BeanProcessor processor = new BeanProcessor(beanHolder);
181            if (methodName != null) {
182                processor.setMethod(methodName);
183                // enable OGNL like invocation
184                processor.setShorthandMethod(true);
185            }
186            try {
187                // copy the original exchange to avoid side effects on it
188                Exchange resultExchange = exchange.copy();
189                // remove any existing exception in case we do OGNL on the exception
190                resultExchange.setException(null);
191
192                // force to use InOut to retrieve the result on the OUT message
193                resultExchange.setPattern(ExchangePattern.InOut);
194                processor.process(resultExchange);
195                result = resultExchange.getOut().getBody();
196
197                // propagate properties and headers from result
198                if (resultExchange.hasProperties()) {
199                    exchange.getProperties().putAll(resultExchange.getProperties());
200                }
201                if (resultExchange.getOut().hasHeaders()) {
202                    exchange.getIn().getHeaders().putAll(resultExchange.getOut().getHeaders());
203                }
204
205                // propagate exceptions
206                if (resultExchange.getException() != null) {
207                    exchange.setException(resultExchange.getException());
208                }
209            } catch (Exception e) {
210                throw new RuntimeBeanExpressionException(exchange, beanName, methodName, e);
211            }
212        }
213
214        public Object getResult() {
215            return result;
216        }
217    }
218
219    /**
220     * To invoke a bean using a OGNL notation which denotes the chain of methods to invoke.
221     * <p/>
222     * For more advanced OGNL you may have to look for a real framework such as OGNL, Mvel or dynamic
223     * programming language such as Groovy, JuEL, JavaScript.
224     */
225    private final class OgnlInvokeProcessor implements Processor {
226
227        private final String ognl;
228        private final BeanHolder beanHolder;
229        private Object result;
230
231        public OgnlInvokeProcessor(BeanHolder beanHolder, String ognl) {
232            this.beanHolder = beanHolder;
233            this.ognl = ognl;
234            // we must start with having bean as the result
235            this.result = beanHolder.getBean();
236        }
237
238        public void process(Exchange exchange) throws Exception {
239            // copy the original exchange to avoid side effects on it
240            Exchange resultExchange = exchange.copy();
241            // remove any existing exception in case we do OGNL on the exception
242            resultExchange.setException(null);
243            // force to use InOut to retrieve the result on the OUT message
244            resultExchange.setPattern(ExchangePattern.InOut);
245            // do not propagate any method name when using OGNL, as with OGNL we
246            // compute and provide the method name to explicit to invoke
247            resultExchange.getIn().removeHeader(Exchange.BEAN_METHOD_NAME);
248
249            // current ognl path as we go along
250            String ognlPath = "";
251
252            // loop and invoke each method
253            Object beanToCall = beanHolder.getBean();
254            // there must be a bean to call with, we currently does not support OGNL expressions on using purely static methods
255            if (beanToCall == null) {
256                throw new IllegalArgumentException("Bean instance is null. OGNL bean expressions requires bean instances.");
257            }
258
259            // Split ognl except when this is not a Map, Array
260            // and we would like to keep the dots within the key name
261            List<String> methods = OgnlHelper.splitOgnl(ognl);
262
263            for (String methodName : methods) {
264                BeanHolder holder = new ConstantBeanHolder(beanToCall, exchange.getContext());
265
266                // support the null safe operator
267                boolean nullSafe = OgnlHelper.isNullSafeOperator(methodName);
268
269                // keep up with how far are we doing
270                ognlPath += methodName;
271
272                // get rid of leading ?. or . as we only needed that to determine if null safe was enabled or not
273                methodName = OgnlHelper.removeLeadingOperators(methodName);
274
275                // are we doing an index lookup (eg in Map/List/array etc)?
276                String key = null;
277                KeyValueHolder<String, String> index = OgnlHelper.isOgnlIndex(methodName);
278                if (index != null) {
279                    methodName = index.getKey();
280                    key = index.getValue();
281                }
282
283                // only invoke if we have a method name to use to invoke
284                if (methodName != null) {
285                    InvokeProcessor invoke = new InvokeProcessor(holder, methodName);
286                    invoke.process(resultExchange);
287
288                    // check for exception and rethrow if we failed
289                    if (resultExchange.getException() != null) {
290                        throw new RuntimeBeanExpressionException(exchange, beanName, methodName, resultExchange.getException());
291                    }
292
293                    result = invoke.getResult();
294                }
295
296                // if there was a key then we need to lookup using the key
297                if (key != null) {
298                    result = lookupResult(resultExchange, key, result, nullSafe, ognlPath, holder.getBean());
299                }
300
301                // check null safe for null results
302                if (result == null && nullSafe) {
303                    return;
304                }
305
306                // prepare for next bean to invoke
307                beanToCall = result;
308            }
309        }
310
311        private Object lookupResult(Exchange exchange, String key, Object result, boolean nullSafe, String ognlPath, Object bean) {
312            ObjectHelper.notEmpty(key, "key", "in Simple language ognl path: " + ognlPath);
313
314            // trim key
315            key = key.trim();
316
317            // remove any enclosing quotes
318            key = StringHelper.removeLeadingAndEndingQuotes(key);
319
320            // try map first
321            Map<?, ?> map = exchange.getContext().getTypeConverter().convertTo(Map.class, result);
322            if (map != null) {
323                return map.get(key);
324            }
325
326            // special for list is last keyword
327            Integer num = exchange.getContext().getTypeConverter().tryConvertTo(Integer.class, key);
328            boolean checkList = key.startsWith("last") || num != null;
329
330            if (checkList) {
331                List<?> list = exchange.getContext().getTypeConverter().convertTo(List.class, result);
332                if (list != null) {
333                    if (key.startsWith("last")) {
334                        num = list.size() - 1;
335
336                        // maybe its an expression to subtract a number after last
337                        String after = ObjectHelper.after(key, "-");
338                        if (after != null) {
339                            Integer redux = exchange.getContext().getTypeConverter().tryConvertTo(Integer.class, after.trim());
340                            if (redux != null) {
341                                num -= redux;
342                            } else {
343                                throw new ExpressionIllegalSyntaxException(key);
344                            }
345                        }
346                    }
347                    if (num != null && num >= 0 && list.size() > num - 1) {
348                        return list.get(num);
349                    }
350                    if (!nullSafe) {
351                        // not null safe then its mandatory so thrown out of bounds exception
352                        throw new IndexOutOfBoundsException("Index: " + num + ", Size: " + list.size()
353                                + " out of bounds with List from bean: " + bean + "using OGNL path [" + ognlPath + "]");
354                    }
355                }
356            }
357
358            if (!nullSafe) {
359                throw new IndexOutOfBoundsException("Key: " + key + " not found in bean: " + bean + " of type: "
360                        + ObjectHelper.classCanonicalName(bean) + " using OGNL path [" + ognlPath + "]");
361            } else {
362                // null safe so we can return null
363                return null;
364            }
365        }
366
367        public Object getResult() {
368            return result;
369        }
370    }
371
372}