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