View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.scxml2.env.groovy;
18  
19  import groovy.lang.Script;
20  
21  import java.io.Serializable;
22  import java.util.Collections;
23  import java.util.HashMap;
24  import java.util.Map;
25  import java.util.UUID;
26  import java.util.regex.Matcher;
27  import java.util.regex.Pattern;
28  
29  import org.apache.commons.scxml2.Context;
30  import org.apache.commons.scxml2.Evaluator;
31  import org.apache.commons.scxml2.EvaluatorProvider;
32  import org.apache.commons.scxml2.SCXMLExpressionException;
33  import org.apache.commons.scxml2.SCXMLSystemContext;
34  import org.apache.commons.scxml2.XPathBuiltin;
35  import org.apache.commons.scxml2.env.EffectiveContextMap;
36  import org.apache.commons.scxml2.model.SCXML;
37  
38  /**
39   * Evaluator implementation enabling use of Groovy expressions in SCXML documents.
40   * <P>
41   * This implementation itself is thread-safe, so you can keep singleton for efficiency.
42   * </P>
43   */
44  public class GroovyEvaluator implements Evaluator, Serializable {
45  
46      /** Serial version UID. */
47      private static final long serialVersionUID = 1L;
48  
49      /**
50       * Unique context variable name used for temporary reference to assign data (thus must be a valid variable name)
51       */
52      private static final String ASSIGN_VARIABLE_NAME = "a"+UUID.randomUUID().toString().replace('-','x');
53  
54      public static final String SUPPORTED_DATA_MODEL = "groovy";
55  
56      public static class GroovyEvaluatorProvider implements EvaluatorProvider {
57  
58          @Override
59          public String getSupportedDatamodel() {
60              return SUPPORTED_DATA_MODEL;
61          }
62  
63          @Override
64          public Evaluator getEvaluator() {
65              return new GroovyEvaluator();
66          }
67  
68          @Override
69          public Evaluator getEvaluator(final SCXML document) {
70              return new GroovyEvaluator();
71          }
72      }
73  
74      /** Error message if evaluation context is not a GroovyContext. */
75      private static final String ERR_CTX_TYPE = "Error evaluating Groovy "
76              + "expression, Context must be a org.apache.commons.scxml2.env.groovy.GroovyContext";
77  
78      protected static final GroovyExtendableScriptCache.ScriptPreProcessor scriptPreProcessor = new GroovyExtendableScriptCache.ScriptPreProcessor () {
79  
80          /**
81           * Pattern for case-sensitive matching of the Groovy operator aliases, delimited by whitespace
82           */
83          public final Pattern GROOVY_OPERATOR_ALIASES_PATTERN = Pattern.compile("(?<=\\s)(and|or|not|eq|lt|le|ne|gt|ge)(?=\\s)");
84  
85          /**
86           * Groovy operator aliases mapped to their underlying Groovy operator
87           */
88          public final Map<String, String> GROOVY_OPERATOR_ALIASES = Collections.unmodifiableMap(new HashMap<String, String>() {{
89              put("and", "&& "); put("or",  "||"); put("not", " ! ");
90              put("eq",  "==");  put("lt",  "< "); put("le",  "<=");
91              put("ne",  "!=");  put("gt",  "> "); put("ge",  ">=");
92          }});
93  
94          @Override
95          public String preProcess(final String script) {
96              if (script == null || script.length() == 0) {
97                  return script;
98              }
99              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 }