blob: e6e10274985adbb9960ed6f5a5efed9f75a58440 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.jexl3;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import org.apache.commons.jexl3.parser.JexlParser;
import org.junit.Assert;
import org.junit.Test;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Tests local variables.
*/
@SuppressWarnings({"UnnecessaryBoxing", "AssertEqualsBetweenInconvertibleTypes"})
public class VarTest extends JexlTestCase {
static final Log LOGGER = LogFactory.getLog(VarTest.class.getName());
public static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd");
static {
SDF.setTimeZone(TimeZone.getTimeZone("UTC"));
}
public VarTest() {
super("VarTest");
}
@Test
public void testStrict() throws Exception {
final JexlEvalContext env = new JexlEvalContext();
final JexlOptions options = env.getEngineOptions();
final JexlContext ctxt = new ReadonlyContext(env, options);
options.setStrict(true);
options.setSilent(false);
options.setSafe(false);
JexlScript e;
e = JEXL.createScript("x");
try {
final Object o = e.execute(ctxt);
Assert.fail("should have thrown an unknown var exception");
} catch(final JexlException xjexl) {
// ok since we are strict and x does not exist
}
e = JEXL.createScript("x = 42");
try {
final Object o = e.execute(ctxt);
Assert.fail("should have thrown a readonly context exception");
} catch(final JexlException xjexl) {
// ok since we are strict and context is readonly
}
env.set("x", "fourty-two");
e = JEXL.createScript("x.theAnswerToEverything()");
try {
final Object o = e.execute(ctxt);
Assert.fail("should have thrown an unknown method exception");
} catch(final JexlException xjexl) {
// ok since we are strict and method does not exist
}
}
@Test
public void testLocalBasic() throws Exception {
final JexlScript e = JEXL.createScript("var x; x = 42");
final Object o = e.execute(null);
Assert.assertEquals("Result is not 42", new Integer(42), o);
}
@Test
public void testLocalSimple() throws Exception {
final JexlScript e = JEXL.createScript("var x = 21; x + x");
final Object o = e.execute(null);
Assert.assertEquals("Result is not 42", new Integer(42), o);
}
@Test
public void testLocalFor() throws Exception {
final JexlScript e = JEXL.createScript("var y = 0; for(var x : [5, 17, 20]) { y = y + x; } y;");
final Object o = e.execute(null);
Assert.assertEquals("Result is not 42", new Integer(42), o);
}
public static class NumbersContext extends MapContext implements JexlContext.NamespaceResolver {
@Override
public Object resolveNamespace(final String name) {
return name == null ? this : null;
}
public Object numbers() {
return new int[]{5, 17, 20};
}
}
@Test
public void testLocalForFunc() throws Exception {
final JexlContext jc = new NumbersContext();
final JexlScript e = JEXL.createScript("var y = 0; for(var x : numbers()) { y = y + x; } y;");
final Object o = e.execute(jc);
Assert.assertEquals("Result is not 42", new Integer(42), o);
}
@Test
public void testLocalForFuncReturn() throws Exception {
final JexlContext jc = new NumbersContext();
final JexlScript e = JEXL.createScript("var y = 42; for(var x : numbers()) { if (x > 10) return x } y;");
final Object o = e.execute(jc);
Assert.assertEquals("Result is not 17", new Integer(17), o);
Assert.assertTrue(toString(e.getVariables()), e.getVariables().isEmpty());
}
/**
* Generate a string representation of Set<List&t;String>>, useful to dump script variables
* @param refs the variable reference set
* @return the string representation
*/
String toString(final Set<List<String>> refs) {
final StringBuilder strb = new StringBuilder("{");
int r = 0;
for (final List<String> strs : refs) {
if (r++ > 0) {
strb.append(", ");
}
strb.append("{");
for (int s = 0; s < strs.size(); ++s) {
if (s > 0) {
strb.append(", ");
}
strb.append('"');
strb.append(strs.get(s));
strb.append('"');
}
strb.append("}");
}
strb.append("}");
return strb.toString();
}
/**
* Creates a variable reference set from an array of array of strings.
* @param refs the variable reference set
* @return the set of variables
*/
Set<List<String>> mkref(final String[][] refs) {
final Set<List<String>> set = new HashSet<List<String>>();
for(final String[] ref : refs) {
set.add(Arrays.asList(ref));
}
return set;
}
/**
* Checks that two sets of variable references are equal
* @param lhs the left set
* @param rhs the right set
* @return true if equal, false otherwise
*/
boolean eq(final Set<List<String>> lhs, final Set<List<String>> rhs) {
if (lhs.size() != rhs.size()) {
return false;
}
final List<String> llhs = stringify(lhs);
final List<String> lrhs = stringify(rhs);
for(int s = 0; s < llhs.size(); ++s) {
final String l = llhs.get(s);
final String r = lrhs.get(s);
if (!l.equals(r)) {
return false;
}
}
return true;
}
List<String> stringify(final Set<List<String>> sls) {
final List<String> ls = new ArrayList<String>();
for(final List<String> l : sls) {
final StringBuilder strb = new StringBuilder();
for(final String s : l) {
strb.append(s);
strb.append('|');
}
ls.add(strb.toString());
}
Collections.sort(ls);
return ls;
}
@Test
public void testRefs() throws Exception {
JexlScript e;
Set<List<String>> vars;
Set<List<String>> expect;
e = JEXL.createScript("a[b]['c']");
vars = e.getVariables();
expect = mkref(new String[][]{{"a"},{"b"}});
Assert.assertTrue(eq(expect, vars));
e = JEXL.createScript("a.'b + c'");
vars = e.getVariables();
expect = mkref(new String[][]{{"a", "b + c"}});
Assert.assertTrue(eq(expect, vars));
e = JEXL.createScript("e[f]");
vars = e.getVariables();
expect = mkref(new String[][]{{"e"},{"f"}});
Assert.assertTrue(eq(expect, vars));
e = JEXL.createScript("e[f][g]");
vars = e.getVariables();
expect = mkref(new String[][]{{"e"},{"f"},{"g"}});
Assert.assertTrue(eq(expect, vars));
e = JEXL.createScript("e['f'].goo");
vars = e.getVariables();
expect = mkref(new String[][]{{"e","f","goo"}});
Assert.assertTrue(eq(expect, vars));
e = JEXL.createScript("e['f']");
vars = e.getVariables();
expect = mkref(new String[][]{{"e","f"}});
Assert.assertTrue(eq(expect, vars));
e = JEXL.createScript("e[f]['g']");
vars = e.getVariables();
expect = mkref(new String[][]{{"e"},{"f"}});
Assert.assertTrue(eq(expect, vars));
e = JEXL.createScript("e['f']['g']");
vars = e.getVariables();
expect = mkref(new String[][]{{"e","f","g"}});
Assert.assertTrue(eq(expect, vars));
e = JEXL.createScript("a['b'].c['d'].e");
vars = e.getVariables();
expect = mkref(new String[][]{{"a", "b", "c", "d", "e"}});
Assert.assertTrue(eq(expect, vars));
e = JEXL.createScript("a + b.c + b.c.d + e['f']");
vars = e.getVariables();
expect = mkref(new String[][]{{"a"}, {"b", "c"}, {"b", "c", "d"}, {"e", "f"}});
Assert.assertTrue(eq(expect, vars));
e = JEXL.createScript("D[E[F]]");
vars = e.getVariables();
expect = mkref(new String[][]{{"D"}, {"E"}, {"F"}});
Assert.assertTrue(eq(expect, vars));
e = JEXL.createScript("D[E[F[G[H]]]]");
vars = e.getVariables();
expect = mkref(new String[][]{{"D"}, {"E"}, {"F"}, {"G"}, {"H"}});
Assert.assertTrue(eq(expect, vars));
e = JEXL.createScript(" A + B[C] + D[E[F]] + x[y[z]] ");
vars = e.getVariables();
expect = mkref(new String[][]{{"A"}, {"B"}, {"C"}, {"D"}, {"E"}, {"F"}, {"x"} , {"y"}, {"z"}});
Assert.assertTrue(eq(expect, vars));
e = JEXL.createScript(" A + B[C] + D.E['F'] + x[y.z] ");
vars = e.getVariables();
expect = mkref(new String[][]{{"A"}, {"B"}, {"C"}, {"D", "E", "F"}, {"x"} , {"y", "z"}});
Assert.assertTrue(eq(expect, vars));
e = JEXL.createScript("(A)");
vars = e.getVariables();
expect = mkref(new String[][]{{"A"}});
Assert.assertTrue(eq(expect, vars));
e = JEXL.createScript("not(A)");
vars = e.getVariables();
expect = mkref(new String[][]{{"A"}});
Assert.assertTrue(eq(expect, vars));
e = JEXL.createScript("not((A))");
vars = e.getVariables();
expect = mkref(new String[][]{{"A"}});
Assert.assertTrue(eq(expect, vars));
e = JEXL.createScript("a[b]['c']");
vars = e.getVariables();
expect = mkref(new String[][]{{"a"}, {"b"}});
Assert.assertTrue(eq(expect, vars));
e = JEXL.createScript("a['b'][c]");
vars = e.getVariables();
expect = mkref(new String[][]{{"a", "b"}, {"c"}});
Assert.assertTrue(eq(expect, vars));
e = JEXL.createScript("a[b].c");
vars = e.getVariables();
expect = mkref(new String[][]{{"a"}, {"b"}});
Assert.assertTrue(eq(expect, vars));
e = JEXL.createScript("a[b].c[d]");
vars = e.getVariables();
expect = mkref(new String[][]{{"a"}, {"b"}, {"d"}});
Assert.assertTrue(eq(expect, vars));
e = JEXL.createScript("a[b][e].c[d][f]");
vars = e.getVariables();
expect = mkref(new String[][]{{"a"}, {"b"}, {"d"}, {"e"}, {"f"}});
Assert.assertTrue(eq(expect, vars));
}
@Test
public void testVarCollectNotAll() throws Exception {
JexlScript e;
Set<List<String>> vars;
Set<List<String>> expect;
final JexlEngine jexl = new JexlBuilder().strict(true).silent(false).cache(32).collectAll(false).create();
e = jexl.createScript("a['b'][c]");
vars = e.getVariables();
expect = mkref(new String[][]{{"a"}, {"c"}});
Assert.assertTrue(eq(expect, vars));
e = jexl.createScript(" A + B[C] + D[E[F]] + x[y[z]] ");
vars = e.getVariables();
expect = mkref(new String[][]{{"A"}, {"B"}, {"C"}, {"D"}, {"E"}, {"F"}, {"x"} , {"y"}, {"z"}});
Assert.assertTrue(eq(expect, vars));
e = jexl.createScript("e['f']['g']");
vars = e.getVariables();
expect = mkref(new String[][]{{"e"}});
Assert.assertTrue(eq(expect, vars));
e = jexl.createScript("a[b][e].c[d][f]");
vars = e.getVariables();
expect = mkref(new String[][]{{"a"}, {"b"}, {"d"}, {"e"}, {"f"}});
Assert.assertTrue(eq(expect, vars));
e = jexl.createScript("a + b.c + b.c.d + e['f']");
vars = e.getVariables();
expect = mkref(new String[][]{{"a"}, {"b", "c"}, {"b", "c", "d"}, {"e"}});
Assert.assertTrue(eq(expect, vars));
}
@Test
public void testMix() throws Exception {
JexlScript e;
// x is a parameter, y a context variable, z a local variable
e = JEXL.createScript("if (x) { y } else { var z = 2 * x}", "x");
final Set<List<String>> vars = e.getVariables();
final String[] parms = e.getParameters();
final String[] locals = e.getLocalVariables();
Assert.assertTrue(eq(mkref(new String[][]{{"y"}}), vars));
Assert.assertEquals(1, parms.length);
Assert.assertEquals("x", parms[0]);
Assert.assertEquals(1, locals.length);
Assert.assertEquals("z", locals[0]);
}
/**
* Dates that can return multiple properties in one call.
*/
public static class VarDate {
private final Calendar cal;
public VarDate(final String date) throws Exception {
this(SDF.parse(date));
}
public VarDate(final Date date) {
cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
cal.setTime(date);
cal.setLenient(true);
}
/**
* Gets a date property
* @param property yyyy or MM or dd
* @return the string representation of year, month or day
*/
public String get(final String property) {
if ("yyyy".equals(property)) {
return Integer.toString(cal.get(Calendar.YEAR));
}
if ("MM".equals(property)) {
return Integer.toString(cal.get(Calendar.MONTH) + 1);
}
if ("dd".equals(property)) {
return Integer.toString(cal.get(Calendar.DAY_OF_MONTH));
}
return null;
}
/**
* Gets a list of properties.
* @param keys the property names
* @return the property values
*/
public List<String> get(final String[] keys) {
return get(Arrays.asList(keys));
}
/**
* Gets a list of properties.
* @param keys the property names
* @return the property values
*/
public List<String> get(final List<String> keys) {
final List<String> values = new ArrayList<String>();
for(final String key : keys) {
final String value = get(key);
if (value != null) {
values.add(value);
}
}
return values;
}
/**
* Gets a map of properties.
* <p>Uses each map key as a property name and each value as an alias
* used to key the resulting property value.
* @param map a map of property name to alias
* @return the alia map
*/
public Map<String,Object> get(final Map<String,String> map) {
final Map<String,Object> values = new LinkedHashMap<String,Object>();
for(final Map.Entry<String,String> entry : map.entrySet()) {
final String value = get(entry.getKey());
if (value != null) {
values.put(entry.getValue(), value);
}
}
return values;
}
}
/**
* Getting properties from an array, set or map.
* @param str the stringified source
* @return the properties array
*/
private static String[] readIdentifiers(final String str) {
final List<String> ids = new ArrayList<String>();
StringBuilder strb = null;
String id = null;
char kind = 0; // array, set or map kind using first char
for (int i = 0; i < str.length(); ++i) {
final char c = str.charAt(i);
// strb != null when array,set or map deteced
if (strb == null) {
if (c == '{' || c == '(' || c == '[') {
strb = new StringBuilder();
kind = c;
}
continue;
}
// identifier pending to be added (only add map keys)
if (id != null && c == ']' || c == ')'
|| (kind != '{' && c == ',') // array or set
|| (kind == '{' && c == ':')) // map key
{
ids.add(id);
id = null;
}
else if (c == '\'' || c == '"') {
strb.append(c);
final int l = JexlParser.readString(strb, str, i + 1, c);
if (l > 0) {
id = strb.substring(1, strb.length() - 1);
strb.delete(0, l + 1);
i = l;
}
}
// discard all chars not in identifier
}
return ids.toArray(new String[ids.size()]);
}
@Test
public void testReferenceLiteral() throws Exception {
final JexlEngine jexld = new JexlBuilder().collectMode(2).create();
JexlScript script;
List<String> result;
Set<List<String>> vars;
// in collectAll mode, the collector grabs all syntactic variations of
// constant variable references including map/arry/set literals
final JexlContext ctxt = new MapContext();
//d.yyyy = 1969; d.MM = 7; d.dd = 20
ctxt.set("moon.landing", new VarDate("1969-07-20"));
script = jexld.createScript("moon.landing[['yyyy', 'MM', 'dd']]");
result = (List<String>) script.execute(ctxt);
Assert.assertEquals(Arrays.asList("1969", "7", "20"), result);
vars = script.getVariables();
Assert.assertEquals(1, vars.size());
List<String> var = vars.iterator().next();
Assert.assertEquals("moon", var.get(0));
Assert.assertEquals("landing", var.get(1));
Assert.assertArrayEquals(new String[]{"yyyy", "MM", "dd"}, readIdentifiers(var.get(2)));
script = jexld.createScript("moon.landing[ { 'yyyy' : 'year', 'MM' : 'month', 'dd' : 'day' } ]");
final Map<String, String> mapr = (Map<String, String>) script.execute(ctxt);
Assert.assertEquals(3, mapr.size());
Assert.assertEquals("1969", mapr.get("year"));
Assert.assertEquals("7", mapr.get("month"));
Assert.assertEquals("20", mapr.get("day"));
vars = script.getVariables();
Assert.assertEquals(1, vars.size());
var = vars.iterator().next();
Assert.assertEquals("moon", var.get(0));
Assert.assertEquals("landing", var.get(1));
Assert.assertArrayEquals(new String[]{"yyyy", "MM", "dd"}, readIdentifiers(var.get(2)));
}
@Test
public void testLiteral() throws Exception {
JexlBuilder builder = new JexlBuilder().collectMode(2);
Assert.assertEquals(2, builder.collectMode());
Assert.assertTrue(builder.collectAll());
JexlEngine jexld = builder.create();
JexlScript e = jexld.createScript("x.y[['z', 't']]");
Set<List<String>> vars = e.getVariables();
Assert.assertEquals(1, vars.size());
Assert.assertTrue(eq(mkref(new String[][]{{"x", "y", "[ 'z', 't' ]"}}), vars));
e = jexld.createScript("x.y[{'z': 't'}]");
vars = e.getVariables();
Assert.assertEquals(1, vars.size());
Assert.assertTrue(eq(mkref(new String[][]{{"x", "y", "{ 'z' : 't' }"}}), vars));
e = jexld.createScript("x.y.'{ \\'z\\' : \\'t\\' }'");
vars = e.getVariables();
Assert.assertEquals(1, vars.size());
Assert.assertTrue(eq(mkref(new String[][]{{"x", "y", "{ 'z' : 't' }"}}), vars));
// only string or number literals
builder = builder.collectAll(true);
Assert.assertEquals(1, builder.collectMode());
Assert.assertTrue(builder.collectAll());
jexld = builder.create();
e = jexld.createScript("x.y[{'z': 't'}]");
vars = e.getVariables();
Assert.assertEquals(1, vars.size());
Assert.assertTrue(eq(mkref(new String[][]{{"x", "y"}}), vars));
e = jexld.createScript("x.y[['z', 't']]");
vars = e.getVariables();
Assert.assertEquals(1, vars.size());
Assert.assertTrue(eq(mkref(new String[][]{{"x", "y"}}), vars));
e = jexld.createScript("x.y['z']");
vars = e.getVariables();
Assert.assertEquals(1, vars.size());
Assert.assertTrue(eq(mkref(new String[][]{{"x", "y", "z"}}), vars));
e = jexld.createScript("x.y[42]");
vars = e.getVariables();
Assert.assertEquals(1, vars.size());
Assert.assertTrue(eq(mkref(new String[][]{{"x", "y", "42"}}), vars));
}
@Test
public void testSyntacticVariations() throws Exception {
final JexlScript script = JEXL.createScript("sum(TOTAL) - partial.sum() + partial['sub'].avg() - sum(partial.sub)");
final Set<List<String>> vars = script.getVariables();
Assert.assertEquals(3, vars.size());
}
public static class TheVarContext {
private int x;
private String color;
public void setX(final int x) {
this.x = x;
}
public void setColor(final String color) {
this.color = color;
}
public int getX() {
return x;
}
public String getColor() {
return color;
}
}
@Test
public void testObjectContext() throws Exception {
final TheVarContext vars = new TheVarContext();
final JexlContext jc = new ObjectContext<TheVarContext>(JEXL, vars);
try {
JexlScript script;
Object result;
script = JEXL.createScript("x = 3");
result = script.execute(jc);
Assert.assertEquals(3, vars.getX());
Assert.assertEquals(3, result);
script = JEXL.createScript("x == 3");
result = script.execute(jc);
Assert.assertTrue((Boolean) result);
Assert.assertTrue(jc.has("x"));
script = JEXL.createScript("color = 'blue'");
result = script.execute(jc);
Assert.assertEquals("blue", vars.getColor());
Assert.assertEquals("blue", result);
script = JEXL.createScript("color == 'blue'");
result = script.execute(jc);
Assert.assertTrue((Boolean) result);
Assert.assertTrue(jc.has("color"));
} catch (final JexlException.Method ambiguous) {
Assert.fail("total() is solvable");
}
}
}