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.commons.scxml2.env.jexl;
018
019import java.io.Serializable;
020import java.util.HashMap;
021import java.util.Map;
022import java.util.UUID;
023
024import org.apache.commons.jexl2.Expression;
025import org.apache.commons.jexl2.JexlEngine;
026import org.apache.commons.jexl2.Script;
027import org.apache.commons.scxml2.Context;
028import org.apache.commons.scxml2.Evaluator;
029import org.apache.commons.scxml2.EvaluatorProvider;
030import org.apache.commons.scxml2.SCXMLExpressionException;
031import org.apache.commons.scxml2.XPathBuiltin;
032import org.apache.commons.scxml2.env.EffectiveContextMap;
033import org.apache.commons.scxml2.model.SCXML;
034
035/**
036 * Evaluator implementation enabling use of JEXL expressions in
037 * SCXML documents.
038 * <P>
039 * This implementation itself is thread-safe, so you can keep singleton
040 * for efficiency of the internal <code>JexlEngine</code> member.
041 * </P>
042 */
043public class JexlEvaluator implements Evaluator, Serializable {
044
045    /** Serial version UID. */
046    private static final long serialVersionUID = 1L;
047
048    /**
049     * Unique context variable name used for temporary reference to assign data (thus must be a valid variable name)
050     */
051    private static final String ASSIGN_VARIABLE_NAME = "a"+UUID.randomUUID().toString().replace('-','x');
052
053    public static final String SUPPORTED_DATA_MODEL = "jexl";
054
055    public static class JexlEvaluatorProvider implements EvaluatorProvider {
056
057        @Override
058        public String getSupportedDatamodel() {
059            return SUPPORTED_DATA_MODEL;
060        }
061
062        @Override
063        public Evaluator getEvaluator() {
064            return new JexlEvaluator();
065        }
066
067        @Override
068        public Evaluator getEvaluator(final SCXML document) {
069            return new JexlEvaluator();
070        }
071    }
072
073    /** Error message if evaluation context is not a JexlContext. */
074    private static final String ERR_CTX_TYPE = "Error evaluating JEXL "
075        + "expression, Context must be a org.apache.commons.scxml2.env.jexl.JexlContext";
076
077
078
079    /** The internal JexlEngine instance to use. */
080    private transient volatile JexlEngine jexlEngine;
081
082    /** The current JexlEngine silent mode, stored locally to be reapplied after deserialization of the engine */
083    private boolean jexlEngineSilent;
084    /** The current JexlEngine strict mode, stored locally to be reapplied after deserialization of the engine */
085    private boolean jexlEngineStrict;
086
087    /** Constructor. */
088    public JexlEvaluator() {
089        super();
090        // create the internal JexlEngine initially
091        jexlEngine = createJexlEngine();
092        jexlEngineSilent = jexlEngine.isSilent();
093        jexlEngineStrict = jexlEngine.isStrict();
094    }
095
096    /**
097     * Checks whether the internal Jexl engine throws JexlException during evaluation.
098     * @return true if silent, false (default) otherwise
099     */
100    public boolean isJexlEngineSilent() {
101        return jexlEngineSilent;
102    }
103
104    /**
105     * Delegate method for {@link JexlEngine#setSilent(boolean)} to set whether the engine throws JexlException during
106     * evaluation when an error is triggered.
107     * <p>This method should be called as an optional step of the JexlEngine
108     * initialization code before expression creation &amp; evaluation.</p>
109     * @param silent true means no JexlException will occur, false allows them
110     */
111    public void setJexlEngineSilent(boolean silent) {
112        synchronized (this) {
113            JexlEngine engine = getJexlEngine();
114            engine.setSilent(silent);
115            this.jexlEngineSilent = silent;
116        }
117    }
118
119    /**
120     * Checks whether the internal Jexl engine behaves in strict or lenient mode.
121     * @return true for strict, false for lenient
122     */
123    public boolean isJexlEngineStrict() {
124        return jexlEngineStrict;
125    }
126
127    /**
128     * Delegate method for {@link JexlEngine#setStrict(boolean)} to set whether it behaves in strict or lenient mode.
129     * <p>This method is should be called as an optional step of the JexlEngine
130     * initialization code before expression creation &amp; evaluation.</p>
131     * @param strict true for strict, false for lenient
132     */
133    public void setJexlEngineStrict(boolean strict) {
134        synchronized (this) {
135            JexlEngine engine = getJexlEngine();
136            engine.setStrict(strict);
137            this.jexlEngineStrict = strict;
138        }
139    }
140
141    @Override
142    public String getSupportedDatamodel() {
143        return SUPPORTED_DATA_MODEL;
144    }
145
146    /**
147     * Evaluate an expression.
148     *
149     * @param ctx variable context
150     * @param expr expression
151     * @return a result of the evaluation
152     * @throws SCXMLExpressionException For a malformed expression
153     * @see Evaluator#eval(Context, String)
154     */
155    public Object eval(final Context ctx, final String expr)
156    throws SCXMLExpressionException {
157        if (expr == null) {
158            return null;
159        }
160        if (!(ctx instanceof JexlContext)) {
161            throw new SCXMLExpressionException(ERR_CTX_TYPE);
162        }
163        try {
164            final JexlContext effective = getEffectiveContext((JexlContext)ctx);
165            Expression exp = getJexlEngine().createExpression(expr);
166            return exp.evaluate(effective);
167        } catch (Exception e) {
168            String exMessage = e.getMessage() != null ? e.getMessage() : e.getClass().getCanonicalName();
169            throw new SCXMLExpressionException("eval('" + expr + "'): " + exMessage, e);
170        }
171    }
172
173    /**
174     * @see Evaluator#evalCond(Context, String)
175     */
176    public Boolean evalCond(final Context ctx, final String expr)
177    throws SCXMLExpressionException {
178        if (expr == null) {
179            return null;
180        }
181        if (!(ctx instanceof JexlContext)) {
182            throw new SCXMLExpressionException(ERR_CTX_TYPE);
183        }
184        try {
185            final JexlContext effective = getEffectiveContext((JexlContext)ctx);
186            Expression exp = getJexlEngine().createExpression(expr);
187            final Object result = exp.evaluate(effective);
188            return result == null ? Boolean.FALSE : (Boolean)result;
189        } catch (Exception e) {
190            String exMessage = e.getMessage() != null ? e.getMessage() : e.getClass().getCanonicalName();
191            throw new SCXMLExpressionException("evalCond('" + expr + "'): " + exMessage, e);
192        }
193    }
194
195    /**
196     * @see Evaluator#evalLocation(Context, String)
197     */
198    public Object evalLocation(final Context ctx, final String expr)
199    throws SCXMLExpressionException {
200        if (expr == null) {
201            return null;
202        }
203        else if (ctx.has(expr)) {
204            return expr;
205        }
206
207        if (!(ctx instanceof JexlContext)) {
208            throw new SCXMLExpressionException(ERR_CTX_TYPE);
209        }
210        try {
211            final JexlContext effective = getEffectiveContext((JexlContext)ctx);
212            Expression exp = getJexlEngine().createExpression(expr);
213            return exp.evaluate(effective);
214        } catch (Exception e) {
215            String exMessage = e.getMessage() != null ? e.getMessage() : e.getClass().getCanonicalName();
216            throw new SCXMLExpressionException("evalLocation('" + expr + "'): " + exMessage, e);
217        }
218    }
219
220    /**
221     * @see Evaluator#evalAssign(Context, String, Object, AssignType, String)
222     */
223    public void evalAssign(final Context ctx, final String location, final Object data, final AssignType type,
224                           final String attr) throws SCXMLExpressionException {
225
226        Object loc = evalLocation(ctx, location);
227        if (loc != null) {
228
229            if (XPathBuiltin.isXPathLocation(ctx, loc)) {
230                XPathBuiltin.assign(ctx, loc, data, type, attr);
231            }
232            else {
233                StringBuilder sb = new StringBuilder(location).append("=").append(ASSIGN_VARIABLE_NAME);
234                try {
235                    ctx.getVars().put(ASSIGN_VARIABLE_NAME, data);
236                    eval(ctx, sb.toString());
237                }
238                finally {
239                    ctx.getVars().remove(ASSIGN_VARIABLE_NAME);
240                }
241            }
242        }
243        else {
244            throw new SCXMLExpressionException("evalAssign - cannot resolve location: '" + location + "'");
245        }
246    }
247
248    /**
249     * @see Evaluator#evalScript(Context, String)
250     */
251    public Object evalScript(final Context ctx, final String script)
252    throws SCXMLExpressionException {
253        if (script == null) {
254            return null;
255        }
256        if (!(ctx instanceof JexlContext)) {
257            throw new SCXMLExpressionException(ERR_CTX_TYPE);
258        }
259        try {
260            final JexlContext effective = getEffectiveContext((JexlContext) ctx);
261            final Script jexlScript = getJexlEngine().createScript(script);
262            return jexlScript.execute(effective);
263        } catch (Exception e) {
264            String exMessage = e.getMessage() != null ? e.getMessage() : e.getClass().getCanonicalName();
265            throw new SCXMLExpressionException("evalScript('" + script + "'): " + exMessage, e);
266        }
267    }
268
269    /**
270     * Create a new child context.
271     *
272     * @param parent parent context
273     * @return new child context
274     * @see Evaluator#newContext(Context)
275     */
276    public Context newContext(final Context parent) {
277        return new JexlContext(parent);
278    }
279
280    /**
281     * Create the internal JexlEngine member during the initialization.
282     * This method can be overriden to specify more detailed options
283     * into the JexlEngine.
284     * @return new JexlEngine instance
285     */
286    protected JexlEngine createJexlEngine() {
287        JexlEngine engine = new JexlEngine();
288        // With null prefix, define top-level user defined functions.
289        // See javadoc of org.apache.commons.jexl2.JexlEngine#setFunctions(Map<String,Object> funcs) for detail.
290        Map<String, Object> funcs = new HashMap<String, Object>();
291        funcs.put(null, JexlBuiltin.class);
292        engine.setFunctions(funcs);
293        engine.setCache(256);
294        return engine;
295    }
296
297    /**
298     * Returns the internal JexlEngine if existing.
299     * Otherwise, it creates a new engine by invoking {@link #createJexlEngine()}.
300     * <P>
301     * <EM>NOTE: The internal JexlEngine instance can be null when this is deserialized.</EM>
302     * </P>
303     * @return the current JexlEngine
304     */
305    private JexlEngine getJexlEngine() {
306        JexlEngine engine = jexlEngine;
307        if (engine == null) {
308            synchronized (this) {
309                engine = jexlEngine;
310                if (engine == null) {
311                    jexlEngine = engine = createJexlEngine();
312                    jexlEngine.setSilent(jexlEngineSilent);
313                    jexlEngine.setStrict(jexlEngineStrict);
314                }
315            }
316        }
317        return engine;
318    }
319
320    /**
321     * Create a new context which is the summation of contexts from the
322     * current state to document root, child has priority over parent
323     * in scoping rules.
324     *
325     * @param nodeCtx The JexlContext for this state.
326     * @return The effective JexlContext for the path leading up to
327     *         document root.
328     */
329    protected JexlContext getEffectiveContext(final JexlContext nodeCtx) {
330        return new JexlContext(nodeCtx, new EffectiveContextMap(nodeCtx));
331    }
332}
333