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