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}