View Javadoc
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.math.MathContext;
21  import java.util.Collection;
22  import java.util.Collections;
23  import java.util.Map;
24  
25  import org.apache.commons.jexl3.internal.Engine;
26  
27  /**
28   * Flags and properties that can alter the evaluation behavior.
29   * The flags, briefly explained, are the following:
30   * <ul>
31   * <li>silent: whether errors throw exception</li>
32   * <li>safe: whether navigation through null is <em>not</em>an error</li>
33   * <li>cancellable: whether thread interruption is an error</li>
34   * <li>lexical: whether redefining local variables is an error</li>
35   * <li>lexicalShade: whether local variables shade global ones even outside their scope</li>
36   * <li>strict: whether unknown or unsolvable identifiers are errors</li>
37   * <li>strictArithmetic: whether null as operand is an error</li>
38   * <li>sharedInstance: whether these options can be modified at runtime during execution (expert)</li>
39   * <li>constCapture: whether captured variables will throw an error if an attempt is made to change their value</li>
40   * <li>strictInterpolation: whether interpolation strings always return a string or attempt to parse and return integer</li>
41   * <li>booleanLogical: whether logical expressions (&quot;&quot; , ||) coerce their result to boolean</li>
42   * </ul>
43   * The sensible default is cancellable, strict and strictArithmetic.
44   * <p>This interface replaces the now deprecated JexlEngine.Options.
45   *
46   * @since 3.2
47   */
48  public final class JexlOptions {
49  
50      /** The boolean logical flag. */
51      private static final int BOOLEAN_LOGICAL = 10;
52  
53      /** The interpolation string bit. */
54      private static final int STRICT_INTERPOLATION= 9;
55  
56      /** The const capture bit. */
57      private static final int CONST_CAPTURE = 8;
58  
59      /** The shared instance bit. */
60      private static final int SHARED = 7;
61  
62      /** The local shade bit. */
63      private static final int SHADE = 6;
64  
65      /** The antish var bit. */
66      private static final int ANTISH = 5;
67  
68      /** The lexical scope bit. */
69      private static final int LEXICAL = 4;
70  
71      /** The safe bit. */
72      private static final int SAFE = 3;
73  
74      /** The silent bit. */
75      private static final int SILENT = 2;
76  
77      /** The strict bit. */
78      private static final int STRICT = 1;
79  
80      /** The cancellable bit. */
81      private static final int CANCELLABLE = 0;
82  
83      /** The flag names ordered. */
84      private static final String[] NAMES = {
85          "cancellable", "strict", "silent", "safe", "lexical", "antish",
86          "lexicalShade", "sharedInstance", "constCapture", "strictInterpolation",
87          "booleanShortCircuit"
88      };
89  
90      /** Default mask .*/
91      private static int DEFAULT = 1 /*<< CANCELLABLE*/ | 1 << STRICT | 1 << ANTISH | 1 << SAFE;
92  
93      /**
94       * Checks the value of a flag in the mask.
95       *
96       * @param ordinal the flag ordinal
97       * @param mask the flags mask
98       * @return the mask value with this flag or-ed in
99       */
100     private static boolean isSet(final int ordinal, final int mask) {
101         return (mask & 1 << ordinal) != 0;
102     }
103 
104     /**
105      * Parses flags by name.
106      * <p>A '+flag' or 'flag' will set flag as true, '-flag' set as false.
107      * The possible flag names are:
108      * cancellable, strict, silent, safe, lexical, antish, lexicalShade
109      *
110      * @param initial the initial mask state
111      * @param flags the flags to set
112      * @return the flag mask updated
113      */
114     public static int parseFlags(final int initial, final String... flags) {
115         int mask = initial;
116         for (final String flag : flags) {
117             boolean b = true;
118             final String name;
119             if (flag.charAt(0) == '+') {
120                 name = flag.substring(1);
121             } else if (flag.charAt(0) == '-') {
122                 name = flag.substring(1);
123                 b = false;
124             } else {
125                 name = flag;
126             }
127             for (int f = 0; f < NAMES.length; ++f) {
128                 if (NAMES[f].equals(name)) {
129                     if (b) {
130                         mask |= 1 << f;
131                     } else {
132                         mask &= ~(1 << f);
133                     }
134                     break;
135                 }
136             }
137         }
138         return mask;
139     }
140 
141     /**
142      * Sets the value of a flag in a mask.
143      *
144      * @param ordinal the flag ordinal
145      * @param mask the flags mask
146      * @param value true or false
147      * @return the new flags mask value
148      */
149     private static int set(final int ordinal, final int mask, final boolean value) {
150         return value? mask | 1 << ordinal : mask & ~(1 << ordinal);
151     }
152 
153     /**
154      * Sets the default (static, shared) option flags.
155      * <p>
156      * Whenever possible, we recommend using JexlBuilder methods to unambiguously instantiate a JEXL
157      * engine; this method should only be used for testing / validation.
158      * <p>A '+flag' or 'flag' will set the option named 'flag' as true, '-flag' set as false.
159      * The possible flag names are:
160      * cancellable, strict, silent, safe, lexical, antish, lexicalShade
161      * <p>Calling JexlBuilder.setDefaultOptions("+safe") once before JEXL engine creation
162      * may ease validating JEXL3.2 in your environment.
163      *
164      * @param flags the flags to set
165      */
166     public static void setDefaultFlags(final String...flags) {
167         DEFAULT = parseFlags(DEFAULT, flags);
168     }
169 
170     /** The arithmetic math context. */
171     private MathContext mathContext;
172 
173     /** The arithmetic math scale. */
174     private int mathScale = Integer.MIN_VALUE;
175 
176     /** The arithmetic strict math flag. */
177     private boolean strictArithmetic = true;
178 
179     /** The default flags, all but safe. */
180     private int flags = DEFAULT;
181 
182     /** The namespaces .*/
183     private Map<String, Object> namespaces = Collections.emptyMap();
184 
185     /** The imports. */
186     private Collection<String> imports = Collections.emptySet();
187 
188     /**
189      * Default ctor.
190      */
191     public JexlOptions() {
192         // all inits in members declarations
193     }
194 
195     /**
196      * Creates a copy of this instance.
197      *
198      * @return a copy
199      */
200     public JexlOptions copy() {
201         return new JexlOptions().set(this);
202     }
203 
204     /**
205      * Gets the optional set of imported packages.
206      *
207      * @return the set of imports, may be empty, not null
208      */
209     public Collection<String> getImports() {
210         return imports;
211     }
212 
213     /**
214      * The MathContext instance used for +,-,/,*,% operations on big decimals.
215      *
216      * @return the math context
217      */
218     public MathContext getMathContext() {
219         return mathContext;
220     }
221 
222     /**
223      * The BigDecimal scale used for comparison and coercion operations.
224      *
225      * @return the scale
226      */
227     public int getMathScale() {
228         return mathScale;
229     }
230 
231     /**
232      * Gets the optional map of namespaces.
233      *
234      * @return the map of namespaces, may be empty, not null
235      */
236     public Map<String, Object> getNamespaces() {
237         return namespaces;
238     }
239 
240     /**
241      * Checks whether evaluation will attempt resolving antish variable names.
242      *
243      * @return true if antish variables are solved, false otherwise
244      */
245     public boolean isAntish() {
246         return isSet(ANTISH, flags);
247     }
248 
249     /**
250      * Gets whether logical expressions (&quot;&quot; , ||) coerce their result to boolean; if set,
251      * an expression like (3 &quot;&quot; 4 &quot;&quot; 5) will evaluate to true. If not, it will evaluate to 5.
252      * To preserve strict compatibility with 3.4, set the flag to true.
253      *
254      * @return true if short-circuit logicals coerce their result to boolean, false otherwise
255      * @since 3.5.0
256      */
257     public boolean isBooleanLogical() {
258         return isSet(BOOLEAN_LOGICAL, flags);
259     }
260 
261     /**
262      * Checks whether evaluation will throw JexlException.Cancel (true) or
263      * return null (false) if interrupted.
264      *
265      * @return true when cancellable, false otherwise
266      */
267     public boolean isCancellable() {
268         return isSet(CANCELLABLE, flags);
269     }
270 
271     /**
272      * Are lambda captured-variables const?
273      *
274      * @return true if lambda captured-variables are const, false otherwise
275      */
276     public boolean isConstCapture() {
277         return isSet(CONST_CAPTURE, flags);
278     }
279 
280     /**
281      * Checks whether runtime variable scope is lexical.
282      * <p>If true, lexical scope applies to local variables and parameters.
283      * Redefining a variable in the same lexical unit will generate errors.
284      *
285      * @return true if scope is lexical, false otherwise
286      */
287     public boolean isLexical() {
288         return isSet(LEXICAL, flags);
289     }
290 
291     /**
292      * Checks whether local variables shade global ones.
293      * <p>After a symbol is defined as local, dereferencing it outside its
294      * scope will trigger an error instead of seeking a global variable of the
295      * same name. To further reduce potential naming ambiguity errors,
296      * global variables (ie non-local) must be declared to be assigned {@link JexlContext#has(String)}
297      * when this flag is on; attempting to set an undeclared global variables will
298      * raise an error.
299      *
300      * @return true if lexical shading is applied, false otherwise
301      */
302     public boolean isLexicalShade() {
303         return isSet(SHADE, flags);
304     }
305 
306     /**
307      * Checks whether the engine considers null in navigation expression as
308      * errors during evaluation.
309      *
310      * @return true if safe, false otherwise
311      */
312     public boolean isSafe() {
313         return isSet(SAFE, flags);
314     }
315 
316     /**
317      * Gets sharing state.
318      *
319      * @return false if a copy of these options is used during execution,
320      * true if those can potentially be modified
321      */
322     public boolean isSharedInstance() {
323         return isSet(SHARED, flags);
324     }
325 
326     /**
327      * Checks whether the engine will throw a {@link JexlException} when an
328      * error is encountered during evaluation.
329      *
330      * @return true if silent, false otherwise
331      */
332     public boolean isSilent() {
333         return isSet(SILENT, flags);
334     }
335 
336     /**
337      * Checks whether the engine considers unknown variables, methods and
338      * constructors as errors during evaluation.
339      *
340      * @return true if strict, false otherwise
341      */
342     public boolean isStrict() {
343         return isSet(STRICT, flags);
344     }
345 
346     /**
347      * Checks whether the arithmetic triggers errors during evaluation when null
348      * is used as an operand.
349      *
350      * @return true if strict, false otherwise
351      */
352     public boolean isStrictArithmetic() {
353         return strictArithmetic;
354     }
355 
356     /**
357      * Gets the strict-interpolation flag of this options instance.
358      *
359      * @return true if interpolation strings always return string, false otherwise
360      */
361     public boolean isStrictInterpolation() {
362         return isSet(STRICT_INTERPOLATION, flags);
363     }
364 
365     /**
366      * Sets options from engine.
367      *
368      * @param jexl the engine
369      * @return {@code this} instance
370      */
371     public JexlOptions set(final JexlEngine jexl) {
372         if (jexl instanceof Engine) {
373             return ((Engine) jexl).optionsSet(this);
374         }
375         throw new UnsupportedOperationException("JexlEngine is not an Engine instance: " + jexl.getClass().getName());
376     }
377 
378     /**
379      * Sets options from options.
380      *
381      * @param src the options
382      * @return {@code this} instance
383      */
384     public JexlOptions set(final JexlOptions src) {
385         mathContext = src.mathContext;
386         mathScale = src.mathScale;
387         strictArithmetic = src.strictArithmetic;
388         flags = src.flags;
389         namespaces = src.namespaces;
390         imports = src.imports;
391         return this;
392     }
393 
394     /**
395      * Sets whether the engine will attempt solving antish variable names from
396      * context.
397      *
398      * @param flag true if antish variables are solved, false otherwise
399      */
400     public void setAntish(final boolean flag) {
401         flags = set(ANTISH, flags, flag);
402     }
403 
404     /**
405      * Sets whether logical expressions (&quot;&quot; , ||) coerce their result to boolean.
406      *
407      * @param flag true or false
408      */
409     public void setBooleanLogical(final boolean flag) {
410         flags = set(BOOLEAN_LOGICAL, flags, flag);
411     }
412 
413     /**
414      * Sets whether the engine will throw JexlException.Cancel (true) or return
415      * null (false) when interrupted during evaluation.
416      *
417      * @param flag true when cancellable, false otherwise
418      */
419     public void setCancellable(final boolean flag) {
420         flags = set(CANCELLABLE, flags, flag);
421     }
422 
423     /**
424      * Sets whether lambda captured-variables are const or not.
425      * <p>
426      * When disabled, lambda-captured variables are implicitly converted to read-write local variable (let),
427      * when enabled, those are implicitly converted to read-only local variables (const).
428      * </p>
429      *
430      * @param flag true to enable, false to disable
431      */
432     public void setConstCapture(final boolean flag) {
433         flags = set(CONST_CAPTURE, flags, flag);
434     }
435 
436     /**
437      * Sets this option flags using the +/- syntax.
438      *
439      * @param opts the option flags
440      */
441     public void setFlags(final String... opts) {
442         flags = parseFlags(flags, opts);
443     }
444 
445     /**
446      * Sets the optional set of imports.
447      *
448      * @param imports the imported packages
449      */
450     public void setImports(final Collection<String> imports) {
451         this.imports = imports == null || imports.isEmpty()? Collections.emptySet() : imports;
452     }
453 
454     /**
455      * Sets whether the engine uses a strict block lexical scope during
456      * evaluation.
457      *
458      * @param flag true if lexical scope is used, false otherwise
459      */
460     public void setLexical(final boolean flag) {
461         flags = set(LEXICAL, flags, flag);
462     }
463 
464     /**
465      * Sets whether the engine strictly shades global variables.
466      * Local symbols shade globals after definition and creating global
467      * variables is prohibited during evaluation.
468      * If setting to lexical shade, lexical scope is also set.
469      *
470      * @param flag true if creation is allowed, false otherwise
471      */
472     public void setLexicalShade(final boolean flag) {
473         flags = set(SHADE, flags, flag);
474         if (flag) {
475             flags = set(LEXICAL, flags, true);
476         }
477     }
478 
479     /**
480      * Sets the arithmetic math context.
481      *
482      * @param mcontext the context
483      */
484     public void setMathContext(final MathContext mcontext) {
485         this.mathContext = mcontext;
486     }
487 
488     /**
489      * Sets the arithmetic math scale.
490      *
491      * @param mscale the scale
492      */
493     public void setMathScale(final int mscale) {
494         this.mathScale = mscale;
495     }
496 
497     /**
498      * Sets the optional map of namespaces.
499      *
500      * @param ns a namespaces map
501      */
502     public void setNamespaces(final Map<String, Object> ns) {
503         this.namespaces = ns == null || ns.isEmpty()? Collections.emptyMap() : ns;
504     }
505 
506     /**
507      * Sets whether the engine considers null in navigation expression as null or as errors
508      * during evaluation.
509      * <p>If safe, encountering null during a navigation expression - dereferencing a method or a field through a null
510      * object or property - will <em>not</em> be considered an error but evaluated as <em>null</em>. It is recommended
511      * to use <em>setSafe(false)</em> as an explicit default.</p>
512      *
513      * @param flag true if safe, false otherwise
514      */
515     public void setSafe(final boolean flag) {
516         flags = set(SAFE, flags, flag);
517     }
518 
519     /**
520      * Sets wether these options are immutable at runtime.
521      * <p>Expert mode; allows instance handled through context to be shared
522      * instead of copied.
523      *
524      * @param flag true if shared, false if not
525      */
526     public void setSharedInstance(final boolean flag) {
527         flags = set(SHARED, flags, flag);
528     }
529 
530     /**
531      * Sets whether the engine will throw a {@link JexlException} when an error
532      * is encountered during evaluation.
533      *
534      * @param flag true if silent, false otherwise
535      */
536     public void setSilent(final boolean flag) {
537         flags = set(SILENT, flags, flag);
538     }
539 
540     /**
541      * Sets whether the engine considers unknown variables, methods and
542      * constructors as errors during evaluation.
543      *
544      * @param flag true if strict, false otherwise
545      */
546     public void setStrict(final boolean flag) {
547         flags = set(STRICT, flags, flag);
548     }
549 
550     /**
551      * Sets the strict arithmetic flag.
552      *
553      * @param stricta true or false
554      */
555     public void setStrictArithmetic(final boolean stricta) {
556         this.strictArithmetic = stricta;
557     }
558 
559     /**
560      * Sets the strict interpolation flag.
561      * <p>When strict, interpolation strings composed only of an expression (ie `${...}`) are evaluated
562      * as strings; when not strict, integer results are left untouched.</p>
563      * This can affect the results of expressions like <code>map.`${key}`</code> when key is
564      * an integer (or a string); it is almost always possible to use <code>map[key]</code> to ensure
565      * that the key type is not altered.
566      *
567      * @param strict true or false
568      */
569     public void setStrictInterpolation(final boolean strict) {
570         flags = set(STRICT_INTERPOLATION, flags, strict);
571     }
572 
573     @Override public String toString() {
574         final StringBuilder strb = new StringBuilder();
575         for(int i = 0; i < NAMES.length; ++i) {
576             if (i > 0) {
577                 strb.append(' ');
578             }
579             strb.append((flags & 1 << i) != 0? '+':'-');
580             strb.append(NAMES[i]);
581         }
582         return strb.toString();
583     }
584 
585 }