001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 * 
010 *      http://www.apache.org/licenses/LICENSE-2.0
011 * 
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.oozie.util;
019
020import org.apache.commons.el.ExpressionEvaluatorImpl;
021import org.apache.commons.el.ExpressionString;
022
023import javax.servlet.jsp.el.ELException;
024import javax.servlet.jsp.el.ExpressionEvaluator;
025import javax.servlet.jsp.el.FunctionMapper;
026import javax.servlet.jsp.el.VariableResolver;
027import java.lang.reflect.Method;
028import java.lang.reflect.Modifier;
029import java.util.HashMap;
030import java.util.Map;
031
032/**
033 * JSP Expression Language Evaluator. <p/> It provides a more convenient way of using the JSP EL Evaluator.
034 */
035public class ELEvaluator {
036
037    /**
038     * Provides functions and variables for the EL evaluator. <p/> All functions and variables in the context of an EL
039     * evaluator are accessible from EL expressions.
040     */
041    public static class Context implements VariableResolver, FunctionMapper {
042        private Map<String, Object> vars;
043        private Map<String, Method> functions;
044
045        /**
046         * Create an empty context.
047         */
048        public Context() {
049            vars = new HashMap<String, Object>();
050            functions = new HashMap<String, Method>();
051        }
052
053        /**
054         * Add variables to the context. <p/>
055         *
056         * @param vars variables to add to the context.
057         */
058        public void setVariables(Map<String, Object> vars) {
059            this.vars.putAll(vars);
060        }
061
062        /**
063         * Add a variable to the context. <p/>
064         *
065         * @param name variable name.
066         * @param value variable value.
067         */
068        public void setVariable(String name, Object value) {
069            vars.put(name, value);
070        }
071
072        /**
073         * Return a variable from the context. <p/>
074         *
075         * @param name variable name.
076         * @return the variable value.
077         */
078        public Object getVariable(String name) {
079            return vars.get(name);
080        }
081
082        /**
083         * Add a function to the context. <p/>
084         *
085         * @param prefix function prefix.
086         * @param functionName function name.
087         * @param method method that will be invoked for the function, it must be a static and public method.
088         */
089        public void addFunction(String prefix, String functionName, Method method) {
090            if ((method.getModifiers() & (Modifier.PUBLIC | Modifier.STATIC)) != (Modifier.PUBLIC | Modifier.STATIC)) {
091                throw new IllegalArgumentException(XLog.format("Method[{0}] must be public and static", method));
092            }
093            prefix = (prefix.length() > 0) ? prefix + ":" : "";
094            functions.put(prefix + functionName, method);
095        }
096
097        /**
098         * Resolve a variable name. Used by the EL evaluator implemenation. <p/>
099         *
100         * @param name variable name.
101         * @return the variable value.
102         * @throws ELException thrown if the variable is not defined in the context.
103         */
104        public Object resolveVariable(String name) throws ELException {
105            if (!vars.containsKey(name)) {
106                throw new ELException(XLog.format("variable [{0}] cannot be resolved", name));
107            }
108            return vars.get(name);
109        }
110
111        /**
112         * Resolve a function prefix:name. Used by the EL evaluator implementation. <p/>
113         *
114         * @param prefix function prefix.
115         * @param name function name.
116         * @return the method associated to the function.
117         */
118        public Method resolveFunction(String prefix, String name) {
119            if (prefix.length() > 0) {
120                name = prefix + ":" + name;
121            }
122            return functions.get(name);
123        }
124    }
125
126    private static ThreadLocal<ELEvaluator> current = new ThreadLocal<ELEvaluator>();
127
128    /**
129     * If within the scope of a EL evaluation call, it gives access to the ELEvaluator instance performing the EL
130     * evaluation. <p/> This is useful for EL function methods to get access to the variables of the Evaluator. Because
131     * of this, ELEvaluator variables can be used to pass context to EL function methods (which must be static methods).
132     * <p/>
133     *
134     * @return the ELEvaluator in scope, or <code>null</code> if none.
135     */
136    public static ELEvaluator getCurrent() {
137        return current.get();
138    }
139
140    private Context context;
141
142    private ExpressionEvaluatorImpl evaluator = new ExpressionEvaluatorImpl();
143
144    /**
145     * Creates an ELEvaluator with no functions and no variables defined.
146     */
147    public ELEvaluator() {
148        this(new Context());
149    }
150
151    /**
152     * Creates an ELEvaluator with the functions and variables defined in the given {@link ELEvaluator.Context}. <p/>
153     *
154     * @param context the ELSupport with functions and variables to be available for EL evalution.
155     */
156    public ELEvaluator(Context context) {
157        this.context = context;
158    }
159
160    /**
161     * Return the context with the functions and variables of the EL evaluator. <p/>
162     *
163     * @return the context.
164     */
165    public Context getContext() {
166        return context;
167    }
168
169    /**
170     * Convenience method that sets a variable in the EL evaluator context. <p/>
171     *
172     * @param name variable name.
173     * @param value variable value.
174     */
175    public void setVariable(String name, Object value) {
176        context.setVariable(name, value);
177    }
178
179    /**
180     * Convenience method that returns a variable from the EL evaluator context. <p/>
181     *
182     * @param name variable name.
183     * @return the variable value, <code>null</code> if not defined.
184     */
185    public Object getVariable(String name) {
186        return context.getVariable(name);
187    }
188
189    /**
190     * Evaluate an EL expression. <p/>
191     *
192     * @param expr EL expression to evaluate.
193     * @param clazz return type of the EL expression.
194     * @return the object the EL expression evaluated to.
195     * @throws Exception thrown if an EL function failed due to a transient error or EL expression could not be
196     * evaluated.
197     */
198    @SuppressWarnings({"unchecked", "deprecation"})
199    public <T> T evaluate(String expr, Class<T> clazz) throws Exception {
200        ELEvaluator existing = current.get();
201        try {
202            current.set(this);
203            return (T) evaluator.evaluate(expr, clazz, context, context);
204        }
205        catch (ELException ex) {
206            if (ex.getRootCause() instanceof Exception) {
207                throw (Exception) ex.getRootCause();
208            }
209            else {
210                throw ex;
211            }
212        }
213        finally {
214            current.set(existing);
215        }
216    }
217
218    /**
219     * Check if the input expression contains sequence statically. for example
220     * identify if "," is present outside of a function invocation in the given
221     * expression. Ex "${func('abc')},${func('def'}",
222     *
223     * @param expr - Expression string
224     * @param sequence - char sequence to check in the input expression
225     * @return true if present
226     * @throws Exception Exception thrown if an EL function failed due to a
227     *         transient error or EL expression could not be parsed
228     */
229    public boolean checkForExistence(String expr, String sequence)
230            throws Exception {
231        try {
232            Object exprString = evaluator.parseExpressionString(expr);
233            if (exprString instanceof ExpressionString) {
234                for (Object element : ((ExpressionString)exprString).getElements()) {
235                    if (element instanceof String &&
236                            element.toString().contains(sequence)) {
237                        return true;
238                    }
239                }
240            } else if (exprString instanceof String) {
241                if (((String)exprString).contains(sequence)) {
242                    return true;
243                }
244            }
245            return false;
246        } catch (ELException ex) {
247            if (ex.getRootCause() instanceof Exception) {
248                throw (Exception) ex.getRootCause();
249            }
250            else {
251                throw ex;
252            }
253        }
254    }
255}