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