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.assertArrayEquals;
20  import static org.junit.jupiter.api.Assertions.assertEquals;
21  import static org.junit.jupiter.api.Assertions.assertThrows;
22  import static org.junit.jupiter.api.Assertions.assertTrue;
23  import static org.junit.jupiter.api.Assertions.fail;
24  
25  import java.text.SimpleDateFormat;
26  import java.util.ArrayList;
27  import java.util.Arrays;
28  import java.util.Calendar;
29  import java.util.Collections;
30  import java.util.Date;
31  import java.util.HashSet;
32  import java.util.LinkedHashMap;
33  import java.util.List;
34  import java.util.Map;
35  import java.util.Set;
36  import java.util.TimeZone;
37  
38  import org.apache.commons.jexl3.parser.StringParser;
39  import org.apache.commons.logging.Log;
40  import org.apache.commons.logging.LogFactory;
41  import org.junit.jupiter.api.Test;
42  
43  /**
44   * Tests local variables.
45   */
46  @SuppressWarnings({"UnnecessaryBoxing", "AssertEqualsBetweenInconvertibleTypes"})
47  class VarTest extends JexlTestCase {
48      public static class NumbersContext extends MapContext implements JexlContext.NamespaceResolver {
49          public Object numbers() {
50              return new int[]{5, 17, 20};
51          }
52  
53          @Override
54          public Object resolveNamespace(final String name) {
55              return name == null ? this : null;
56          }
57      }
58      public static class TheVarContext {
59          private int x;
60          private String color;
61  
62          public String getColor() {
63              return color;
64          }
65  
66          public int getX() {
67              return x;
68          }
69  
70          public void setColor(final String color) {
71              this.color = color;
72          }
73  
74          public void setX(final int x) {
75              this.x = x;
76          }
77      }
78  
79      /**
80       * Dates that can return multiple properties in one call.
81       */
82      public static class VarDate {
83          private final Calendar cal;
84  
85          public VarDate(final Date date) {
86              cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
87              cal.setTime(date);
88              cal.setLenient(true);
89          }
90          public VarDate(final String date) throws Exception {
91              this(SDF.parse(date));
92          }
93  
94          /**
95           * Gets a list of properties.
96           * @param keys The property names
97           * @return The property values
98           */
99          public List<String> get(final List<String> keys) {
100             final List<String> values = new ArrayList<>();
101             for(final String key : keys) {
102                 final String value = get(key);
103                 if (value != null) {
104                     values.add(value);
105                 }
106             }
107             return values;
108         }
109 
110         /**
111          * Gets a map of properties.
112          * <p>Uses each map key as a property name and each value as an alias
113          * used to key the resulting property value.
114          * @param map A map of property name to alias
115          * @return The alia map
116          */
117         public Map<String,Object> get(final Map<String,String> map) {
118             final Map<String,Object> values = new LinkedHashMap<>();
119             for(final Map.Entry<String,String> entry : map.entrySet()) {
120                 final String value = get(entry.getKey());
121                 if (value != null) {
122                     values.put(entry.getValue(), value);
123                 }
124             }
125             return values;
126         }
127 
128         /**
129          * Gets a date property
130          * @param property yyyy or MM or dd
131          * @return The string representation of year, month or day
132          */
133         public String get(final String property) {
134             switch (property) {
135             case "yyyy":
136                 return Integer.toString(cal.get(Calendar.YEAR));
137             case "MM":
138                 return Integer.toString(cal.get(Calendar.MONTH) + 1);
139             case "dd":
140                 return Integer.toString(cal.get(Calendar.DAY_OF_MONTH));
141             default:
142                 break;
143             }
144             return null;
145         }
146 
147         /**
148          * Gets a list of properties.
149          * @param keys The property names
150          * @return The property values
151          */
152         public List<String> get(final String[] keys) {
153             return get(Arrays.asList(keys));
154         }
155     }
156 
157     static final Log LOGGER = LogFactory.getLog(VarTest.class.getName());
158 
159     public static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd");
160 
161     static {
162         SDF.setTimeZone(TimeZone.getTimeZone("UTC"));
163     }
164 
165     /**
166      * Getting properties from an array, set or map.
167      * @param str The stringified source
168      * @return The properties array
169      */
170     private static String[] readIdentifiers(final String str) {
171         final List<String> ids = new ArrayList<>();
172         StringBuilder strb = null;
173         String id = null;
174         char kind = 0; // array, set or map kind using first char
175         for (int i = 0; i < str.length(); ++i) {
176             final char c = str.charAt(i);
177             // strb != null when array,set or map deteced
178             if (strb == null) {
179                 if (c == '{' || c == '(' || c == '[') {
180                     strb = new StringBuilder();
181                     kind = c;
182                 }
183                 continue;
184             }
185             // identifier pending to be added (only add map keys)
186             if (id != null && c == ']' || c == ')'
187                 || kind != '{' && c == ',' // array or set
188                 || kind == '{' && c == ':') // map key
189             {
190                 ids.add(id);
191                 id = null;
192             }
193             else if (c == '\'' || c == '"') {
194                 strb.append(c);
195                 final int l = StringParser.readString(strb, str, i + 1, c);
196                 if (l > 0) {
197                     id = strb.substring(1, strb.length() - 1);
198                     strb.delete(0, l + 1);
199                     i = l;
200                 }
201             }
202             // discard all chars not in identifier
203         }
204         return ids.toArray(new String[0]);
205     }
206 
207     public VarTest() {
208         super("VarTest");
209     }
210 
211     /**
212      * Checks that two sets of variable references are equal
213      * @param lhs The left set
214      * @param rhs The right set
215      * @return true if equal, false otherwise
216      */
217     boolean eq(final Set<List<String>> lhs, final Set<List<String>> rhs) {
218         if (lhs.size() != rhs.size()) {
219             return false;
220         }
221         final List<String> llhs = stringify(lhs);
222         final List<String> lrhs = stringify(rhs);
223         for(int s = 0; s < llhs.size(); ++s) {
224             final String l = llhs.get(s);
225             final String r = lrhs.get(s);
226             if (!l.equals(r)) {
227                 return false;
228             }
229         }
230         return true;
231     }
232 
233     /**
234      * Creates a variable reference set from an array of array of strings.
235      * @param refs The variable reference set
236      * @return The set of variables
237      */
238     Set<List<String>> mkref(final String[][] refs) {
239         final Set<List<String>> set = new HashSet<>();
240         for(final String[] ref : refs) {
241             set.add(Arrays.asList(ref));
242         }
243         return set;
244     }
245 
246     List<String> stringify(final Set<List<String>> sls) {
247         final List<String> ls = new ArrayList<>();
248         for(final List<String> l : sls) {
249         final StringBuilder strb = new StringBuilder();
250         for(final String s : l) {
251             strb.append(s);
252             strb.append('|');
253         }
254             ls.add(strb.toString());
255         }
256         Collections.sort(ls);
257         return ls;
258     }
259 
260     @Test
261     void testLiteral() throws Exception {
262         JexlBuilder builder = new JexlBuilder().collectMode(2);
263         assertEquals(2, builder.collectMode());
264         assertTrue(builder.collectAll());
265 
266         JexlEngine jexld = builder.create();
267         JexlScript e = jexld.createScript("x.y[['z', 't']]");
268         Set<List<String>> vars = e.getVariables();
269         assertEquals(1, vars.size());
270         assertTrue(eq(mkref(new String[][]{{"x", "y", "[ 'z', 't' ]"}}), vars));
271 
272         e = jexld.createScript("x.y[{'z': 't'}]");
273         vars = e.getVariables();
274         assertEquals(1, vars.size());
275         assertTrue(eq(mkref(new String[][]{{"x", "y", "{ 'z' : 't' }"}}), vars));
276 
277         e = jexld.createScript("x.y.'{ \\'z\\' : \\'t\\' }'");
278         vars = e.getVariables();
279         assertEquals(1, vars.size());
280         assertTrue(eq(mkref(new String[][]{{"x", "y", "{ 'z' : 't' }"}}), vars));
281 
282         // only string or number literals
283         builder = builder.collectAll(true);
284         assertEquals(1, builder.collectMode());
285         assertTrue(builder.collectAll());
286 
287         jexld = builder.create();
288         e = jexld.createScript("x.y[{'z': 't'}]");
289         vars = e.getVariables();
290         assertEquals(1, vars.size());
291         assertTrue(eq(mkref(new String[][]{{"x", "y"}}), vars));
292 
293         e = jexld.createScript("x.y[['z', 't']]");
294         vars = e.getVariables();
295         assertEquals(1, vars.size());
296         assertTrue(eq(mkref(new String[][]{{"x", "y"}}), vars));
297 
298         e = jexld.createScript("x.y['z']");
299         vars = e.getVariables();
300         assertEquals(1, vars.size());
301         assertTrue(eq(mkref(new String[][]{{"x", "y", "z"}}), vars));
302 
303         e = jexld.createScript("x.y[42]");
304         vars = e.getVariables();
305         assertEquals(1, vars.size());
306         assertTrue(eq(mkref(new String[][]{{"x", "y", "42"}}), vars));
307     }
308 
309     @Test
310     void testLocalBasic() throws Exception {
311         final JexlScript e = JEXL.createScript("var x; x = 42");
312         final Object o = e.execute(null);
313         assertEquals(Integer.valueOf(42), o);
314     }
315 
316     @Test
317     void testLocalFor() throws Exception {
318         final JexlScript e = JEXL.createScript("var y  = 0; for(var x : [5, 17, 20]) { y = y + x; } y;");
319         final Object o = e.execute(null);
320         assertEquals(Integer.valueOf(42), o);
321     }
322 
323     @Test
324     void testLocalForFunc() throws Exception {
325         final JexlContext jc = new NumbersContext();
326         final JexlScript e = JEXL.createScript("var y  = 0; for(var x : numbers()) { y = y + x; } y;");
327         final Object o = e.execute(jc);
328         assertEquals(Integer.valueOf(42), o);
329     }
330 
331     @Test
332     void testLocalForFuncReturn() throws Exception {
333         final JexlContext jc = new NumbersContext();
334         final JexlScript e = JEXL.createScript("var y  = 42; for(var x : numbers()) { if (x > 10) return x } y;");
335         final Object o = e.execute(jc);
336         assertEquals(Integer.valueOf(17), o);
337 
338         assertTrue(e.getVariables().isEmpty(), () -> toString(e.getVariables()));
339     }
340 
341     @Test
342     void testLocalSimple() throws Exception {
343         final JexlScript e = JEXL.createScript("var x = 21; x + x");
344         final Object o = e.execute(null);
345         assertEquals(Integer.valueOf(42), o);
346     }
347 
348     @Test
349     void testMix() throws Exception {
350         JexlScript e;
351         // x is a parameter, y a context variable, z a local variable
352         e = JEXL.createScript("if (x) { y } else { var z = 2 * x}", "x");
353         final Set<List<String>> vars = e.getVariables();
354         final String[] parms = e.getParameters();
355         final String[] locals = e.getLocalVariables();
356 
357         assertTrue(eq(mkref(new String[][]{{"y"}}), vars));
358         assertEquals(1, parms.length);
359         assertEquals("x", parms[0]);
360         assertEquals(1, locals.length);
361         assertEquals("z", locals[0]);
362     }
363 
364     @Test
365     void testObjectContext() throws Exception {
366         final TheVarContext vars = new TheVarContext();
367         final JexlContext jc = new ObjectContext<>(JEXL, vars);
368         try {
369             JexlScript script;
370             Object result;
371             script = JEXL.createScript("x = 3");
372             result = script.execute(jc);
373             assertEquals(3, vars.getX());
374             assertEquals(3, result);
375             script = JEXL.createScript("x == 3");
376             result = script.execute(jc);
377             assertTrue((Boolean) result);
378             assertTrue(jc.has("x"));
379 
380             script = JEXL.createScript("color = 'blue'");
381             result = script.execute(jc);
382             assertEquals("blue", vars.getColor());
383             assertEquals("blue", result);
384             script = JEXL.createScript("color == 'blue'");
385             result = script.execute(jc);
386             assertTrue((Boolean) result);
387             assertTrue(jc.has("color"));
388         } catch (final JexlException.Method ambiguous) {
389             fail("total() is solvable");
390         }
391     }
392 
393     @Test
394     void testReferenceLiteral() throws Exception {
395         final JexlEngine jexld = new JexlBuilder().collectMode(2).create();
396         JexlScript script;
397         List<String> result;
398         Set<List<String>> vars;
399         // in collectAll mode, the collector grabs all syntactic variations of
400         // constant variable references including map/arry/set literals
401         final JexlContext ctxt = new MapContext();
402         //d.yyyy = 1969; d.MM = 7; d.dd = 20
403         ctxt.set("moon.landing", new VarDate("1969-07-20"));
404 
405         script = jexld.createScript("moon.landing[['yyyy', 'MM', 'dd']]");
406         result = (List<String>) script.execute(ctxt);
407         assertEquals(Arrays.asList("1969", "7", "20"), result);
408 
409         vars = script.getVariables();
410         assertEquals(1, vars.size());
411         List<String> var = vars.iterator().next();
412         assertEquals("moon", var.get(0));
413         assertEquals("landing", var.get(1));
414         assertArrayEquals(new String[]{"yyyy", "MM", "dd"}, readIdentifiers(var.get(2)));
415 
416         script = jexld.createScript("moon.landing[ { 'yyyy' : 'year', 'MM' : 'month', 'dd' : 'day' } ]");
417         final Map<String, String> mapr = (Map<String, String>) script.execute(ctxt);
418         assertEquals(3, mapr.size());
419         assertEquals("1969", mapr.get("year"));
420         assertEquals("7", mapr.get("month"));
421         assertEquals("20", mapr.get("day"));
422 
423         vars = script.getVariables();
424         assertEquals(1, vars.size());
425         var = vars.iterator().next();
426         assertEquals("moon", var.get(0));
427         assertEquals("landing", var.get(1));
428         assertArrayEquals(new String[]{"yyyy", "MM", "dd"}, readIdentifiers(var.get(2)));
429     }
430 
431     @Test
432     void testRefs() throws Exception {
433         JexlScript e;
434         Set<List<String>> vars;
435         Set<List<String>> expect;
436 
437         e = JEXL.createScript("a[b]['c']");
438         vars = e.getVariables();
439         expect = mkref(new String[][]{{"a"},{"b"}});
440         assertTrue(eq(expect, vars));
441 
442         e = JEXL.createScript("a.'b + c'");
443         vars = e.getVariables();
444         expect = mkref(new String[][]{{"a", "b + c"}});
445         assertTrue(eq(expect, vars));
446 
447         e = JEXL.createScript("e[f]");
448         vars = e.getVariables();
449         expect = mkref(new String[][]{{"e"},{"f"}});
450         assertTrue(eq(expect, vars));
451 
452         e = JEXL.createScript("e[f][g]");
453         vars = e.getVariables();
454         expect = mkref(new String[][]{{"e"},{"f"},{"g"}});
455         assertTrue(eq(expect, vars));
456 
457         e = JEXL.createScript("e['f'].goo");
458         vars = e.getVariables();
459         expect = mkref(new String[][]{{"e","f","goo"}});
460         assertTrue(eq(expect, vars));
461 
462         e = JEXL.createScript("e['f']");
463         vars = e.getVariables();
464         expect = mkref(new String[][]{{"e","f"}});
465         assertTrue(eq(expect, vars));
466 
467         e = JEXL.createScript("e[f]['g']");
468         vars = e.getVariables();
469         expect = mkref(new String[][]{{"e"},{"f"}});
470         assertTrue(eq(expect, vars));
471 
472         e = JEXL.createScript("e['f']['g']");
473         vars = e.getVariables();
474         expect = mkref(new String[][]{{"e","f","g"}});
475         assertTrue(eq(expect, vars));
476 
477         e = JEXL.createScript("a['b'].c['d'].e");
478         vars = e.getVariables();
479         expect = mkref(new String[][]{{"a", "b", "c", "d", "e"}});
480         assertTrue(eq(expect, vars));
481 
482         e = JEXL.createScript("a + b.c + b.c.d + e['f']");
483         vars = e.getVariables();
484         expect = mkref(new String[][]{{"a"}, {"b", "c"}, {"b", "c", "d"}, {"e", "f"}});
485         assertTrue(eq(expect, vars));
486 
487         e = JEXL.createScript("D[E[F]]");
488         vars = e.getVariables();
489         expect = mkref(new String[][]{{"D"}, {"E"}, {"F"}});
490         assertTrue(eq(expect, vars));
491 
492         e = JEXL.createScript("D[E[F[G[H]]]]");
493         vars = e.getVariables();
494         expect = mkref(new String[][]{{"D"}, {"E"}, {"F"}, {"G"}, {"H"}});
495         assertTrue(eq(expect, vars));
496 
497         e = JEXL.createScript(" A + B[C] + D[E[F]] + x[y[z]] ");
498         vars = e.getVariables();
499         expect = mkref(new String[][]{{"A"}, {"B"}, {"C"}, {"D"}, {"E"}, {"F"}, {"x"} , {"y"}, {"z"}});
500         assertTrue(eq(expect, vars));
501 
502         e = JEXL.createScript(" A + B[C] + D.E['F'] + x[y.z] ");
503         vars = e.getVariables();
504         expect = mkref(new String[][]{{"A"}, {"B"}, {"C"}, {"D", "E", "F"}, {"x"} , {"y", "z"}});
505         assertTrue(eq(expect, vars));
506 
507         e = JEXL.createScript("(A)");
508         vars = e.getVariables();
509         expect = mkref(new String[][]{{"A"}});
510         assertTrue(eq(expect, vars));
511 
512         e = JEXL.createScript("not(A)");
513         vars = e.getVariables();
514         expect = mkref(new String[][]{{"A"}});
515         assertTrue(eq(expect, vars));
516 
517         e = JEXL.createScript("not((A))");
518         vars = e.getVariables();
519         expect = mkref(new String[][]{{"A"}});
520         assertTrue(eq(expect, vars));
521 
522         e = JEXL.createScript("a[b]['c']");
523         vars = e.getVariables();
524         expect = mkref(new String[][]{{"a"}, {"b"}});
525         assertTrue(eq(expect, vars));
526 
527         e = JEXL.createScript("a['b'][c]");
528         vars = e.getVariables();
529         expect = mkref(new String[][]{{"a", "b"}, {"c"}});
530         assertTrue(eq(expect, vars));
531 
532         e = JEXL.createScript("a[b].c");
533         vars = e.getVariables();
534         expect = mkref(new String[][]{{"a"}, {"b"}});
535         assertTrue(eq(expect, vars));
536 
537         e = JEXL.createScript("a[b].c[d]");
538         vars = e.getVariables();
539         expect = mkref(new String[][]{{"a"}, {"b"}, {"d"}});
540         assertTrue(eq(expect, vars));
541 
542         e = JEXL.createScript("a[b][e].c[d][f]");
543         vars = e.getVariables();
544         expect = mkref(new String[][]{{"a"}, {"b"}, {"d"}, {"e"}, {"f"}});
545         assertTrue(eq(expect, vars));
546     }
547 
548     @Test
549     void testStrict() throws Exception {
550         final JexlEvalContext env = new JexlEvalContext();
551         final JexlOptions options = env.getEngineOptions();
552         final JexlContext ctxt = new ReadonlyContext(env, options);
553         options.setStrict(true);
554         options.setSilent(false);
555         options.setSafe(false);
556 
557         final JexlScript e0 = JEXL.createScript("x");
558         assertThrows(JexlException.class, () -> e0.execute(ctxt), "should have thrown an unknown var exception");
559 
560         final JexlScript e1 = JEXL.createScript("x = 42");
561         assertThrows(JexlException.class, () -> e1.execute(ctxt), "should have thrown a readonly context exception");
562 
563         env.set("x", "fourty-two");
564         final JexlScript e2 = JEXL.createScript("x.theAnswerToEverything()");
565         assertThrows(JexlException.class, () -> e2.execute(ctxt), "should have thrown an unknown method exception");
566     }
567 
568     @Test
569     void testSyntacticVariations() throws Exception {
570         final JexlScript script = JEXL.createScript("sum(TOTAL) - partial.sum() + partial['sub'].avg() - sum(partial.sub)");
571         final Set<List<String>> vars = script.getVariables();
572 
573         assertEquals(3, vars.size());
574     }
575 
576     @Test
577     void testVarCollectNotAll() throws Exception {
578         JexlScript e;
579         Set<List<String>> vars;
580         Set<List<String>> expect;
581         final JexlEngine jexl = new JexlBuilder().strict(true).silent(false).cache(32).collectAll(false).create();
582 
583         e = jexl.createScript("a['b'][c]");
584         vars = e.getVariables();
585         expect = mkref(new String[][]{{"a"}, {"c"}});
586         assertTrue(eq(expect, vars));
587 
588         e = jexl.createScript(" A + B[C] + D[E[F]] + x[y[z]] ");
589         vars = e.getVariables();
590         expect = mkref(new String[][]{{"A"}, {"B"}, {"C"}, {"D"}, {"E"}, {"F"}, {"x"} , {"y"}, {"z"}});
591         assertTrue(eq(expect, vars));
592 
593         e = jexl.createScript("e['f']['g']");
594         vars = e.getVariables();
595         expect = mkref(new String[][]{{"e"}});
596         assertTrue(eq(expect, vars));
597 
598         e = jexl.createScript("a[b][e].c[d][f]");
599         vars = e.getVariables();
600         expect = mkref(new String[][]{{"a"}, {"b"}, {"d"}, {"e"}, {"f"}});
601         assertTrue(eq(expect, vars));
602 
603         e = jexl.createScript("a + b.c + b.c.d + e['f']");
604         vars = e.getVariables();
605         expect = mkref(new String[][]{{"a"}, {"b", "c"}, {"b", "c", "d"}, {"e"}});
606         assertTrue(eq(expect, vars));
607     }
608 
609     /**
610      * Generate a string representation of Set&lt;List&t;String>>, useful to dump script variables
611      * @param refs The variable reference set
612      * @return  the string representation
613      */
614     String toString(final Set<List<String>> refs) {
615         final StringBuilder strb = new StringBuilder("{");
616         int r = 0;
617         for (final List<String> strs : refs) {
618             if (r++ > 0) {
619                 strb.append(", ");
620             }
621             strb.append("{");
622             for (int s = 0; s < strs.size(); ++s) {
623                 if (s > 0) {
624                     strb.append(", ");
625                 }
626                 strb.append('"');
627                 strb.append(strs.get(s));
628                 strb.append('"');
629             }
630             strb.append("}");
631         }
632         strb.append("}");
633         return strb.toString();
634     }
635 
636 }