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.language.bean;
018    
019    import java.util.List;
020    import java.util.Map;
021    
022    import org.apache.camel.CamelContext;
023    import org.apache.camel.Exchange;
024    import org.apache.camel.ExchangePattern;
025    import org.apache.camel.Expression;
026    import org.apache.camel.ExpressionIllegalSyntaxException;
027    import org.apache.camel.Predicate;
028    import org.apache.camel.Processor;
029    import org.apache.camel.component.bean.BeanHolder;
030    import org.apache.camel.component.bean.BeanProcessor;
031    import org.apache.camel.component.bean.ConstantBeanHolder;
032    import org.apache.camel.component.bean.ConstantTypeBeanHolder;
033    import org.apache.camel.component.bean.RegistryBean;
034    import org.apache.camel.util.KeyValueHolder;
035    import org.apache.camel.util.ObjectHelper;
036    import org.apache.camel.util.OgnlHelper;
037    import org.apache.camel.util.StringHelper;
038    
039    /**
040     * Evaluates an expression using a bean method invocation
041     */
042    public 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    }