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