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 & 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 & 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