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