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