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        return exchange.getContext().getTypeConverter().convertTo(type, exchange, result);
134    }
135
136    public boolean matches(Exchange exchange) {
137        Object value = evaluate(exchange);
138        return ObjectHelper.evaluateValuePredicate(value);
139    }
140
141    /**
142     * Optimize to create the bean holder once, so we can reuse it for further
143     * evaluation, which is faster.
144     */
145    private synchronized BeanHolder createBeanHolder(CamelContext context) {
146        // either use registry lookup or a constant bean
147        BeanHolder holder;
148        if (bean != null) {
149            holder = new ConstantBeanHolder(bean, context);
150        } else if (beanName != null) {
151            holder = new RegistryBean(context, beanName);
152        } else if (type != null) {
153            holder = new ConstantTypeBeanHolder(type, context);
154        } else {
155            throw new IllegalArgumentException("Either bean, beanName or type should be set on " + this);
156        }
157        return holder;
158    }
159
160    /**
161     * Invokes a given bean holder. The method name is optional.
162     */
163    private final class InvokeProcessor implements Processor {
164
165        private BeanHolder beanHolder;
166        private String methodName;
167        private Object result;
168
169        private InvokeProcessor(BeanHolder beanHolder, String methodName) {
170            this.beanHolder = beanHolder;
171            this.methodName = methodName;
172        }
173
174        public void process(Exchange exchange) throws Exception {
175            BeanProcessor processor = new BeanProcessor(beanHolder);
176            if (methodName != null) {
177                processor.setMethod(methodName);
178                // enable OGNL like invocation
179                processor.setShorthandMethod(true);
180            }
181            try {
182                // copy the original exchange to avoid side effects on it
183                Exchange resultExchange = exchange.copy();
184                // remove any existing exception in case we do OGNL on the exception
185                resultExchange.setException(null);
186
187                // force to use InOut to retrieve the result on the OUT message
188                resultExchange.setPattern(ExchangePattern.InOut);
189                processor.process(resultExchange);
190                result = resultExchange.getOut().getBody();
191
192                // propagate properties and headers from result
193                if (resultExchange.hasProperties()) {
194                    exchange.getProperties().putAll(resultExchange.getProperties());
195                }
196                if (resultExchange.getOut().hasHeaders()) {
197                    exchange.getIn().getHeaders().putAll(resultExchange.getOut().getHeaders());
198                }
199
200                // propagate exceptions
201                if (resultExchange.getException() != null) {
202                    exchange.setException(resultExchange.getException());
203                }
204            } catch (Exception e) {
205                throw new RuntimeBeanExpressionException(exchange, beanName, methodName, e);
206            }
207        }
208
209        public Object getResult() {
210            return result;
211        }
212    }
213
214    /**
215     * To invoke a bean using a OGNL notation which denotes the chain of methods to invoke.
216     * <p/>
217     * For more advanced OGNL you may have to look for a real framework such as OGNL, Mvel or dynamic
218     * programming language such as Groovy, JuEL, JavaScript.
219     */
220    private final class OgnlInvokeProcessor implements Processor {
221
222        private final String ognl;
223        private final BeanHolder beanHolder;
224        private Object result;
225
226        public OgnlInvokeProcessor(BeanHolder beanHolder, String ognl) {
227            this.beanHolder = beanHolder;
228            this.ognl = ognl;
229            // we must start with having bean as the result
230            this.result = beanHolder.getBean();
231        }
232
233        public void process(Exchange exchange) throws Exception {
234            // copy the original exchange to avoid side effects on it
235            Exchange resultExchange = exchange.copy();
236            // remove any existing exception in case we do OGNL on the exception
237            resultExchange.setException(null);
238            // force to use InOut to retrieve the result on the OUT message
239            resultExchange.setPattern(ExchangePattern.InOut);
240            // do not propagate any method name when using OGNL, as with OGNL we
241            // compute and provide the method name to explicit to invoke
242            resultExchange.getIn().removeHeader(Exchange.BEAN_METHOD_NAME);
243
244            // current ognl path as we go along
245            String ognlPath = "";
246
247            // loop and invoke each method
248            Object beanToCall = beanHolder.getBean();
249            // there must be a bean to call with, we currently does not support OGNL expressions on using purely static methods
250            if (beanToCall == null) {
251                throw new IllegalArgumentException("Bean instance is null. OGNL bean expressions requires bean instances.");
252            }
253
254            // Split ognl except when this is not a Map, Array
255            // and we would like to keep the dots within the key name
256            List<String> methods = OgnlHelper.splitOgnl(ognl);
257
258            for (String methodName : methods) {
259                BeanHolder holder = new ConstantBeanHolder(beanToCall, exchange.getContext());
260
261                // support the null safe operator
262                boolean nullSafe = OgnlHelper.isNullSafeOperator(methodName);
263
264                // keep up with how far are we doing
265                ognlPath += methodName;
266
267                // get rid of leading ?. or . as we only needed that to determine if null safe was enabled or not
268                methodName = OgnlHelper.removeLeadingOperators(methodName);
269
270                // are we doing an index lookup (eg in Map/List/array etc)?
271                String key = null;
272                KeyValueHolder<String, String> index = OgnlHelper.isOgnlIndex(methodName);
273                if (index != null) {
274                    methodName = index.getKey();
275                    key = index.getValue();
276                }
277
278                // only invoke if we have a method name to use to invoke
279                if (methodName != null) {
280                    InvokeProcessor invoke = new InvokeProcessor(holder, methodName);
281                    invoke.process(resultExchange);
282
283                    // check for exception and rethrow if we failed
284                    if (resultExchange.getException() != null) {
285                        throw new RuntimeBeanExpressionException(exchange, beanName, methodName, resultExchange.getException());
286                    }
287
288                    result = invoke.getResult();
289                }
290
291                // if there was a key then we need to lookup using the key
292                if (key != null) {
293                    result = lookupResult(resultExchange, key, result, nullSafe, ognlPath, holder.getBean());
294                }
295
296                // check null safe for null results
297                if (result == null && nullSafe) {
298                    return;
299                }
300
301                // prepare for next bean to invoke
302                beanToCall = result;
303            }
304        }
305
306        private Object lookupResult(Exchange exchange, String key, Object result, boolean nullSafe, String ognlPath, Object bean) {
307            ObjectHelper.notEmpty(key, "key", "in Simple language ognl path: " + ognlPath);
308
309            // trim key
310            key = key.trim();
311
312            // remove any enclosing quotes
313            key = StringHelper.removeLeadingAndEndingQuotes(key);
314
315            // try map first
316            Map<?, ?> map = exchange.getContext().getTypeConverter().convertTo(Map.class, result);
317            if (map != null) {
318                return map.get(key);
319            }
320
321            // special for list is last keyword
322            Integer num = exchange.getContext().getTypeConverter().tryConvertTo(Integer.class, key);
323            boolean checkList = key.startsWith("last") || num != null;
324
325            if (checkList) {
326                List<?> list = exchange.getContext().getTypeConverter().convertTo(List.class, result);
327                if (list != null) {
328                    if (key.startsWith("last")) {
329                        num = list.size() - 1;
330
331                        // maybe its an expression to subtract a number after last
332                        String after = ObjectHelper.after(key, "-");
333                        if (after != null) {
334                            Integer redux = exchange.getContext().getTypeConverter().tryConvertTo(Integer.class, after.trim());
335                            if (redux != null) {
336                                num -= redux;
337                            } else {
338                                throw new ExpressionIllegalSyntaxException(key);
339                            }
340                        }
341                    }
342                    if (num != null && num >= 0 && list.size() > num - 1) {
343                        return list.get(num);
344                    }
345                    if (!nullSafe) {
346                        // not null safe then its mandatory so thrown out of bounds exception
347                        throw new IndexOutOfBoundsException("Index: " + num + ", Size: " + list.size()
348                                + " out of bounds with List from bean: " + bean + "using OGNL path [" + ognlPath + "]");
349                    }
350                }
351            }
352
353            if (!nullSafe) {
354                throw new IndexOutOfBoundsException("Key: " + key + " not found in bean: " + bean + " of type: "
355                        + ObjectHelper.classCanonicalName(bean) + " using OGNL path [" + ognlPath + "]");
356            } else {
357                // null safe so we can return null
358                return null;
359            }
360        }
361
362        public Object getResult() {
363            return result;
364        }
365    }
366
367}