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