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    *      https://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.jexl3.internal;
18  
19  import java.util.List;
20  import java.util.Map;
21  import java.util.Objects;
22  import java.util.Set;
23  
24  import org.apache.commons.jexl3.JexlContext;
25  import org.apache.commons.jexl3.JexlEngine;
26  import org.apache.commons.jexl3.JexlExpression;
27  import org.apache.commons.jexl3.JexlFeatures;
28  import org.apache.commons.jexl3.JexlInfo;
29  import org.apache.commons.jexl3.JexlOptions;
30  import org.apache.commons.jexl3.JexlScript;
31  import org.apache.commons.jexl3.parser.ASTJexlScript;
32  
33  /**
34   * <p>A JexlScript implementation.</p>
35   *
36   * @since 1.1
37   */
38  public class Script implements JexlScript, JexlExpression {
39  
40      /**
41       * Implements the Future and Callable interfaces to help delegation.
42       */
43      public class Callable implements java.util.concurrent.Callable<Object> {
44  
45          /** The actual interpreter. */
46          protected final Interpreter interpreter;
47  
48          /** Use interpreter as marker for not having run. */
49          protected volatile Object result;
50  
51          /**
52           * The base constructor.
53           *
54           * @param intrprtr the interpreter to use
55           */
56          protected Callable(final Interpreter intrprtr) {
57              this.interpreter = intrprtr;
58              this.result = intrprtr;
59          }
60  
61          @Override
62          public Object call() throws Exception {
63              synchronized(this) {
64                  if (result == interpreter) {
65                      checkCacheVersion();
66                      result = interpret();
67                  }
68                  return result;
69              }
70          }
71  
72          /**
73           * Soft cancel the execution.
74           *
75           * @return true if cancel was successful, false otherwise
76           */
77          public boolean cancel() {
78              return interpreter.cancel();
79          }
80  
81          /**
82           * Run the interpreter.
83           *
84           * @return the evaluation result
85           */
86          protected Object interpret() {
87              return interpreter.interpret(script);
88          }
89  
90          /**
91           * @return true if interruption will throw a JexlException.Cancel, false otherwise
92           */
93          public boolean isCancellable() {
94              return interpreter.isCancellable();
95          }
96  
97          /**
98           * @return true if evaluation was canceled, false otherwise
99           */
100         public boolean isCancelled() {
101             return interpreter.isCancelled();
102         }
103     }
104 
105     /**
106      * The engine for this expression.
107      */
108     protected final Engine jexl;
109 
110     /**
111      * Original expression stripped from leading and trailing spaces.
112      */
113     protected final String source;
114 
115     /**
116      * The resulting AST we can interpret.
117      */
118     protected final ASTJexlScript script;
119 
120     /**
121      * The engine version (as class loader change count) that last evaluated this script.
122      */
123     protected int version;
124 
125     /**
126      * Do not let this be generally instantiated with a 'new'.
127      *
128      * @param engine the interpreter to evaluate the expression
129      * @param expr   the expression source.
130      * @param ref    the parsed expression.
131      */
132     protected Script(final Engine engine, final String expr, final ASTJexlScript ref) {
133         jexl = engine;
134         source = expr;
135         script = ref;
136         version = jexl.getUberspect().getVersion();
137     }
138 
139     /**
140      * Creates a Callable from this script.
141      * <p>This allows submitting it to an executor pool and provides support for asynchronous calls.</p>
142      * <p>The interpreter will handle interruption/cancellation gracefully if needed.</p>
143      *
144      * @param context the context
145      * @return the callable
146      */
147     @Override
148     public Callable callable(final JexlContext context) {
149         return callable(context, (Object[]) null);
150     }
151 
152     /**
153      * Creates a Callable from this script.
154      * <p>This allows submitting it to an executor pool and provides support for asynchronous calls.</p>
155      * <p>The interpreter will handle interruption/cancellation gracefully if needed.</p>
156      *
157      * @param context the context
158      * @param args    the script arguments
159      * @return the callable
160      */
161     @Override
162     public Callable callable(final JexlContext context, final Object... args) {
163         return new Callable(createInterpreter(context, script.createFrame(args)));
164     }
165 
166     /**
167      * Checks that this script cached methods (wrt introspection) matches the engine version.
168      * <p>
169      * If the engine class loader has changed since we last evaluated this script, the script local cache
170      * is invalidated to drop references to obsolete methods. It is not strictly necessary since the tryExecute
171      * will fail because the class won't match but it seems cleaner nevertheless.
172      * </p>
173      */
174     protected void checkCacheVersion() {
175         final int uberVersion = jexl.getUberspect().getVersion();
176         if (version != uberVersion) {
177             // version 0 of the uberSpect is an illusion due to order of construction; no need to clear cache
178             if (version > 0) {
179                 script.clearCache();
180             }
181             version = uberVersion;
182         }
183     }
184 
185     /**
186      * Creates this script frame for evaluation.
187      *
188      * @param args the arguments to bind to parameters
189      * @return the frame (may be null)
190      */
191     protected Frame createFrame(final Object[] args) {
192         return script.createFrame(args);
193     }
194 
195     /**
196      * Creates this script interpreter.
197      *
198      * @param context the context
199      * @param frame the calling frame
200      * @return  the interpreter
201      */
202     protected Interpreter createInterpreter(final JexlContext context, final Frame frame) {
203         return createInterpreter(context, frame, null);
204     }
205 
206     /**
207      * Creates this script interpreter.
208      *
209      * @param context the context
210      * @param frame the calling frame
211      * @param options the interpreter options
212      * @return  the interpreter
213      */
214     protected Interpreter createInterpreter(final JexlContext context, final Frame frame, final JexlOptions options) {
215         return jexl.createInterpreter(context, frame, options != null ? options : jexl.evalOptions(script, context));
216     }
217 
218     @Override
219     public JexlScript curry(final Object... args) {
220         final String[] parms = script.getParameters();
221         if (parms == null || parms.length == 0) {
222             return this;
223         }
224         return new Closure(this, args);
225     }
226 
227     @Override
228     public boolean equals(final Object obj) {
229         if (obj == null) {
230             return false;
231         }
232         if (getClass() != obj.getClass()) {
233             return false;
234         }
235         final Script other = (Script) obj;
236         if (this.jexl != other.jexl) {
237             return false;
238         }
239         if (!Objects.equals(this.source, other.source)) {
240             return false;
241         }
242         return true;
243     }
244 
245     @Override
246     public Object evaluate(final JexlContext context) {
247         return execute(context);
248     }
249 
250     @Override
251     public Object execute(final JexlContext context) {
252         checkCacheVersion();
253         final Frame frame = createFrame(null);
254         final Interpreter interpreter = createInterpreter(context, frame);
255         return interpreter.interpret(script);
256     }
257 
258     @Override
259     public Object execute(final JexlContext context, final Object... args) {
260         checkCacheVersion();
261         final Frame frame = createFrame(args != null && args.length > 0 ? args : null);
262         final Interpreter interpreter = createInterpreter(context, frame);
263         return interpreter.interpret(script);
264     }
265 
266     /**
267      * Gets this script captured variable, i.e. symbols captured from outer scopes.
268      *
269      * @return the captured variable names
270      */
271     public String[] getCapturedVariables() {
272         return script.getCapturedVariables();
273     }
274 
275     /**
276      * @return the engine that created this script
277      */
278     public JexlEngine getEngine() {
279         return jexl;
280     }
281 
282     /**
283      * @return the script features
284      */
285     public JexlFeatures getFeatures() {
286         return script.getFeatures();
287     }
288 
289     /**
290      * @return the info
291      */
292     public JexlInfo getInfo() {
293         return script.jexlInfo();
294     }
295 
296     @Override
297     public String[] getLocalVariables() {
298         return script.getLocalVariables();
299     }
300 
301     @Override
302     public String[] getParameters() {
303         return script.getParameters();
304     }
305 
306     @Override
307     public String getParsedText() {
308         return getParsedText(2);
309     }
310 
311     @Override
312     public String getParsedText(final int indent) {
313         final Debugger debug = new Debugger();
314         debug.outputPragmas(true).indentation(indent).debug(script, false);
315         return debug.toString();
316     }
317 
318     /**
319      * Gets this script pragmas
320      * <p>Pragma keys are ant-ish variables, their values are scalar literals.
321      *
322      * @return the pragmas
323      */
324     @Override
325     public Map<String, Object> getPragmas() {
326         return script.getPragmas();
327     }
328 
329     /**
330      * @return the script AST
331      */
332     protected ASTJexlScript getScript() {
333         return script;
334     }
335 
336     @Override
337     public String getSourceText() {
338         return source;
339     }
340 
341     @Override
342     public String[] getUnboundParameters() {
343         return getParameters();
344     }
345 
346     /**
347      * Gets this script variables.
348      * <p>Note that since variables can be in an ant-ish form (ie foo.bar.quux), each variable is returned as
349      * a list of strings where each entry is a fragment of the variable ({"foo", "bar", "quux"} in the example.</p>
350      *
351      * @return the variables or null
352      */
353     @Override
354     public Set<List<String>> getVariables() {
355         return jexl.getVariables(script);
356     }
357 
358     @Override
359     public int hashCode() {
360         // CSOFF: Magic number
361         int hash = 17;
362         hash = 31 * hash + (this.jexl != null ? this.jexl.hashCode() : 0);
363         hash = 31 * hash + (this.source != null ? this.source.hashCode() : 0);
364         return hash;
365         // CSON: Magic number
366     }
367 
368     @Override
369     public String toString() {
370         CharSequence src = source;
371         if (src == null) {
372             final Debugger debug = new Debugger();
373             debug.debug(script, false);
374             src = debug.toString();
375         }
376         return src.toString();
377     }
378 }