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 */
017
018package org.apache.commons.jexl3;
019
020import org.apache.commons.jexl3.introspection.JexlUberspect;
021
022import java.io.BufferedReader;
023import java.io.File;
024import java.io.FileInputStream;
025import java.io.IOException;
026import java.io.InputStreamReader;
027import java.math.MathContext;
028import java.net.URL;
029import java.nio.charset.Charset;
030
031/**
032 * Creates and evaluates JexlExpression and JexlScript objects.
033 * Determines the behavior of expressions and scripts during their evaluation with respect to:
034 * <ul>
035 * <li>Introspection, see {@link JexlUberspect}</li>
036 * <li>Arithmetic and comparison, see {@link JexlArithmetic}</li>
037 * <li>Error reporting</li>
038 * <li>Logging</li>
039 * </ul>
040 *
041 * <p>Note that methods that evaluate expressions may throw <em>unchecked</em> exceptions;
042 * The {@link JexlException} are thrown in "non-silent" mode but since these are
043 * RuntimeException, user-code <em>should</em> catch them wherever most appropriate.</p>
044 *
045 * @since 2.0
046 */
047public abstract class JexlEngine {
048
049    /** A marker for invocation failures in tryInvoke. */
050    public static final Object TRY_FAILED = new Object() {
051        @Override
052        public String toString() {
053            return "tryExecute failed";
054        }
055    };
056
057    /**
058     * The thread local context.
059     */
060    protected static final java.lang.ThreadLocal<JexlContext.ThreadLocal> CONTEXT =
061            new java.lang.ThreadLocal<JexlContext.ThreadLocal>() {
062                @Override
063                protected JexlContext.ThreadLocal initialValue() {
064                    return null;
065                }
066            };
067
068    /**
069     * Accesses the current thread local context.
070     *
071     * @return the context or null
072     */
073    public static JexlContext.ThreadLocal getThreadContext() {
074        return CONTEXT.get();
075    }
076
077    /**
078     * Sets the current thread local context.
079     * <p>This should only be used carefully, for instance when re-evaluating a "stored" script that requires a
080     * given Namespace resolver. Remember to synchronize access if context is shared between threads.
081     *
082     * @param tls the thread local context to set
083     */
084    public static void setThreadContext(JexlContext.ThreadLocal tls) {
085        CONTEXT.set(tls);
086    }
087
088    /**
089     * Script evaluation options.
090     * <p>The JexlContext used for evaluation can implement this interface to alter behavior.</p>
091     */
092    public interface Options {
093
094        /**
095         * The charset used for parsing.
096         *
097         * @return the charset
098         */
099        Charset getCharset();
100        /**
101         * Sets whether the engine will throw a {@link JexlException} when an error is encountered during evaluation.
102         *
103         * @return true if silent, false otherwise
104         */
105        Boolean isSilent();
106
107        /**
108         * Checks whether the engine considers unknown variables, methods, functions and constructors as errors or
109         * evaluates them as null.
110         *
111         * @return true if strict, false otherwise
112         */
113        Boolean isStrict();
114
115        /**
116         * Checks whether the arithmetic triggers errors during evaluation when null is used as an operand.
117         *
118         * @return true if strict, false otherwise
119         */
120        Boolean isStrictArithmetic();
121
122        /**
123         * Whether evaluation will throw JexlException.Cancel (true) or return null (false) when interrupted.
124         * @return true when cancellable, false otherwise
125         * @since 3.1
126         */
127        Boolean isCancellable();
128
129        /**
130         * The MathContext instance used for +,-,/,*,% operations on big decimals.
131         *
132         * @return the math context
133         */
134        MathContext getArithmeticMathContext();
135
136        /**
137         * The BigDecimal scale used for comparison and coercion operations.
138         *
139         * @return the scale
140         */
141        int getArithmeticMathScale();
142    }
143
144    /**
145     * An empty/static/non-mutable JexlContext used instead of null context.
146     */
147    public static final JexlContext EMPTY_CONTEXT = new JexlContext() {
148        @Override
149        public Object get(String name) {
150            return null;
151        }
152
153        @Override
154        public boolean has(String name) {
155            return false;
156        }
157
158        @Override
159        public void set(String name, Object value) {
160            throw new UnsupportedOperationException("Not supported in void context.");
161        }
162    };
163
164    /**
165     * An empty/static/non-mutable JexlNamesapce used instead of null namespace.
166     */
167    public static final JexlContext.NamespaceResolver EMPTY_NS = new JexlContext.NamespaceResolver() {
168        @Override
169        public Object resolveNamespace(String name) {
170            return null;
171        }
172    };
173
174    /** The default Jxlt cache size. */
175    private static final int JXLT_CACHE_SIZE = 256;
176
177    /**
178     * Gets the charset used for parsing.
179     *
180     * @return the charset
181     */
182    public abstract Charset getCharset();
183
184    /**
185     * Gets this engine underlying {@link JexlUberspect}.
186     *
187     * @return the uberspect
188     */
189    public abstract JexlUberspect getUberspect();
190
191    /**
192     * Gets this engine underlying {@link JexlArithmetic}.
193     *
194     * @return the arithmetic
195     */
196    public abstract JexlArithmetic getArithmetic();
197
198    /**
199     * Checks whether this engine is in debug mode.
200     *
201     * @return true if debug is on, false otherwise
202     */
203    public abstract boolean isDebug();
204
205    /**
206     * Checks whether this engine throws JexlException during evaluation.
207     *
208     * @return true if silent, false (default) otherwise
209     */
210    public abstract boolean isSilent();
211
212    /**
213     * Checks whether this engine considers unknown variables, methods, functions and constructors as errors.
214     *
215     * @return true if strict, false otherwise
216     */
217    public abstract boolean isStrict();
218
219    /**
220     * Checks whether this engine will throw JexlException.Cancel (true) or return null (false) when interrupted
221     * during an execution.
222     *
223     * @return true if cancellable, false otherwise
224     */
225    public abstract boolean isCancellable();
226
227    /**
228     * Sets the class loader used to discover classes in 'new' expressions.
229     * <p>This method is <em>not</em> thread safe; it should be called as an optional step of the JexlEngine
230     * initialization code before expression creation &amp; evaluation.</p>
231     *
232     * @param loader the class loader to use
233     */
234    public abstract void setClassLoader(ClassLoader loader);
235
236    /**
237     * Creates a new {@link JxltEngine} instance using this engine.
238     *
239     * @return a JEXL Template engine
240     */
241    public JxltEngine createJxltEngine() {
242        return createJxltEngine(true);
243    }
244
245    /**
246     * Creates a new {@link JxltEngine} instance using this engine.
247     *
248     * @param noScript  whether the JxltEngine only allows Jexl expressions or scripts
249     * @return a JEXL Template engine
250     */
251    public JxltEngine createJxltEngine(boolean noScript) {
252        return createJxltEngine(noScript, JXLT_CACHE_SIZE, '$', '#');
253    }
254
255    /**
256     * Creates a new instance of {@link JxltEngine} using this engine.
257     *
258     * @param noScript  whether the JxltEngine only allows JEXL expressions or scripts
259     * @param cacheSize the number of expressions in this cache, default is 256
260     * @param immediate the immediate template expression character, default is '$'
261     * @param deferred  the deferred template expression character, default is '#'
262     * @return a JEXL Template engine
263     */
264    public abstract JxltEngine createJxltEngine(boolean noScript, int cacheSize, char immediate, char deferred);
265
266    /**
267     * Clears the expression cache.
268     */
269    public abstract void clearCache();
270
271    /**
272     * Creates an JexlExpression from a String containing valid JEXL syntax.
273     * This method parses the expression which must contain either a reference or an expression.
274     *
275     * @param info       An info structure to carry debugging information if needed
276     * @param expression A String containing valid JEXL syntax
277     * @return An {@link JexlExpression} which can be evaluated using a {@link JexlContext}
278     * @throws JexlException if there is a problem parsing the script
279     */
280    public abstract JexlExpression createExpression(JexlInfo info, String expression);
281
282    /**
283     * Creates a JexlScript from a String containing valid JEXL syntax.
284     * This method parses the script and validates the syntax.
285     *
286     * @param info   An info structure to carry debugging information if needed
287     * @param source A string containing valid JEXL syntax
288     * @param names  The script parameter names used during parsing; a corresponding array of arguments containing
289     * values should be used during evaluation
290     * @return A {@link JexlScript} which can be executed using a {@link JexlContext}
291     * @throws JexlException if there is a problem parsing the script
292     */
293    public abstract JexlScript createScript(JexlInfo info, String source, String[] names);
294
295    /**
296     * Creates a JexlExpression from a String containing valid JEXL syntax.
297     * This method parses the expression which must contain either a reference or an expression.
298     *
299     * @param expression A String containing valid JEXL syntax
300     * @return An {@link JexlExpression} which can be evaluated using a {@link JexlContext}
301     * @throws JexlException if there is a problem parsing the script
302     */
303    public final JexlExpression createExpression(String expression) {
304        return createExpression(null, expression);
305    }
306
307    /**
308     * Creates a Script from a String containing valid JEXL syntax.
309     * This method parses the script and validates the syntax.
310     *
311     * @param scriptText A String containing valid JEXL syntax
312     * @return A {@link JexlScript} which can be executed using a {@link JexlContext}
313     * @throws JexlException if there is a problem parsing the script.
314     */
315    public final JexlScript createScript(String scriptText) {
316        return createScript(null, scriptText, null);
317    }
318
319    /**
320     * Creates a Script from a String containing valid JEXL syntax.
321     * This method parses the script and validates the syntax.
322     *
323     * @param scriptText A String containing valid JEXL syntax
324     * @param names      The script parameter names used during parsing; a corresponding array of arguments containing
325     * values should be used during evaluation
326     * @return A {@link JexlScript} which can be executed using a {@link JexlContext}
327     * @throws JexlException if there is a problem parsing the script
328     */
329    public final JexlScript createScript(String scriptText, String... names) {
330        return createScript(null, scriptText, names);
331    }
332
333    /**
334     * Creates a Script from a {@link File} containing valid JEXL syntax.
335     * This method parses the script and validates the syntax.
336     *
337     * @param scriptFile A {@link File} containing valid JEXL syntax. Must not be null. Must be a readable file.
338     * @return A {@link JexlScript} which can be executed with a {@link JexlContext}.
339     * @throws JexlException if there is a problem reading or parsing the script.
340     */
341    public final JexlScript createScript(File scriptFile) {
342        return createScript(null, readSource(scriptFile), null);
343    }
344
345    /**
346     * Creates a Script from a {@link File} containing valid JEXL syntax.
347     * This method parses the script and validates the syntax.
348     *
349     * @param scriptFile A {@link File} containing valid JEXL syntax. Must not be null. Must be a readable file.
350     * @param names      The script parameter names used during parsing; a corresponding array of arguments containing
351     * values should be used during evaluation.
352     * @return A {@link JexlScript} which can be executed with a {@link JexlContext}.
353     * @throws JexlException if there is a problem reading or parsing the script.
354     */
355    public final JexlScript createScript(File scriptFile, String... names) {
356        return createScript(null, readSource(scriptFile), names);
357    }
358
359    /**
360     * Creates a Script from a {@link File} containing valid JEXL syntax.
361     * This method parses the script and validates the syntax.
362     *
363     * @param info       An info structure to carry debugging information if needed
364     * @param scriptFile A {@link File} containing valid JEXL syntax. Must not be null. Must be a readable file.
365     * @param names      The script parameter names used during parsing; a corresponding array of arguments containing
366     * values should be used during evaluation.
367     * @return A {@link JexlScript} which can be executed with a {@link JexlContext}.
368     * @throws JexlException if there is a problem reading or parsing the script.
369     */
370    public final JexlScript createScript(JexlInfo info, File scriptFile, String[] names) {
371        return createScript(info, readSource(scriptFile), names);
372    }
373
374    /**
375     * Creates a Script from a {@link URL} containing valid JEXL syntax.
376     * This method parses the script and validates the syntax.
377     *
378     * @param scriptUrl A {@link URL} containing valid JEXL syntax. Must not be null.
379     * @return A {@link JexlScript} which can be executed with a {@link JexlContext}.
380     * @throws JexlException if there is a problem reading or parsing the script.
381     */
382    public final JexlScript createScript(URL scriptUrl) {
383        return createScript(null, readSource(scriptUrl), null);
384    }
385
386    /**
387     * Creates a Script from a {@link URL} containing valid JEXL syntax.
388     * This method parses the script and validates the syntax.
389     *
390     * @param scriptUrl A {@link URL} containing valid JEXL syntax. Must not be null.
391     * @param names     The script parameter names used during parsing; a corresponding array of arguments containing
392     * values should be used during evaluation.
393     * @return A {@link JexlScript} which can be executed with a {@link JexlContext}.
394     * @throws JexlException if there is a problem reading or parsing the script.
395     */
396    public final JexlScript createScript(URL scriptUrl, String[] names) {
397        return createScript(null, readSource(scriptUrl), names);
398    }
399
400    /**
401     * Creates a Script from a {@link URL} containing valid JEXL syntax.
402     * This method parses the script and validates the syntax.
403     *
404     * @param info      An info structure to carry debugging information if needed
405     * @param scriptUrl A {@link URL} containing valid JEXL syntax. Must not be null.
406     * @param names     The script parameter names used during parsing; a corresponding array of arguments containing
407     * values should be used during evaluation.
408     * @return A {@link JexlScript} which can be executed with a {@link JexlContext}.
409     * @throws JexlException if there is a problem reading or parsing the script.
410     */
411    public final JexlScript createScript(JexlInfo info, URL scriptUrl, String[] names) {
412        return createScript(info, readSource(scriptUrl), names);
413    }
414
415    /**
416     * Accesses properties of a bean using an expression.
417     * <p>
418     * jexl.get(myobject, "foo.bar"); should equate to
419     * myobject.getFoo().getBar(); (or myobject.getFoo().get("bar"))
420     * </p>
421     * <p>
422     * If the JEXL engine is silent, errors will be logged through its logger as warning.
423     * </p>
424     *
425     * @param bean the bean to get properties from
426     * @param expr the property expression
427     * @return the value of the property
428     * @throws JexlException if there is an error parsing the expression or during evaluation
429     */
430    public abstract Object getProperty(Object bean, String expr);
431
432    /**
433     * Accesses properties of a bean using an expression.
434     * <p>
435     * If the JEXL engine is silent, errors will be logged through its logger as warning.
436     * </p>
437     *
438     * @param context the evaluation context
439     * @param bean    the bean to get properties from
440     * @param expr    the property expression
441     * @return the value of the property
442     * @throws JexlException if there is an error parsing the expression or during evaluation
443     */
444    public abstract Object getProperty(JexlContext context, Object bean, String expr);
445
446    /**
447     * Assign properties of a bean using an expression.
448     * <p>
449     * jexl.set(myobject, "foo.bar", 10); should equate to
450     * myobject.getFoo().setBar(10); (or myobject.getFoo().put("bar", 10) )
451     * </p>
452     * <p>
453     * If the JEXL engine is silent, errors will be logged through its logger as warning.
454     * </p>
455     *
456     * @param bean  the bean to set properties in
457     * @param expr  the property expression
458     * @param value the value of the property
459     * @throws JexlException if there is an error parsing the expression or during evaluation
460     */
461    public abstract void setProperty(Object bean, String expr, Object value);
462
463    /**
464     * Assign properties of a bean using an expression. <p> If the JEXL engine is silent, errors will be logged through
465     * its logger as warning. </p>
466     *
467     * @param context the evaluation context
468     * @param bean    the bean to set properties in
469     * @param expr    the property expression
470     * @param value   the value of the property
471     * @throws JexlException if there is an error parsing the expression or during evaluation
472     */
473    public abstract void setProperty(JexlContext context, Object bean, String expr, Object value);
474
475    /**
476     * Invokes an object's method by name and arguments.
477     *
478     * @param obj  the method's invoker object
479     * @param meth the method's name
480     * @param args the method's arguments
481     * @return the method returned value or null if it failed and engine is silent
482     * @throws JexlException if method could not be found or failed and engine is not silent
483     */
484    public abstract Object invokeMethod(Object obj, String meth, Object... args);
485
486    /**
487     * Creates a new instance of an object using the most appropriate constructor based on the arguments.
488     *
489     * @param <T>   the type of object
490     * @param clazz the class to instantiate
491     * @param args  the constructor arguments
492     * @return the created object instance or null on failure when silent
493     */
494    public abstract <T> T newInstance(Class<? extends T> clazz, Object... args);
495
496    /**
497     * Creates a new instance of an object using the most appropriate constructor based on the arguments.
498     *
499     * @param clazz the name of the class to instantiate resolved through this engine's class loader
500     * @param args  the constructor arguments
501     * @return the created object instance or null on failure when silent
502     */
503    public abstract Object newInstance(String clazz, Object... args);
504
505    /**
506     * Creates a JexlInfo instance.
507     *
508     * @param fn url/file/template/script user given name
509     * @param l  line number
510     * @param c  column number
511     * @return a JexlInfo instance
512     */
513    public JexlInfo createInfo(String fn, int l, int c) {
514        return new JexlInfo(fn, l, c);
515    }
516
517    /**
518     * Create an information structure for dynamic set/get/invoke/new.
519     * <p>This gathers the class, method and line number of the first calling method
520     * outside of o.a.c.jexl3.</p>
521     *
522     * @return a JexlInfo instance
523     */
524    public JexlInfo createInfo() {
525        JexlInfo info = null;
526        StackTraceElement[] stack = new Throwable().getStackTrace();
527        StackTraceElement se = null;
528        String name = getClass().getName();
529        for (int s = 1; s < stack.length; ++s) {
530            se = stack[s];
531            String className = se.getClassName();
532            if (!className.equals(name)) {
533                // go deeper if called from jexl implementation classes
534                if (className.startsWith("org.apache.commons.jexl3.internal.")
535                    || className.startsWith("org.apache.commons.jexl3.J")) {
536                    name = className;
537                } else {
538                    break;
539                }
540            }
541        }
542        if (se != null) {
543            info = createInfo(se.getClassName() + "." + se.getMethodName(), se.getLineNumber(), 0);
544        }
545        return info;
546    }
547
548    /**
549     * Creates a string from a reader.
550     *
551     * @param reader to be read.
552     * @return the contents of the reader as a String.
553     * @throws IOException on any error reading the reader.
554     */
555    protected static String toString(BufferedReader reader) throws IOException {
556        StringBuilder buffer = new StringBuilder();
557        String line;
558        while ((line = reader.readLine()) != null) {
559            buffer.append(line).append('\n');
560        }
561        return buffer.toString();
562    }
563
564    /**
565     * Reads a JEXL source from a File.
566     *
567     * @param file the script file
568     * @return the source
569     */
570    protected String readSource(final File file) {
571        if (file == null) {
572            throw new NullPointerException("source file is null");
573        }
574        BufferedReader reader = null;
575        try {
576            reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), getCharset()));
577            return toString(reader);
578        } catch (IOException xio) {
579            throw new JexlException(createInfo(file.toString(), 1, 1), "could not read source File", xio);
580        } finally {
581            if (reader != null) {
582                try {
583                    reader.close();
584                } catch (IOException xignore) {
585                    // cant do much
586                }
587            }
588        }
589    }
590
591    /**
592     * Reads a JEXL source from an URL.
593     *
594     * @param url the script url
595     * @return the source
596     */
597    protected String readSource(final URL url) {
598        if (url == null) {
599            throw new NullPointerException("source URL is null");
600        }
601        BufferedReader reader = null;
602        try {
603            reader = new BufferedReader(new InputStreamReader(url.openStream(), getCharset()));
604            return toString(reader);
605        } catch (IOException xio) {
606            throw new JexlException(createInfo(url.toString(), 1, 1), "could not read source URL", xio);
607        } finally {
608            if (reader != null) {
609                try {
610                    reader.close();
611                } catch (IOException xignore) {
612                    // cant do much
613                }
614            }
615        }
616    }
617}