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