1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 package org.apache.commons.jexl3;
19
20 import java.io.BufferedReader;
21 import java.io.File;
22 import java.io.IOException;
23 import java.io.InputStreamReader;
24 import java.math.MathContext;
25 import java.net.URL;
26 import java.nio.charset.Charset;
27 import java.nio.file.Files;
28 import java.util.Objects;
29
30 import org.apache.commons.jexl3.introspection.JexlUberspect;
31
32 /**
33 * Creates and evaluates JexlExpression and JexlScript objects.
34 * Determines the behavior of expressions and scripts during their evaluation with respect to:
35 * <ul>
36 * <li>Introspection, see {@link JexlUberspect}</li>
37 * <li>Arithmetic and comparison, see {@link JexlArithmetic}</li>
38 * <li>Error reporting</li>
39 * <li>Logging</li>
40 * </ul>
41 *
42 * <p>Note that methods that evaluate expressions may throw <em>unchecked</em> exceptions;
43 * The {@link JexlException} are thrown in "non-silent" mode but since these are
44 * RuntimeException, user-code <em>should</em> catch them wherever most appropriate.</p>
45 *
46 * @since 2.0
47 */
48 public abstract class JexlEngine {
49
50 /** Default constructor */
51 public JexlEngine() {} // Keep Javadoc happy
52
53 /**
54 * The empty context class, public for instrospection.
55 */
56 public static final class EmptyContext implements JexlContext {
57
58 /**
59 * Default ctor.
60 */
61 EmptyContext() {}
62
63 @Override
64 public Object get(final String name) {
65 return null;
66 }
67
68 @Override
69 public boolean has(final String name) {
70 return false;
71 }
72
73 @Override
74 public void set(final String name, final Object value) {
75 throw new UnsupportedOperationException("Not supported in void context.");
76 }
77 }
78
79 /**
80 * The empty/static/non-mutable JexlNamespace class, public for instrospection.
81 */
82 public static final class EmptyNamespaceResolver implements JexlContext.NamespaceResolver {
83
84 /**
85 * Default ctor.
86 */
87 EmptyNamespaceResolver() {}
88
89 @Override
90 public Object resolveNamespace(final String name) {
91 return null;
92 }
93 }
94
95 /** The failure marker class. */
96 private static final class FailObject {
97
98 /**
99 * 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 }