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.groovy;
018
019import groovy.lang.Script;
020
021import java.io.Serializable;
022import java.util.Collections;
023import java.util.HashMap;
024import java.util.Map;
025import java.util.UUID;
026import java.util.regex.Matcher;
027import java.util.regex.Pattern;
028
029import org.apache.commons.scxml2.Context;
030import org.apache.commons.scxml2.Evaluator;
031import org.apache.commons.scxml2.EvaluatorProvider;
032import org.apache.commons.scxml2.SCXMLExpressionException;
033import org.apache.commons.scxml2.SCXMLSystemContext;
034import org.apache.commons.scxml2.XPathBuiltin;
035import org.apache.commons.scxml2.env.EffectiveContextMap;
036import org.apache.commons.scxml2.model.SCXML;
037
038/**
039 * Evaluator implementation enabling use of Groovy expressions in SCXML documents.
040 * <P>
041 * This implementation itself is thread-safe, so you can keep singleton for efficiency.
042 * </P>
043 */
044public class GroovyEvaluator implements Evaluator, Serializable {
045
046    /** Serial version UID. */
047    private static final long serialVersionUID = 1L;
048
049    /**
050     * Unique context variable name used for temporary reference to assign data (thus must be a valid variable name)
051     */
052    private static final String ASSIGN_VARIABLE_NAME = "a"+UUID.randomUUID().toString().replace('-','x');
053
054    public static final String SUPPORTED_DATA_MODEL = "groovy";
055
056    public static class GroovyEvaluatorProvider implements EvaluatorProvider {
057
058        @Override
059        public String getSupportedDatamodel() {
060            return SUPPORTED_DATA_MODEL;
061        }
062
063        @Override
064        public Evaluator getEvaluator() {
065            return new GroovyEvaluator();
066        }
067
068        @Override
069        public Evaluator getEvaluator(final SCXML document) {
070            return new GroovyEvaluator();
071        }
072    }
073
074    /** Error message if evaluation context is not a GroovyContext. */
075    private static final String ERR_CTX_TYPE = "Error evaluating Groovy "
076            + "expression, Context must be a org.apache.commons.scxml2.env.groovy.GroovyContext";
077
078    protected static final GroovyExtendableScriptCache.ScriptPreProcessor scriptPreProcessor = new GroovyExtendableScriptCache.ScriptPreProcessor () {
079
080        /**
081         * Pattern for case-sensitive matching of the Groovy operator aliases, delimited by whitespace
082         */
083        public final Pattern GROOVY_OPERATOR_ALIASES_PATTERN = Pattern.compile("(?<=\\s)(and|or|not|eq|lt|le|ne|gt|ge)(?=\\s)");
084
085        /**
086         * Groovy operator aliases mapped to their underlying Groovy operator
087         */
088        public final Map<String, String> GROOVY_OPERATOR_ALIASES = Collections.unmodifiableMap(new HashMap<String, String>() {{
089            put("and", "&& "); put("or",  "||"); put("not", " ! ");
090            put("eq",  "==");  put("lt",  "< "); put("le",  "<=");
091            put("ne",  "!=");  put("gt",  "> "); put("ge",  ">=");
092        }});
093
094        @Override
095        public String preProcess(final String script) {
096            if (script == null || script.length() == 0) {
097                return script;
098            }
099            StringBuffer sb = null;
100            Matcher m = GROOVY_OPERATOR_ALIASES_PATTERN.matcher(script);
101            while (m.find()) {
102                if (sb == null) {
103                    sb = new StringBuffer();
104                }
105                m.appendReplacement(sb, GROOVY_OPERATOR_ALIASES.get(m.group()));
106            }
107            if (sb != null) {
108                m.appendTail(sb);
109                return sb.toString();
110            }
111            return script;
112        }
113    };
114
115    private final boolean useInitialScriptAsBaseScript;
116    private final GroovyExtendableScriptCache scriptCache;
117
118    public GroovyEvaluator() {
119        this(false);
120    }
121
122    public GroovyEvaluator(boolean useInitialScriptAsBaseScript) {
123        this.useInitialScriptAsBaseScript = useInitialScriptAsBaseScript;
124        this.scriptCache = newScriptCache();
125    }
126
127    /**
128     * Overridable factory method to create the GroovyExtendableScriptCache for this GroovyEvaluator.
129     * <p>
130     * The default implementation configures the scriptCache to use the {@link #scriptPreProcessor GroovyEvaluator scriptPreProcessor}
131     * and the {@link GroovySCXMLScript} as script base class.
132     * </p>
133     */
134    protected GroovyExtendableScriptCache newScriptCache() {
135        GroovyExtendableScriptCache scriptCache = new GroovyExtendableScriptCache();
136        scriptCache.setScriptPreProcessor(getScriptPreProcessor());
137        scriptCache.setScriptBaseClass(GroovySCXMLScript.class.getName());
138        return scriptCache;
139    }
140
141    @SuppressWarnings("unchecked")
142    protected Script getScript(GroovyContext groovyContext, String scriptBaseClassName, String scriptSource) {
143        Script script = scriptCache.getScript(scriptBaseClassName, scriptSource);
144        script.setBinding(groovyContext.getBinding());
145        return script;
146    }
147
148    @SuppressWarnings("unused")
149    public void clearCache() {
150        scriptCache.clearCache();
151    }
152
153    public GroovyExtendableScriptCache.ScriptPreProcessor getScriptPreProcessor() {
154        return scriptPreProcessor;
155    }
156
157    /* SCXMLEvaluator implementation methods */
158
159
160    @Override
161    public String getSupportedDatamodel() {
162        return SUPPORTED_DATA_MODEL;
163    }
164
165    /**
166     * Evaluate an expression.
167     *
168     * @param ctx variable context
169     * @param expr expression
170     * @return a result of the evaluation
171     * @throws SCXMLExpressionException For a malformed expression
172     * @see Evaluator#eval(Context, String)
173     */
174    @Override
175    public Object eval(final Context ctx, final String expr) throws SCXMLExpressionException {
176        if (expr == null) {
177            return null;
178        }
179
180        if (!(ctx instanceof GroovyContext)) {
181            throw new SCXMLExpressionException(ERR_CTX_TYPE);
182        }
183
184        final GroovyContext groovyCtx = (GroovyContext) ctx;
185        if (groovyCtx.getGroovyEvaluator() == null) {
186            groovyCtx.setGroovyEvaluator(this);
187        }
188        try {
189            return getScript(getEffectiveContext(groovyCtx), groovyCtx.getScriptBaseClass(), expr).run();
190        }
191        catch (Exception e) {
192            String exMessage = e.getMessage() != null ? e.getMessage() : e.getClass().getCanonicalName();
193            throw new SCXMLExpressionException("eval('" + expr + "'): " + exMessage, e);
194        }
195    }
196
197    /**
198     * @see Evaluator#evalCond(Context, String)
199     */
200    @Override
201    public Boolean evalCond(final Context ctx, final String expr) throws SCXMLExpressionException {
202        if (expr == null) {
203            return null;
204        }
205
206        if (!(ctx instanceof GroovyContext)) {
207            throw new SCXMLExpressionException(ERR_CTX_TYPE);
208        }
209
210        final GroovyContext groovyCtx = (GroovyContext) ctx;
211        if (groovyCtx.getGroovyEvaluator() == null) {
212            groovyCtx.setGroovyEvaluator(this);
213        }
214        try {
215            final Object result = getScript(getEffectiveContext(groovyCtx), groovyCtx.getScriptBaseClass(), expr).run();
216            return result == null ? Boolean.FALSE : (Boolean)result;
217        } catch (Exception e) {
218            String exMessage = e.getMessage() != null ? e.getMessage() : e.getClass().getCanonicalName();
219            throw new SCXMLExpressionException("evalCond('" + expr + "'): " + exMessage, e);
220        }
221    }
222
223    /**
224     * @see Evaluator#evalLocation(Context, String)
225     */
226    @Override
227    public Object evalLocation(final Context ctx, final String expr) throws SCXMLExpressionException {
228        if (expr == null) {
229            return null;
230        }
231        else if (ctx.has(expr)) {
232            return expr;
233        }
234
235        if (!(ctx instanceof GroovyContext)) {
236            throw new SCXMLExpressionException(ERR_CTX_TYPE);
237        }
238
239        GroovyContext groovyCtx = (GroovyContext) ctx;
240        if (groovyCtx.getGroovyEvaluator() == null) {
241            groovyCtx.setGroovyEvaluator(this);
242        }
243        try {
244            final GroovyContext effective = getEffectiveContext(groovyCtx);
245            return getScript(effective, groovyCtx.getScriptBaseClass(), expr).run();
246        } catch (Exception e) {
247            String exMessage = e.getMessage() != null ? e.getMessage() : e.getClass().getCanonicalName();
248            throw new SCXMLExpressionException("evalLocation('" + expr + "'): " + exMessage, e);
249        }
250    }
251
252    /**
253     * @see Evaluator#evalAssign(Context, String, Object, AssignType, String)
254     */
255    public void evalAssign(final Context ctx, final String location, final Object data, final AssignType type,
256                           final String attr) throws SCXMLExpressionException {
257
258        final Object loc = evalLocation(ctx, location);
259        if (loc != null) {
260
261            if (XPathBuiltin.isXPathLocation(ctx, loc)) {
262                XPathBuiltin.assign(ctx, loc, data, type, attr);
263            }
264            else {
265                final StringBuilder sb = new StringBuilder(location).append("=").append(ASSIGN_VARIABLE_NAME);
266                try {
267                    ctx.getVars().put(ASSIGN_VARIABLE_NAME, data);
268                    eval(ctx, sb.toString());
269                }
270                finally {
271                    ctx.getVars().remove(ASSIGN_VARIABLE_NAME);
272                }
273            }
274        }
275        else {
276            throw new SCXMLExpressionException("evalAssign - cannot resolve location: '" + location + "'");
277        }
278    }
279
280    /**
281     * @see Evaluator#evalScript(Context, String)
282     */
283    @Override
284    public Object evalScript(final Context ctx, final String scriptSource) throws SCXMLExpressionException {
285        if (scriptSource == null) {
286            return null;
287        }
288
289        if (!(ctx instanceof GroovyContext)) {
290            throw new SCXMLExpressionException(ERR_CTX_TYPE);
291        }
292
293        final GroovyContext groovyCtx = (GroovyContext) ctx;
294        if (groovyCtx.getGroovyEvaluator() == null) {
295            groovyCtx.setGroovyEvaluator(this);
296        }
297        try {
298            final GroovyContext effective = getEffectiveContext(groovyCtx);
299            final boolean inGlobalContext = groovyCtx.getParent() instanceof SCXMLSystemContext;
300            final Script script = getScript(effective, groovyCtx.getScriptBaseClass(), scriptSource);
301            final Object result = script.run();
302            if (inGlobalContext && useInitialScriptAsBaseScript) {
303                groovyCtx.setScriptBaseClass(script.getClass().getName());
304            }
305            return result;
306        } catch (Exception e) {
307            final String exMessage = e.getMessage() != null ? e.getMessage() : e.getClass().getCanonicalName();
308            throw new SCXMLExpressionException("evalScript('" + scriptSource + "'): " + exMessage, e);
309        }
310    }
311
312    protected ClassLoader getGroovyClassLoader() {
313        return scriptCache.getGroovyClassLoader();
314    }
315
316    /**
317     * Create a new child context.
318     *
319     * @param parent parent context
320     * @return new child context
321     * @see Evaluator#newContext(Context)
322     */
323    @Override
324    public Context newContext(final Context parent) {
325        return new GroovyContext(parent, this);
326    }
327
328    /**
329     * Create a new context which is the summation of contexts from the
330     * current state to document root, child has priority over parent
331     * in scoping rules.
332     *
333     * @param nodeCtx The GroovyContext for this state.
334     * @return The effective GroovyContext for the path leading up to
335     *         document root.
336     */
337    protected GroovyContext getEffectiveContext(final GroovyContext nodeCtx) {
338        return new GroovyContext(nodeCtx, new EffectiveContextMap(nodeCtx), this);
339    }
340}