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  package org.apache.commons.jexl3;
18  
19  import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
20  import static org.junit.jupiter.api.Assertions.assertEquals;
21  import static org.junit.jupiter.api.Assertions.assertFalse;
22  import static org.junit.jupiter.api.Assertions.assertInstanceOf;
23  import static org.junit.jupiter.api.Assertions.assertNotNull;
24  import static org.junit.jupiter.api.Assertions.assertNull;
25  import static org.junit.jupiter.api.Assertions.assertSame;
26  import static org.junit.jupiter.api.Assertions.assertThrows;
27  import static org.junit.jupiter.api.Assertions.assertTrue;
28  
29  import java.io.PrintWriter;
30  import java.io.StringReader;
31  import java.io.StringWriter;
32  import java.io.Writer;
33  import java.lang.reflect.Method;
34  import java.util.ArrayList;
35  import java.util.Arrays;
36  import java.util.Collections;
37  import java.util.HashMap;
38  import java.util.List;
39  import java.util.Map;
40  import java.util.Set;
41  
42  import org.apache.commons.jexl3.internal.Debugger;
43  import org.apache.commons.jexl3.internal.TemplateDebugger;
44  import org.apache.commons.jexl3.internal.TemplateInterpreter;
45  import org.apache.commons.jexl3.internal.introspection.Permissions;
46  import org.apache.commons.jexl3.internal.introspection.Uberspect;
47  import org.apache.commons.jexl3.introspection.JexlPermissions;
48  import org.apache.commons.logging.Log;
49  import org.apache.commons.logging.LogFactory;
50  import org.junit.jupiter.api.AfterEach;
51  import org.junit.jupiter.api.BeforeEach;
52  import org.junit.jupiter.api.Test;
53  import org.junit.jupiter.params.ParameterizedTest;
54  import org.junit.jupiter.params.provider.MethodSource;
55  
56  /**
57   * Test cases for the UnifiedEL.
58   */
59  @SuppressWarnings({"UnnecessaryBoxing", "AssertEqualsBetweenInconvertibleTypes"})
60  class JXLTTest extends JexlTestCase {
61  
62      public static class Context311 extends MapContext
63        implements JexlContext.OptionsHandle, JexlContext.ThreadLocal {
64          private JexlOptions options;
65  
66          public Executor311 exec(final String name) {
67              return new Executor311(name);
68          }
69  
70          @Override
71          public JexlOptions getEngineOptions() {
72              return options;
73          }
74  
75          JexlOptions newOptions() {
76              options = new JexlOptions();
77              return options;
78          }
79  
80          public void setOptions(final JexlOptions o) {
81              options = o;
82          }
83      }
84      public static class Executor311 {
85          private final String name;
86  
87          public Executor311(final String name) {
88              this.name = name;
89          }
90          // Injects name as first arg of any called script
91          public Object execute(final JexlScript script, final Object ...args) {
92              Object[] actuals;
93              if (args != null && args.length > 0) {
94                  actuals = new Object[args.length + 1] ;
95                  System.arraycopy(args, 0, actuals, 1, args.length);
96                  actuals[0] = name;
97              } else {
98                  actuals = new Object[]{name};
99              }
100             return script.execute(JexlEngine.getThreadContext(), actuals);
101         }
102     }
103     public static class Froboz {
104         int value;
105 
106         public Froboz(final int v) {
107             value = v;
108         }
109 
110         public int getValue() {
111             return value;
112         }
113 
114         public int plus10() {
115             final int i = value;
116             value += 10;
117             return i;
118         }
119 
120         public void setValue(final int v) {
121             value = v;
122         }
123     }
124     public static class FrobozWriter extends PrintWriter {
125         public FrobozWriter(final Writer w) {
126             super(w);
127         }
128 
129         public void print(final Froboz froboz) {
130             super.print("froboz{");
131             super.print(froboz.value);
132             super.print("}");
133         }
134 
135         @Override
136         public String toString() {
137             return out.toString();
138         }
139     }
140     private static final Log LOGGER = LogFactory.getLog(JxltEngine.class);
141     private static final Permissions NOJEXL3 = new Permissions() {
142         @Override public boolean allow(final Class<?> clazz) {
143             final String cname = clazz.getName();
144             return !cname.contains("jexl3") || cname.contains("311");
145         }
146     };
147 
148    public static List<JexlBuilder> engines() {
149        final JexlFeatures f = new JexlFeatures();
150        f.lexical(true).lexicalShade(true);
151        // deny the test class itself but explicitly allow the nested fixture classes: a whole-class
152        // denial now propagates to nested classes (the -X{} keying gap is closed), so the fixtures
153        // the scripts touch must be positively declared inside the (container) block
154         final JexlPermissions permissions = JexlPermissions.RESTRICTED.compose(
155             "org.apache.commons.jexl3 +{ -JXLTTest {"
156                 + " +Context311 {} +Executor311 {} +Froboz {} +FrobozWriter {} +Arithmetic425 {}"
157                 + " } }");
158       return Arrays.asList(
159               new JexlBuilder().permissions(permissions).silent(false).lexical(true).lexicalShade(true).cache(128).strict(true),
160               new JexlBuilder().permissions(permissions).features(f).silent(false).cache(128).strict(true),
161               new JexlBuilder().permissions(permissions).silent(false).cache(128).strict(true));
162    }
163 
164    private static String refactor(final TemplateDebugger td, final JxltEngine.Template ts) {
165     final boolean dbg = td.debug(ts);
166     if (dbg) {
167         return td.toString();
168     }
169     return "";
170 }
171 
172     private final MapContext vars = new MapContext();
173 
174     private JexlEvalContext context;
175 
176     private JexlBuilder BUILDER;
177 
178     private JexlEngine ENGINE;
179 
180     private JxltEngine JXLT;
181 
182     public JXLTTest() {
183         super("JXLTTest");
184     }
185 
186     boolean contains(final Set<List<String>> set, final List<String> list) {
187         for (final List<String> sl : set) {
188             if (sl.equals(list)) {
189                 return true;
190             }
191         }
192         return false;
193     }
194 
195     /** Extract the source from a toString-ed expression. */
196     private String getSource(final String tostring) {
197         final int len = tostring.length();
198         int sc = tostring.lastIndexOf(" /*= ");
199         if (sc >= 0) {
200             sc += " /*= ".length();
201         }
202         final int ec = tostring.lastIndexOf(" */");
203         if (sc >= 0 && ec >= 0 && ec > sc && ec < len) {
204             return tostring.substring(sc, ec);
205         }
206         return tostring;
207 
208     }
209 
210     private void init(final JexlBuilder builder) {
211         BUILDER = builder;
212         ENGINE = BUILDER.create();
213         JXLT = ENGINE.createJxltEngine();
214     }
215 
216     private boolean isLexicalShade() {
217         JexlOptions options = context.getEngineOptions();
218         if (options.isLexicalShade()) {
219             return true;
220         }
221         options = new JexlOptions().set(ENGINE);
222         return options.isLexicalShade();
223     }
224 
225     @BeforeEach
226     @Override
227     public void setUp() {
228         // ensure jul logging is only error
229         java.util.logging.Logger.getLogger(org.apache.commons.jexl3.JexlEngine.class.getName()).setLevel(java.util.logging.Level.SEVERE);
230         context = new JexlEvalContext(vars);
231     }
232 
233     @AfterEach
234     @Override
235     public void tearDown() throws Exception {
236         debuggerCheck(ENGINE);
237         super.tearDown();
238     }
239 
240     @ParameterizedTest
241     @MethodSource("engines")
242     void test311a(final JexlBuilder builder) {
243         init(builder);
244         final JexlContext ctx = null;
245         // @formatter:off
246         final String rpt
247                 = "$$((a)->{\n"
248                 + "<p>Universe ${a}</p>\n"
249                 + "$$})(42)";
250         // @formatter:on
251         final JxltEngine.Template t = JXLT.createTemplate("$$", new StringReader(rpt));
252         final StringWriter strw = new StringWriter();
253         t.evaluate(ctx, strw);
254         final String output = strw.toString();
255         assertEquals("<p>Universe 42</p>\n", output);
256     }
257 
258     @ParameterizedTest
259     @MethodSource("engines")
260     void test311b(final JexlBuilder builder) {
261         init(builder);
262         final JexlContext ctx311 = new Context311();
263         // @formatter:off
264         final String rpt
265                 = "$$ exec('42').execute(()->{\n"
266                 + "<p>Universe 42</p>\n"
267                 + "$$})";
268         // @formatter:on
269         final JxltEngine.Template t = JXLT.createTemplate("$$", new StringReader(rpt));
270         final StringWriter strw = new StringWriter();
271         t.evaluate(ctx311, strw, 42);
272         final String output = strw.toString();
273         assertEquals("<p>Universe 42</p>\n", output);
274     }
275 
276     @ParameterizedTest
277     @MethodSource("engines")
278     void test311c(final JexlBuilder builder) {
279         init(builder);
280         final Context311 ctx311 = new Context311();
281         ctx311.newOptions().setLexical(true);
282         // @formatter:off
283         final String rpt
284                 = "$$ exec('42').execute((a)->{"
285                 + "\n<p>Universe ${a}</p>"
286                 + "\n$$})";
287         // @formatter:on
288         final JxltEngine.Template t = JXLT.createTemplate("$$", new StringReader(rpt));
289         final StringWriter strw = new StringWriter();
290         t.evaluate(ctx311, strw, 42);
291         final String output = strw.toString();
292         assertEquals("<p>Universe 42</p>\n", output);
293     }
294 
295     @ParameterizedTest
296     @MethodSource("engines")
297     void test311d(final JexlBuilder builder) {
298         init(builder);
299         final Context311 ctx311 = new Context311();
300         ctx311.newOptions().setLexical(true);
301         // @formatter:off
302         final String rpt
303                 = "$$ exec('4').execute((a, b)->{"
304                 + "\n<p>Universe ${a}${b}</p>"
305                 + "\n$$}, '2')";
306         // @formatter:on
307         final JxltEngine.Template t = JXLT.createTemplate("$$", new StringReader(rpt));
308         final StringWriter strw = new StringWriter();
309         t.evaluate(ctx311, strw, 42);
310         final String output = strw.toString();
311         assertEquals("<p>Universe 42</p>\n", output);
312     }
313 
314     @ParameterizedTest
315     @MethodSource("engines")
316     void test311e(final JexlBuilder builder) {
317         init(builder);
318         final Context311 ctx311 = new Context311();
319         ctx311.newOptions().setLexical(true);
320         // @formatter:off
321         final String rpt
322                 = "exec('4').execute((a, b)->{"
323                 + " '<p>Universe ' + a + b + '</p>'"
324                 + "}, '2')";
325         // @formatter:on
326         final JexlScript script = JEXL.createScript(rpt);
327         final String output = script.execute(ctx311, 42).toString();
328         assertEquals("<p>Universe 42</p>", output);
329     }
330 
331     @ParameterizedTest
332     @MethodSource("engines")
333     void test311f(final JexlBuilder builder) {
334         init(builder);
335         final Context311 ctx311 = new Context311();
336         ctx311.newOptions().setLexical(true);
337         // @formatter:off
338         final String rpt
339                 = "exec('4').execute((a, b)->{"
340                 + " `<p>Universe ${a}${b}</p>`"
341                 + "}, '2')";
342         // @formatter:on
343         final JexlScript script = JEXL.createScript(rpt);
344         final String output = script.execute(ctx311, 42).toString();
345         assertEquals("<p>Universe 42</p>", output);
346     }
347 
348     @ParameterizedTest
349     @MethodSource("engines")
350     void test311g(final JexlBuilder builder) {
351         init(builder);
352         final Context311 ctx311 = new Context311();
353         ctx311.newOptions().setLexical(true);
354         // @formatter:off
355         final String rpt
356                 = "(a, b)->{"
357                 + " `<p>Universe ${a}${b}</p>`"
358                 + "}";
359         // @formatter:on
360         final JexlScript script = JEXL.createScript(rpt);
361         final String output = script.execute(ctx311, "4", "2").toString();
362         assertEquals("<p>Universe 42</p>", output);
363     }
364 
365     @ParameterizedTest
366     @MethodSource("engines")
367     void test311h(final JexlBuilder builder) {
368         init(builder);
369         final Context311 ctx311 = new Context311();
370         ctx311.newOptions().setLexical(true);
371         final String rpt= " `<p>Universe ${a}${b}</p>`";
372         final JexlScript script = JEXL.createScript(rpt, "a", "b");
373         final String output = script.execute(ctx311, "4", "2").toString();
374         assertEquals("<p>Universe 42</p>", output);
375     }
376 
377     @ParameterizedTest
378     @MethodSource("engines")
379     void test311i(final JexlBuilder builder) {
380         init(builder);
381         final JexlContext ctx311 = new Context311();
382         // @formatter:off
383         final String rpt
384                 = "$$var u = 'Universe'; exec('4').execute((a, b)->{"
385                 + "\n<p>${u} ${a}${b}</p>"
386                 + "\n$$}, '2')";
387         // @formatter:on
388         final JxltEngine.Template t = JXLT.createTemplate("$$", new StringReader(rpt));
389         final StringWriter strw = new StringWriter();
390         t.evaluate(ctx311, strw, 42);
391         final String output = strw.toString();
392         assertEquals("<p>Universe 42</p>\n", output);
393     }
394 
395     @ParameterizedTest
396     @MethodSource("engines")
397     void test315(final JexlBuilder builder) {
398         init(builder);
399         String s315;
400         StringWriter strw;
401         JxltEngine.Template t315;
402         String output;
403 
404         s315 = "<report/>$";
405         t315 = JXLT.createTemplate("$$", new StringReader(s315));
406         strw = new StringWriter();
407         t315.evaluate(context, strw);
408         output = strw.toString();
409         assertEquals(s315, output);
410 
411         s315 = "<foo/>#";
412         t315 = JXLT.createTemplate("$$", new StringReader(s315));
413          strw = new StringWriter();
414         t315.evaluate(context, strw);
415         output = strw.toString();
416         assertEquals(s315, output);
417 
418         s315 = "<bar/>\\";
419         t315 = JXLT.createTemplate("$$", new StringReader(s315));
420         strw = new StringWriter();
421         t315.evaluate(context, strw);
422         output = strw.toString();
423         assertEquals(s315, output);
424     }
425 
426     @ParameterizedTest
427     @MethodSource("engines")
428     void test42(final JexlBuilder builder) {
429         init(builder);
430         // @formatter:off
431         final String test42
432                 = "$$ for (var x : list) {\n"
433                 + "$$   if (x == 42) {\n"
434                 + "Life, the universe, and everything\n"
435                 + "$$   } else if (x > 42) {\n"
436                 + "The value ${x} is over fourty-two\n"
437                 + "$$   } else {\n"
438                 + "The value ${x} is under fourty-two\n"
439                 + "$$   }\n"
440                 + "$$ }\n";
441         // @formatter:on
442         final JxltEngine.Template t = JXLT.createTemplate("$$", new StringReader(test42), "list");
443         final StringWriter strw = new StringWriter();
444         final int[] list = {1, 3, 5, 42, 169};
445         t.evaluate(context, strw, list);
446         final String output = strw.toString();
447         // @formatter:off
448         final String out42
449                 = "The value 1 is under fourty-two\n"
450                 + "The value 3 is under fourty-two\n"
451                 + "The value 5 is under fourty-two\n"
452                 + "Life, the universe, and everything\n"
453                 + "The value 169 is over fourty-two\n";
454         // @formatter:on
455         assertEquals(out42, output);
456 
457         final String dstr = t.asString();
458         assertNotNull(dstr);
459 
460         final TemplateDebugger td = new TemplateDebugger();
461         final String refactored = refactor(td, t);
462         assertNotNull(refactored);
463         assertEquals(test42, refactored);
464     }
465 
466     @Test
467     void testParseIdentifier() {
468         assertNull(JexlArithmetic.parseIdentifier(null));
469         assertNull(JexlArithmetic.parseIdentifier(""));
470         assertNull(JexlArithmetic.parseIdentifier("za"));
471         assertNull(JexlArithmetic.parseIdentifier("a"));
472         assertNull(JexlArithmetic.parseIdentifier("00"));
473         assertNull(JexlArithmetic.parseIdentifier("01"));
474         assertNull(JexlArithmetic.parseIdentifier("001"));
475         assertNull(JexlArithmetic.parseIdentifier("12345678901"));
476 
477         assertEquals(0, JexlArithmetic.parseIdentifier("0"));
478         assertEquals(10, JexlArithmetic.parseIdentifier("10"));
479         assertEquals(100, JexlArithmetic.parseIdentifier("100"));
480         assertEquals(42, JexlArithmetic.parseIdentifier("42"));
481         assertEquals(42000, JexlArithmetic.parseIdentifier("42000"));
482 
483         assertEquals(42, JexlArithmetic.parseIdentifier(42));
484     }
485 
486     /**
487      * A remediation to strict interpolation that consistently attempts to coerce to integer
488      * index for lists and keys for maps. (see JEXL-425)
489      */
490     public static class Arithmetic425 extends JexlArithmetic {
491         public Arithmetic425(final boolean astrict) {
492             super(astrict);
493         }
494 
495         public Object propertyGet(final List list, final String property) {
496             final Integer id = JexlArithmetic.parseIdentifier(property);
497             return id == null? JexlEngine.TRY_FAILED : list.get(id);
498         }
499 
500         public Object propertySet(final List list, final String property, final Object value) {
501             final Integer id = JexlArithmetic.parseIdentifier(property);
502             if (id != null) {
503                 return list.set(id, value);
504             }
505             return JexlEngine.TRY_FAILED;
506         }
507 
508         public Object propertyGet(final Map list, final String property) {
509             // determine if keys are integers by performing a check on first one
510             if (list.keySet().stream().findFirst().orElse(null) instanceof Number) {
511                 final Integer id = JexlArithmetic.parseIdentifier(property);
512                 if (id != null) {
513                     return list.get(id);
514                 }
515             }
516             return JexlEngine.TRY_FAILED;
517         }
518 
519         public Object propertySet(final Map list, final String property, final Object value) {
520             // determine if keys are integers by performing a check on first one
521             if (list.keySet().stream().findFirst().orElse(null) instanceof Number) {
522                 final Integer id = JexlArithmetic.parseIdentifier(property);
523                 if (id != null) {
524                     list.put(id, value);
525                     return value;
526                 }
527             }
528             return JexlEngine.TRY_FAILED;
529         }
530     }
531 
532     @Test void test425a() {
533         final String S42 = "fourty-two";
534         final JexlBuilder builder = new JexlBuilder().strictInterpolation(true);
535         final JexlEngine jexl = builder.create();
536         JexlScript script;
537         Object result;
538 
539         script = jexl.createScript("let x = 42; let y = `${x}`; y");
540         result = script.execute(null);
541         assertInstanceOf(String.class, result);
542         assertEquals("42", result);
543 
544         final Map<Object,Object> map = Collections.singletonMap("42", S42);
545         script = jexl.createScript("let x = 42; map.`${x}`", "map");
546         result = script.execute(null, map);
547         assertEquals(S42, result);
548 
549         final List<String> list = Collections.singletonList(S42);
550         final JexlScript finalScript = script;
551         assertThrows(JexlException.Property.class, () -> finalScript.execute(null, list));
552         script = jexl.createScript("let x = 0; list[x]", "list");
553         assertEquals(S42, result);
554     }
555 
556     @Test void test425b() {
557         final String S42 = "fourty-two";
558         final JexlEngine jexl = new JexlBuilder().strictInterpolation(false).create();
559         run425bc(jexl, false);
560         run425bc(jexl, false);
561     }
562 
563     @Test void test425c() {
564         final JexlEngine jexl = new JexlBuilder()
565                 .cache(8)
566                 .arithmetic(new Arithmetic425(true))
567                 .strictInterpolation(true).create();
568         run425bc(jexl, true);
569         run425bc(jexl, true);
570     }
571 
572     void run425bc(final JexlEngine jexl, final boolean strictInterpolation) {
573         final String S42 = "fourty-two";
574         JexlScript script;
575         Object result;
576         script = jexl.createScript("let x = 42; let y = `${x}`; y");
577         result = script.execute(null);
578         assertEquals(!strictInterpolation? 42 : "42", result);
579         Map<Object,Object> map = Collections.singletonMap(0, S42);
580         script = jexl.createScript("let x = 0; map.`${x}`", "map");
581         result = script.execute(null, map);
582         assertEquals(S42, result);
583         List<String> list = Collections.singletonList(S42);
584         result = script.execute(null, list);
585         assertEquals(S42, result);
586 
587         map = new HashMap<>(map);
588         map.put(0, "nothing");
589         script = jexl.createScript("let x = 0; map.`${x}` = S42", "map", "S42");
590         result = script.execute(null, map, S42);
591         assertEquals(S42, result);
592         list = new ArrayList<>(list);
593         list.set(0, "nothing");
594         result = script.execute(null, map, S42);
595         assertEquals(S42, result);
596     }
597 
598     @ParameterizedTest
599     @MethodSource("engines")
600     void testAssign(final JexlBuilder builder) {
601         init(builder);
602         final Froboz froboz = new Froboz(32);
603         context.set("froboz", froboz);
604         final JxltEngine.Expression assign = JXLT.createExpression("${froboz.value = 42}");
605         final JxltEngine.Expression check = JXLT.createExpression("${froboz.value}");
606         Object o = assign.evaluate(context);
607         assertEquals(Integer.valueOf(42), o);
608         o = check.evaluate(context);
609         assertEquals(Integer.valueOf(42), o);
610     }
611 
612     @ParameterizedTest
613     @MethodSource("engines")
614     void testBadContextNested(final JexlBuilder builder) {
615         init(builder);
616         final JxltEngine.Expression expr = JXLT.createExpression("#{${hi}+'.world'}");
617         final JexlContext none = null;
618         final JxltEngine.Exception xjexl = assertThrows(JxltEngine.Exception.class, () -> expr.evaluate(none), "should be malformed");
619         LOGGER.debug(xjexl.getMessage());
620     }
621 
622     @ParameterizedTest
623     @MethodSource("engines")
624     void testCharAtBug(final JexlBuilder builder) {
625         init(builder);
626         context.set("foo", "abcdef");
627         final JexlOptions options = context.getEngineOptions();
628         JxltEngine.Expression expr = JXLT.createExpression("${foo.substring(2,4)/*comment*/}");
629         Object o = expr.evaluate(context);
630         assertEquals("cd", o);
631         context.set("bar", "foo");
632         try {
633             options.setSilent(true);
634             expr = JXLT.createExpression("#{${bar}+'.charAt(-2)'}");
635             expr = expr.prepare(context);
636             o = expr.evaluate(context);
637             assertNull(o);
638         } finally {
639             options.setSilent(false);
640         }
641     }
642 
643     @ParameterizedTest
644     @MethodSource("engines")
645     void testCommentedTemplate0(final JexlBuilder builder) {
646         init(builder);
647         final JexlContext ctxt = new MapContext();
648         final JexlEngine jexl = new JexlBuilder().create();
649         final JxltEngine jxlt = jexl.createJxltEngine();
650         JxltEngine.Template tmplt;
651         // @formatter:off
652         final String src = "$$/*\n"
653                 + "Hello\n"
654                 + "$$*/";
655         tmplt = jxlt.createTemplate(src);
656         assertNotNull(tmplt);
657         final Writer strw = new StringWriter();
658         tmplt.evaluate(ctxt, strw);
659         assertTrue(strw.toString().isEmpty());
660     }
661 
662     @ParameterizedTest
663     @MethodSource("engines")
664     void testCommentedTemplate1(final JexlBuilder builder) {
665         init(builder);
666         final JexlContext ctxt = new MapContext();
667         final JexlEngine jexl = new JexlBuilder().create();
668         final JxltEngine jxlt = jexl.createJxltEngine();
669         JxltEngine.Template tmplt;
670         // @formatter:off
671         final String src = "$$/*\n"
672                 + "one\n"
673                 + "$$*/\n"
674                 + "42\n"
675                 + "$$/*\n"
676                 + "three\n"
677                 + "$$*/\n";
678         // @formatter:on
679         tmplt = jxlt.createTemplate(src);
680         assertNotNull(tmplt);
681         final Writer strw = new StringWriter();
682         tmplt.evaluate(ctxt, strw);
683         assertEquals("42\n", strw.toString());
684     }
685 
686     @ParameterizedTest
687     @MethodSource("engines")
688     void testComposite(final JexlBuilder builder) {
689         init(builder);
690         final String source = "Dear ${p} ${name};";
691         final JxltEngine.Expression expr = JXLT.createExpression(source);
692         context.set("p", "Mr");
693         context.set("name", "Doe");
694         assertTrue(expr.isImmediate(), "expression should be immediate");
695         Object o = expr.evaluate(context);
696         assertEquals("Dear Mr Doe;", o);
697         context.set("p", "Ms");
698         context.set("name", "Jones");
699         o = expr.evaluate(context);
700         assertEquals("Dear Ms Jones;", o);
701         assertEquals(source, getSource(expr.toString()));
702     }
703 
704     @ParameterizedTest
705     @MethodSource("engines")
706     void testConstant0(final JexlBuilder builder) {
707         init(builder);
708         final JexlContext none = null;
709         final String source = "Hello World!";
710         final JxltEngine.Expression expr = JXLT.createExpression(source);
711         assertSame(expr.prepare(none), expr, "prepare should return same expression");
712         final Object o = expr.evaluate(none);
713         assertTrue(expr.isImmediate(), "expression should be immediate");
714         assertEquals("Hello World!", o);
715 
716         assertEquals(source, getSource(expr.toString()));
717     }
718 
719     @ParameterizedTest
720     @MethodSource("engines")
721     void testConstant2(final JexlBuilder builder) {
722         init(builder);
723         final JexlContext none = null;
724         final String source = "${size({'map':123,'map2':456})}";
725         final JxltEngine.Expression expr = JXLT.createExpression(source);
726         //assertTrue("prepare should return same expression", expr.prepare(none) == expr);
727         final Object o = expr.evaluate(none);
728         assertTrue(expr.isImmediate(), "expression should be immediate");
729         assertEquals(2, o);
730 
731         assertEquals(source, getSource(expr.toString()));
732     }
733 
734     @ParameterizedTest
735     @MethodSource("engines")
736     void testConstant3(final JexlBuilder builder) {
737         init(builder);
738         final JexlContext none = null;
739         final String source = "#{size({'map':123,'map2':456})}";
740         final JxltEngine.Expression expr = JXLT.createExpression(source);
741         //assertTrue("prepare should return same expression", expr.prepare(none) == expr);
742         final Object o = expr.evaluate(none);
743         assertTrue(expr.isDeferred(), "expression should be deferred");
744         assertEquals(2, o);
745 
746         assertEquals(source, getSource(expr.toString()));
747     }
748 
749     @ParameterizedTest
750     @MethodSource("engines")
751     void testConstant4(final JexlBuilder builder) {
752         init(builder);
753         final JexlContext none = null;
754         final String source = "#{ ${size({'1':2,'2': 3})} }";
755         final JxltEngine.Expression expr = JXLT.createExpression(source);
756         //assertTrue("prepare should return same expression", expr.prepare(none) == expr);
757         final Object o = expr.evaluate(none);
758         assertTrue(expr.isDeferred(), "expression should be deferred");
759         assertEquals(2, o);
760 
761         assertEquals(source, getSource(expr.toString()));
762     }
763 
764     @ParameterizedTest
765     @MethodSource("engines")
766     void testConstantTemplate(final JexlBuilder builder) {
767         init(builder);
768         // @formatter:off
769         final String src = "<script>\n" +
770                 "      function test(src){\n" +
771                 "        var res = src.replace(/\\n\\t\\s/g, '\\n');\n" +
772                 "      }\n" +
773                 "      test();\n" +
774                 "    </script>";
775         // @formatter:on
776         final JexlContext ctxt = new MapContext();
777         final JexlEngine jexl = new JexlBuilder().create();
778         final JxltEngine jxlt = jexl.createJxltEngine();
779         JxltEngine.Template tmplt;
780         tmplt = jxlt.createTemplate(src);
781         assertNotNull(tmplt);
782         final Writer strw = new StringWriter();
783         tmplt.evaluate(ctxt, strw);
784         final String result = strw.toString();
785         assertEquals(src, result);
786     }
787 
788     @ParameterizedTest
789     @MethodSource("engines")
790     void testDbgEscapes(final JexlBuilder builder) {
791         init(builder);
792         // @formatter:off
793         final String[] srcs = {
794                 "jexl:print('hello\\'\\nworld')",
795                 "'hello\\tworld'",
796                 "'hello\\nworld'",
797                 "'hello\\fworld'",
798                 "'hello\\rworld'"
799         };
800         // @formatter:on
801         for(final String src : srcs) {
802             final JexlScript script = ENGINE.createScript(src);
803             final Debugger dbg = new Debugger();
804             dbg.debug(script);
805             final String msrc = dbg.toString();
806             assertEquals(src, msrc);
807         }
808     }
809 
810     @ParameterizedTest
811     @MethodSource("engines")
812     void testDeferred(final JexlBuilder builder) {
813         init(builder);
814         final JexlContext none = null;
815         final String source = "#{'world'}";
816         final JxltEngine.Expression expr = JXLT.createExpression(source);
817         assertTrue(expr.isDeferred(), "expression should be deferred");
818         final String as = expr.prepare(none).asString();
819         assertEquals("${'world'}", as, "prepare should return immediate version");
820         final Object o = expr.evaluate(none);
821         assertEquals("world", o);
822 
823         assertEquals(source, getSource(expr.toString()));
824     }
825 
826     @ParameterizedTest
827     @MethodSource("engines")
828     void testEscape(final JexlBuilder builder) {
829         init(builder);
830         final JexlContext none = null;
831         JxltEngine.Expression expr;
832         Object o;
833         // $ and # are escapable in TemplateEngine
834         expr = JXLT.createExpression("\\#{'world'}");
835         o = expr.evaluate(none);
836         assertEquals("#{'world'}", o);
837         expr = JXLT.createExpression("\\${'world'}");
838         o = expr.evaluate(none);
839         assertEquals("${'world'}", o);
840     }
841 
842     @ParameterizedTest
843     @MethodSource("engines")
844     void testEscapeString(final JexlBuilder builder) {
845         init(builder);
846         final JxltEngine.Expression expr = JXLT.createExpression("\\\"${'world\\'s finest'}\\\"");
847         final JexlContext none = null;
848         final Object o = expr.evaluate(none);
849         assertEquals("\"world's finest\"", o);
850     }
851 
852     @ParameterizedTest
853     @MethodSource("engines")
854     void testImmediate(final JexlBuilder builder) {
855         init(builder);
856         final JexlContext none = null;
857         final String source = "${'Hello ' + 'World!'}";
858         final JxltEngine.Expression expr = JXLT.createExpression(source);
859         final JxltEngine.Expression prepared = expr.prepare(none);
860         assertEquals("Hello World!", prepared.asString(), "prepare should return same expression");
861         final Object o = expr.evaluate(none);
862         assertTrue(expr.isImmediate(), "expression should be immediate");
863         assertEquals("Hello World!", o);
864 
865         assertEquals(source, getSource(expr.toString()));
866     }
867 
868     @ParameterizedTest
869     @MethodSource("engines")
870     void testImmediateTemplate(final JexlBuilder builder) {
871         init(builder);
872         context.set("tables", new String[]{"table1", "table2"});
873         context.set("w" ,"x=1");
874         // @formatter:off
875         final JxltEngine.Template t = JXLT.createTemplate("$$", new StringReader(
876              "select * from \n"+
877              "$$var comma = false; \n"+
878              "$$for(var c : tables) { \n"+
879              "$$  if (comma) $jexl.write(','); else comma = true;\n"+
880              "${c}"+
881              "\n$$}\n"+
882              "where ${w}\n"
883         ));
884         // @formatter:on
885         final StringWriter strw = new StringWriter();
886         //vars.clear();
887         t.evaluate(context, strw);
888         final String output = strw.toString();
889         assertTrue(output.contains("table1") && output.contains("table2"));
890     }
891     @ParameterizedTest
892     @MethodSource("engines")
893     void testInheritedDebugger(final JexlBuilder builder) {
894         init(builder);
895         final String src = "if ($A) { $B + 1; } else { $C - 2 }";
896         final JexlEngine jexl = JXLT.getEngine();
897         final JexlScript script = jexl.createScript(src);
898 
899         final Debugger sd = new Debugger();
900         final String rscript = sd.debug(script)? sd.toString() : null;
901         assertNotNull(rscript);
902 
903         final TemplateDebugger td = new TemplateDebugger();
904         final String refactored = td.debug(script)? td.toString() : null;
905         assertNotNull(refactored);
906         assertEquals(refactored, rscript);
907     }
908 
909     @ParameterizedTest
910     @MethodSource("engines")
911     void testInterpolation(final JexlBuilder builder) {
912         init(builder);
913         final String expr =  "`Hello \n${user}`";
914         final JexlScript script = ENGINE.createScript(expr);
915         context.set("user", "Dimitri");
916         Object value = script.execute(context);
917         assertEquals("Hello \nDimitri", value, expr);
918         context.set("user", "Rahul");
919         value = script.execute(context);
920         assertEquals("Hello \nRahul", value, expr);
921     }
922 
923     @ParameterizedTest
924     @MethodSource("engines")
925     void testInterpolationGlobal(final JexlBuilder builder) {
926         init(builder);
927         if (isLexicalShade()) {
928             context.set("user", null);
929         }
930         final String expr =  "user='Dimitri'; `Hello \n${user}`";
931         final Object value = ENGINE.createScript(expr).execute(context);
932         assertEquals("Hello \nDimitri", value, expr);
933     }
934 
935     @ParameterizedTest
936     @MethodSource("engines")
937     void testInterpolationLocal(final JexlBuilder builder) {
938         init(builder);
939         final String expr =  "var user='Henrib'; `Hello \n${user}`";
940         final Object value = ENGINE.createScript(expr).execute(context);
941         assertEquals("Hello \nHenrib", value, expr);
942     }
943 
944     @ParameterizedTest
945     @MethodSource("engines")
946     void testInterpolationLvsG(final JexlBuilder builder) {
947         init(builder);
948         if (isLexicalShade()) {
949             context.set("user", null);
950         }
951         final String expr =  "user='Dimitri'; var user='Henrib'; `H\\\"ello \n${user}`";
952         final Object value = ENGINE.createScript(expr).execute(context);
953         assertEquals("H\"ello \nHenrib", value, expr);
954     }
955 
956     @ParameterizedTest
957     @MethodSource("engines")
958     void testInterpolationLvsG2(final JexlBuilder builder) {
959         init(builder);
960         if (isLexicalShade()) {
961             context.set("user", null);
962         }
963         final String expr =  "user='Dimitri'; var user='Henrib'; `H\\`ello \n${user}`";
964         final Object value = ENGINE.createScript(expr).execute(context);
965         assertEquals("H`ello \nHenrib", value, expr);
966     }
967 
968     @ParameterizedTest
969     @MethodSource("engines")
970     void testInterpolationParameter(final JexlBuilder builder) {
971         init(builder);
972         final String expr =  "(user)->{`Hello \n${user}`}";
973         final JexlScript script = ENGINE.createScript(expr);
974         Object value = script.execute(context, "Henrib");
975         assertEquals("Hello \nHenrib", value, expr);
976         value = ENGINE.createScript(expr).execute(context, "Dimitri");
977         assertEquals("Hello \nDimitri", value, expr);
978     }
979 
980     @ParameterizedTest
981     @MethodSource("engines")
982     void testLexicalTemplate(final JexlBuilder builder) {
983         init(builder);
984         final JexlOptions opts = new JexlOptions();
985         final JexlContext ctxt = new PragmaticContext(opts);
986         opts.setCancellable(false);
987         opts.setStrict(false);
988         opts.setSafe(true);
989         opts.setLexical(false);
990         opts.setLexicalShade(false);
991         // @formatter:off
992         final String src0 = "${$options.strict?'+':'-'}strict"
993                 + " ${$options.cancellable?'+':'-'}cancellable"
994                 + " ${$options.lexical?'+':'-'}lexical"
995                 + " ${$options.lexicalShade?'+':'-'}lexicalShade"
996                 + " ${$options.safe?'+':'-'}safe";
997         // @formatter:on
998         final JxltEngine.Template tmplt0 = JXLT.createTemplate("$$", new StringReader(src0));
999         final Writer strw0 = new StringWriter();
1000         tmplt0.evaluate(ctxt, strw0);
1001         final String output0 = strw0.toString();
1002         final JexlFeatures features = BUILDER.features();
1003         if (features != null && features.isLexical() && features.isLexicalShade()) {
1004             assertEquals("-strict -cancellable +lexical +lexicalShade +safe", output0);
1005         } else {
1006             assertEquals("-strict -cancellable -lexical -lexicalShade +safe", output0);
1007         }
1008 
1009         final String src = "$$ #pragma script.mode pro50\n" + src0;
1010 
1011         final JxltEngine.Template tmplt = JXLT.createTemplate("$$", new StringReader(src));
1012         final Writer strw = new StringWriter();
1013         tmplt.evaluate(ctxt, strw);
1014         final String output = strw.toString();
1015         assertEquals("+strict +cancellable +lexical +lexicalShade -safe", output);
1016     }
1017 
1018     @ParameterizedTest
1019     @MethodSource("engines")
1020     void testMalformed(final JexlBuilder builder) {
1021         init(builder);
1022         final JxltEngine.Exception xjexl = assertThrows(JxltEngine.Exception.class, () -> JXLT.createExpression("${'world'"), "should be malformed");
1023         LOGGER.debug(xjexl.getMessage());
1024     }
1025 
1026     @ParameterizedTest
1027     @MethodSource("engines")
1028     void testMalformedNested(final JexlBuilder builder) {
1029         init(builder);
1030         final JxltEngine.Exception xjexl = assertThrows(JxltEngine.Exception.class, () -> JXLT.createExpression("#{${hi} world}"), "should be malformed");
1031         LOGGER.debug(xjexl.getMessage());
1032     }
1033 
1034     @ParameterizedTest
1035     @MethodSource("engines")
1036     void testMalformedNested2(final JexlBuilder builder) {
1037         init(builder);
1038         final JxltEngine.Exception xjexl = assertThrows(JxltEngine.Exception.class, () -> JXLT.createExpression("#{${hi} world}"), "should be malformed");
1039         LOGGER.debug(xjexl.getMessage());
1040     }
1041 
1042     @ParameterizedTest
1043     @MethodSource("engines")
1044     void testNested(final JexlBuilder builder) {
1045         init(builder);
1046         final String source = "#{${hi}+'.world'}";
1047         final JxltEngine.Expression expr = JXLT.createExpression(source);
1048 
1049         final Set<List<String>> evars = expr.getVariables();
1050         assertEquals(1, evars.size());
1051         assertTrue(contains(evars, Collections.singletonList("hi")));
1052 
1053         context.set("hi", "greeting");
1054         context.set("greeting.world", "Hello World!");
1055         assertTrue(expr.isDeferred(), "expression should be deferred");
1056         final Object o = expr.evaluate(context);
1057         assertEquals("Hello World!", o);
1058 
1059         assertEquals(source, getSource(expr.toString()));
1060     }
1061 
1062     @ParameterizedTest
1063     @MethodSource("engines")
1064     void testNestedTemplate(final JexlBuilder builder) {
1065         init(builder);
1066         final String source = "#{${hi}+'.world'}";
1067         final JxltEngine.Template expr = JXLT.createTemplate(source, "hi");
1068 
1069         context.set("greeting.world", "Hello World!");
1070         final StringWriter strw = new StringWriter();
1071         expr.evaluate(context, strw, "greeting");
1072         final String o = strw.toString();
1073         assertEquals("Hello World!", o);
1074 
1075         assertEquals(source, getSource(expr.toString()));
1076     }
1077 
1078     @ParameterizedTest
1079     @MethodSource("engines")
1080     void testNonEscapeString(final JexlBuilder builder) {
1081         init(builder);
1082         final JxltEngine.Expression expr = JXLT.createExpression("c:\\some\\windows\\path");
1083         final JexlContext none = null;
1084         final Object o = expr.evaluate(none);
1085         assertEquals("c:\\some\\windows\\path", o);
1086     }
1087 
1088     @ParameterizedTest
1089     @MethodSource("engines")
1090     void testOneLiner(final JexlBuilder builder) {
1091         init(builder);
1092         final JxltEngine.Template t = JXLT.createTemplate("$$", new StringReader("fourty-two"));
1093         final StringWriter strw = new StringWriter();
1094         t.evaluate(context, strw);
1095         final String output = strw.toString();
1096         assertEquals("fourty-two", output);
1097     }
1098 
1099     @ParameterizedTest
1100     @MethodSource("engines")
1101     void testOneLinerVar(final JexlBuilder builder) {
1102         init(builder);
1103         final JxltEngine.Template t = JXLT.createTemplate("$$", new StringReader("fourty-${x}"));
1104         final StringWriter strw = new StringWriter();
1105         context.set("x", "two");
1106         t.evaluate(context, strw);
1107         final String output = strw.toString();
1108         assertEquals("fourty-two", output);
1109     }
1110 
1111     @ParameterizedTest
1112     @MethodSource("engines")
1113     void testPrepareEvaluate(final JexlBuilder builder) {
1114         init(builder);
1115         final String source = "Dear #{p} ${name};";
1116         final JxltEngine.Expression expr = JXLT.createExpression("Dear #{p} ${name};");
1117         assertTrue(expr.isDeferred(), "expression should be deferred");
1118 
1119         final Set<List<String>> evars = expr.getVariables();
1120         assertEquals(1, evars.size());
1121         assertTrue(contains(evars, Collections.singletonList("name")));
1122         context.set("name", "Doe");
1123         final JxltEngine.Expression phase1 = expr.prepare(context);
1124         final String as = phase1.asString();
1125         assertEquals("Dear ${p} Doe;", as);
1126         final Set<List<String>> evars1 = phase1.getVariables();
1127         assertEquals(1, evars1.size());
1128         assertTrue(contains(evars1, Collections.singletonList("p")));
1129         vars.clear();
1130         context.set("p", "Mr");
1131         context.set("name", "Should not be used in 2nd phase");
1132         final Object o = phase1.evaluate(context);
1133         assertEquals("Dear Mr Doe;", o);
1134 
1135         final String p1 = getSource(phase1.toString());
1136         assertEquals(source, getSource(phase1.toString()));
1137         assertEquals(source, getSource(expr.toString()));
1138     }
1139 
1140     @ParameterizedTest
1141     @MethodSource("engines")
1142     void testPrepareTemplate(final JexlBuilder builder) {
1143         init(builder);
1144         // @formatter:off
1145         final String source
1146                 = "$$ for(var x : list) {\n"
1147                 + "${l10n}=#{x}\n"
1148                 + "$$ }\n";
1149         // @formatter:on
1150         final int[] args = {42};
1151         final JxltEngine.Template tl10n = JXLT.createTemplate(source, "list");
1152         final String dstr = tl10n.asString();
1153         assertNotNull(dstr);
1154         final Set<List<String>> vars = tl10n.getVariables();
1155         assertFalse(vars.isEmpty());
1156         context.set("l10n", "valeur");
1157         final JxltEngine.Template tpFR = tl10n.prepare(context);
1158         context.set("l10n", "value");
1159         final JxltEngine.Template tpEN = tl10n.prepare(context);
1160         context.set("l10n", null);
1161 
1162         StringWriter strw;
1163         strw = new StringWriter();
1164         tpFR.evaluate(context, strw, args);
1165         final String outFR = strw.toString();
1166         assertEquals("valeur=42\n", outFR);
1167 
1168         context.set("l10n", null);
1169         strw = new StringWriter();
1170         tpEN.evaluate(context, strw, args);
1171         final String outEN = strw.toString();
1172         assertEquals("value=42\n", outEN);
1173     }
1174 
1175     @ParameterizedTest
1176     @MethodSource("engines")
1177     void testReport(final JexlBuilder builder) {
1178         init(builder);
1179         // @formatter:off
1180         final String rpt
1181                 = "<report>\n"
1182                 + "\n"
1183                 + "\n$$ var a = 1;"
1184                 + "\n$$ var x = 2;"
1185                 + "\n"
1186                 + "\n$$ var y = 9;"
1187                 + "\n"
1188                 + "\n        ${x + y}"
1189                 + "\n</report>\n";
1190         // @formatter:on
1191         final JxltEngine.Template t = JXLT.createTemplate("$$", new StringReader(rpt));
1192         final StringWriter strw = new StringWriter();
1193         t.evaluate(context, strw);
1194         final String output = strw.toString();
1195         final String ctl = "<report>\n\n\n\n\n        11\n</report>\n";
1196         assertEquals(ctl, output);
1197 
1198         final TemplateDebugger td = new TemplateDebugger();
1199         final String refactored = refactor(td, t);
1200         assertNotNull(refactored);
1201         assertEquals(rpt, refactored);
1202     }
1203 
1204     @ParameterizedTest
1205     @MethodSource("engines")
1206     void testReport1(final JexlBuilder builder) {
1207         init(builder);
1208         // @formatter:off
1209         final String rpt
1210                 = "<report>\n"
1211                 + "this is ${x}\n"
1212                 + "${x + 1}\n"
1213                 + "${x + 2}\n"
1214                 + "${x + 3}\n"
1215                 + "</report>\n";
1216         // @formatter:on
1217         final JxltEngine.Template t = JXLT.createTemplate("$$", new StringReader(rpt));
1218         final StringWriter strw = new StringWriter();
1219         context.set("x", 42);
1220         t.evaluate(context, strw, 42);
1221         final String output = strw.toString();
1222         int count = 0;
1223         for (int i = 0; i < output.length(); ++i) {
1224             final char c = output.charAt(i);
1225             if ('\n' == c) {
1226                 count += 1;
1227             }
1228         }
1229         assertEquals(6, count);
1230         assertTrue(output.indexOf("42") > 0);
1231         assertTrue(output.indexOf("43") > 0);
1232         assertTrue(output.indexOf("44") > 0);
1233         assertTrue(output.indexOf("45") > 0);
1234     }
1235 
1236     @ParameterizedTest
1237     @MethodSource("engines")
1238     void testReport2(final JexlBuilder builder) {
1239         init(builder);
1240         // @formatter:off
1241         final String rpt
1242                 = "<report>\n"
1243                 + "this is ${x}\n"
1244                 + "${x + 1}\n"
1245                 + "${x + 2}\n"
1246                 + "${x + 3}\n"
1247                 + "</report>\n";
1248         // @formatter:on
1249         final JxltEngine.Template t = JXLT.createTemplate("$$", new StringReader(rpt), "x");
1250         final StringWriter strw = new StringWriter();
1251         t.evaluate(context, strw, 42);
1252         final String output = strw.toString();
1253         int count = 0;
1254         for (int i = 0; i < output.length(); ++i) {
1255             final char c = output.charAt(i);
1256             if ('\n' == c) {
1257                 count += 1;
1258             }
1259         }
1260         assertEquals(6, count);
1261         assertTrue(output.indexOf("42") > 0);
1262         assertTrue(output.indexOf("43") > 0);
1263         assertTrue(output.indexOf("44") > 0);
1264         assertTrue(output.indexOf("45") > 0);
1265 
1266         final TemplateDebugger td = new TemplateDebugger();
1267         final String xxx = refactor(td, t);
1268         assertNotNull(xxx);
1269         assertEquals(rpt, xxx);
1270     }
1271 
1272     @ParameterizedTest
1273     @MethodSource("engines")
1274     void testSanboxed311i(final JexlBuilder builder) {
1275         init(builder);
1276         /// this uberspect cannot access jexl3 classes (besides test)
1277         final Uberspect uberspect = new Uberspect(LogFactory.getLog(JXLTTest.class), null, NOJEXL3);
1278         final Method method = uberspect.getMethod(TemplateInterpreter.class, "print", new Object[]{Integer.TYPE});
1279         final JexlEngine jexl= new JexlBuilder().uberspect(uberspect).create();
1280         final JxltEngine jxlt = jexl.createJxltEngine();
1281         final JexlContext ctx311 = new Context311();
1282         // @formatter:off
1283         final String rpt
1284                 = "$$var u = 'Universe'; exec('4').execute((a, b)->{"
1285                 + "\n<p>${u} ${a}${b}</p>"
1286                 + "\n$$}, '2')";
1287         // @formatter:on
1288         final JxltEngine.Template t = jxlt.createTemplate("$$", new StringReader(rpt));
1289         final StringWriter strw = new StringWriter();
1290         t.evaluate(ctx311, strw, 42);
1291         final String output = strw.toString();
1292         assertEquals("<p>Universe 42</p>\n", output);
1293     }
1294 
1295     @ParameterizedTest
1296     @MethodSource("engines")
1297     void testSanboxedTemplate(final JexlBuilder builder) {
1298         init(builder);
1299         final String src = "Hello ${user}";
1300         final JexlContext ctxt = new MapContext();
1301         ctxt.set("user", "Francesco");
1302         /// this uberspect cannot access jexl3 classes (besides test)
1303         final Uberspect uberspect = new Uberspect(LogFactory.getLog(JXLTTest.class), null, NOJEXL3);
1304         final Method method = uberspect.getMethod(TemplateInterpreter.class, "print", new Object[]{Integer.TYPE});
1305         assertNull(method);
1306         // ensures JXLT sandboxed still executes
1307         final JexlEngine jexl= new JexlBuilder().uberspect(uberspect).create();
1308         final JxltEngine jxlt = jexl.createJxltEngine();
1309 
1310         final JxltEngine.Template tmplt = jxlt.createTemplate(src);
1311         final Writer strw = new StringWriter();
1312         tmplt.evaluate(ctxt, strw);
1313         final String result = strw.toString();
1314         assertEquals("Hello Francesco", result);
1315     }
1316 
1317     @ParameterizedTest
1318     @MethodSource("engines")
1319     void testStatement(final JexlBuilder builder) {
1320         init(builder);
1321         final Froboz froboz = new Froboz(32);
1322         context.set("froboz", froboz);
1323         final JxltEngine.Expression check = JXLT.createExpression("${ froboz.plus10() }");
1324         final Object o = check.evaluate(context);
1325         assertEquals(Integer.valueOf(32), o);
1326         assertEquals(42, froboz.getValue());
1327         final Set<List<String>> evars = check.getVariables();
1328         assertEquals(1, evars.size());
1329     }
1330 
1331     @ParameterizedTest
1332     @MethodSource("engines")
1333     void testTemplate0(final JexlBuilder builder) {
1334         init(builder);
1335         final String source = "   $$ if(x) {\nx is ${x}\n   $$ } else {\n${'no x'}\n$$ }\n";
1336         StringWriter strw;
1337         String output;
1338 
1339         final JxltEngine.Template t = JXLT.createTemplate(source);
1340 
1341         context.set("x", 42);
1342         strw = new StringWriter();
1343         t.evaluate(context, strw);
1344         output = strw.toString();
1345         assertEquals("x is 42\n", output);
1346 
1347         strw = new StringWriter();
1348         context.set("x", "");
1349         t.evaluate(context, strw);
1350         output = strw.toString();
1351         assertEquals("no x\n", output);
1352 
1353         final String dstr = t.toString();
1354         assertNotNull(dstr);
1355     }
1356 
1357     @ParameterizedTest
1358     @MethodSource("engines")
1359     void testTemplate1(final JexlBuilder builder) {
1360         init(builder);
1361         final String source = "$$ if(x) {\nx is ${x}\n$$ } else {\n${'no x'}\n$$ }\n";
1362         StringWriter strw;
1363         String output;
1364 
1365         final JxltEngine.Template t = JXLT.createTemplate("$$", new StringReader(source), "x");
1366         final String dstr = t.asString();
1367         assertNotNull(dstr);
1368 
1369         strw = new StringWriter();
1370         t.evaluate(context, strw, 42);
1371         output = strw.toString();
1372         assertEquals("x is 42\n", output);
1373 
1374         strw = new StringWriter();
1375         t.evaluate(context, strw, "");
1376         output = strw.toString();
1377         assertEquals("no x\n", output);
1378     }
1379 
1380     @ParameterizedTest
1381     @MethodSource("engines")
1382     void testTemplate10(final JexlBuilder builder) {
1383         init(builder);
1384         final String source = "$$(x)->{ if(x) {\nx is ${x}\n$$ } else {\n${'no x'}\n$$ } }\n";
1385         StringWriter strw;
1386         String output;
1387 
1388         final JxltEngine.Template t = JXLT.createTemplate("$$", new StringReader(source), (String[]) null);
1389         final String dstr = t.asString();
1390         assertNotNull(dstr);
1391 
1392         final String[] ps = t.getParameters();
1393         assertTrue(Arrays.asList(ps).contains("x"));
1394 
1395         strw = new StringWriter();
1396         t.evaluate(context, strw, 42);
1397         output = strw.toString();
1398         assertEquals("x is 42\n", output);
1399     }
1400 
1401     @ParameterizedTest
1402     @MethodSource("engines")
1403     void testTemplate2(final JexlBuilder builder) {
1404         init(builder);
1405         final String source = "The answer: ${x}";
1406         StringWriter strw;
1407         String output;
1408 
1409         final JxltEngine.Template t = JXLT.createTemplate("$$", new StringReader(source), "x");
1410         final String dstr = t.asString();
1411         assertNotNull(dstr);
1412 
1413         strw = new StringWriter();
1414         t.evaluate(context, strw, 42);
1415         output = strw.toString();
1416         assertEquals("The answer: 42", output);
1417     }
1418 
1419     @ParameterizedTest
1420     @MethodSource("engines")
1421     void testTemplateOutOfScope(final JexlBuilder builder) {
1422         init(builder);
1423         final JexlOptions opts = new JexlOptions();
1424         opts.setCancellable(false);
1425         opts.setStrict(false);
1426         opts.setLexical(false);
1427         opts.setLexicalShade(false);
1428         opts.setSharedInstance(true);
1429         final JexlContext ctxt = new PragmaticContext(opts);
1430         final String src = "$$if (false) { var tab = 42; }\n" + "${tab}";
1431         JxltEngine.Template tmplt;
1432         final JexlFeatures features = BUILDER.features();
1433         try {
1434             tmplt = JXLT.createTemplate("$$", new StringReader(src));
1435         } catch (final JexlException xparse) {
1436             if (features != null && features.isLexicalShade()) {
1437                 return;
1438             }
1439             throw xparse;
1440         }
1441         final Writer strw = new StringWriter();
1442         opts.setSafe(true);
1443         assertDoesNotThrow(() -> tmplt.evaluate(ctxt, strw), "safe should prevent local shade");
1444         assertTrue(strw.toString().isEmpty());
1445         opts.setStrict(true);
1446         opts.setSafe(false);
1447         final JexlException.Variable xvar = assertThrows(JexlException.Variable.class, () -> tmplt.evaluate(ctxt, strw));
1448         assertTrue("tab".equals(xvar.getVariable()));
1449         assertTrue(xvar.isUndefined());
1450     }
1451 
1452     @ParameterizedTest
1453     @MethodSource("engines")
1454     void testTemplatePragmaPro50(final JexlBuilder builder) {
1455         init(builder);
1456         final JexlOptions opts = new JexlOptions();
1457         opts.setCancellable(false);
1458         opts.setStrict(false);
1459         opts.setSafe(true);
1460         opts.setLexical(false);
1461         opts.setLexicalShade(false);
1462         opts.setSharedInstance(true);
1463         final JexlContext ctxt = new PragmaticContext(opts);
1464         // @formatter:off
1465         final String src = "$$ #pragma script.mode pro50\n"
1466                 + "$$ var tab = null;\n"
1467                 + "$$ tab.dummy();";
1468         // @formatter:on
1469         final JxltEngine.Template tmplt = JXLT.createTemplate("$$", new StringReader(src));
1470         final Writer strw = new StringWriter();
1471         final JexlException.Variable xvar = assertThrows(JexlException.Variable.class, () -> tmplt.evaluate(ctxt, strw));
1472         assertEquals("tab", xvar.getVariable());
1473         assertFalse(xvar.isUndefined());
1474     }
1475 
1476     @ParameterizedTest
1477     @MethodSource("engines")
1478     void testWriter(final JexlBuilder builder) {
1479         init(builder);
1480         final Froboz froboz = new Froboz(42);
1481         final Writer writer = new FrobozWriter(new StringWriter());
1482         final JxltEngine.Template t = JXLT.createTemplate("$$", new StringReader("$$$jexl.print(froboz)"), "froboz");
1483         t.evaluate(context, writer, froboz);
1484         assertEquals("froboz{42}", writer.toString());
1485     }
1486 }