Author: henrib
Date: Wed Aug 3 13:17:44 2011
New Revision: 1153476
URL: http://svn.apache.org/viewvc?rev=1153476&view=rev
Log:
JEXL-118
* Extended =~ and !~ operator to behave as IN and NOT IN on sets, collections,
maps (on keys), arrays and "duck-typed" collections (classes exposing
"contains" method)
* Updated ArrayListWrapper to allow better control over its usage as method
declaring class
* Added specific tests
* Updated syntax reference
* Updated changes
Modified:
commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl2/Interpreter.java
commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl2/JexlArithmetic.java
commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl2/internal/ArrayListWrapper.java
commons/proper/jexl/trunk/src/site/xdoc/changes.xml
commons/proper/jexl/trunk/src/site/xdoc/reference/syntax.xml
commons/proper/jexl/trunk/src/test/java/org/apache/commons/jexl2/ArithmeticTest.java
Modified:
commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl2/Interpreter.java
URL:
http://svn.apache.org/viewvc/commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl2/Interpreter.java?rev=1153476&r1=1153475&r2=1153476&view=diff
==============================================================================
---
commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl2/Interpreter.java
(original)
+++
commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl2/Interpreter.java
Wed Aug 3 13:17:44 2011
@@ -23,6 +23,7 @@ import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
+import java.util.Set;
import org.apache.commons.jexl2.parser.SimpleNode;
import org.apache.commons.logging.Log;
@@ -727,7 +728,7 @@ public class Interpreter implements Pars
JexlNode statement = node.jjtGetChild(2);
// get an iterator for the collection/array etc via the
// introspector.
- Iterator<?> itemsIterator =
getUberspect().getIterator(iterableValue, node);
+ Iterator<?> itemsIterator = uberspect.getIterator(iterableValue,
node);
if (itemsIterator != null) {
while (itemsIterator.hasNext()) {
if (isCancelled()) {
@@ -775,7 +776,53 @@ public class Interpreter implements Pars
Object left = node.jjtGetChild(0).jjtAccept(this, data);
Object right = node.jjtGetChild(1).jjtAccept(this, data);
try {
- return arithmetic.matches(left, right) ? Boolean.TRUE :
Boolean.FALSE;
+ // use arithmetic / pattern matching ?
+ if (right instanceof java.util.regex.Pattern || right instanceof
String) {
+ return arithmetic.matches(left, right) ? Boolean.TRUE :
Boolean.FALSE;
+ }
+ // left in right ? <=> right.contains(left) ?
+ // try contains on collection
+ if (right instanceof Set<?>) {
+ return ((Set<?>) right).contains(left) ? Boolean.TRUE :
Boolean.FALSE;
+ }
+ // try contains on map key
+ if (right instanceof Map<?,?>) {
+ return ((Map<?,?>) right).containsKey(left) ? Boolean.TRUE :
Boolean.FALSE;
+ }
+ // try contains on collection
+ if (right instanceof Collection<?>) {
+ return ((Collection<?>) right).contains(left) ? Boolean.TRUE :
Boolean.FALSE;
+ }
+ // try a contains method (duck type set)
+ try {
+ Object[] argv = {left};
+ JexlMethod vm = uberspect.getMethod(right, "contains", argv,
node);
+ if (vm != null) {
+ return arithmetic.toBoolean(vm.invoke(right, argv)) ?
Boolean.TRUE : Boolean.FALSE;
+ } else if (arithmetic.narrowArguments(argv)) {
+ vm = uberspect.getMethod(right, "contains", argv, node);
+ if (vm != null) {
+ return arithmetic.toBoolean(vm.invoke(right, argv)) ?
Boolean.TRUE : Boolean.FALSE;
+ }
+ }
+ } catch (InvocationTargetException e) {
+ throw new JexlException(node, "=~ invocation error",
e.getCause());
+ } catch (Exception e) {
+ throw new JexlException(node, "=~ error", e);
+ }
+ // try iterative comparison
+ Iterator<?> it = uberspect.getIterator(right, node);
+ if (it != null) {
+ while (it.hasNext()) {
+ Object next = it.next();
+ if (next == left || (next != null && next.equals(left))) {
+ return Boolean.TRUE;
+ }
+ }
+ return Boolean.FALSE;
+ }
+ // defaults to equal
+ return arithmetic.equals(left, right) ? Boolean.TRUE :
Boolean.FALSE;
} catch (RuntimeException xrt) {
throw new JexlException(node, "=~ error", xrt);
}
@@ -1107,7 +1154,52 @@ public class Interpreter implements Pars
Object left = node.jjtGetChild(0).jjtAccept(this, data);
Object right = node.jjtGetChild(1).jjtAccept(this, data);
try {
- return arithmetic.matches(left, right) ? Boolean.FALSE :
Boolean.TRUE;
+ if (right instanceof java.util.regex.Pattern || right instanceof
String) {
+ // use arithmetic / pattern matching
+ return arithmetic.matches(left, right) ? Boolean.FALSE :
Boolean.TRUE;
+ }
+ // try contains on collection
+ if (right instanceof Set<?>) {
+ return ((Set<?>) right).contains(left) ? Boolean.FALSE :
Boolean.TRUE;
+ }
+ // try contains on map key
+ if (right instanceof Map<?,?>) {
+ return ((Map<?,?>) right).containsKey(left) ? Boolean.FALSE :
Boolean.TRUE;
+ }
+ // try contains on collection
+ if (right instanceof Collection<?>) {
+ return ((Collection<?>) right).contains(left) ? Boolean.FALSE
: Boolean.TRUE;
+ }
+ // try a contains method (duck type set)
+ try {
+ Object[] argv = {right};
+ JexlMethod vm = uberspect.getMethod(left, "contains", argv,
node);
+ if (vm != null) {
+ return arithmetic.toBoolean(vm.invoke(left, argv)) ?
Boolean.FALSE : Boolean.TRUE;
+ } else if (arithmetic.narrowArguments(argv)) {
+ vm = uberspect.getMethod(left, "contains", argv, node);
+ if (vm != null) {
+ return arithmetic.toBoolean(vm.invoke(left, argv)) ?
Boolean.FALSE : Boolean.TRUE;
+ }
+ }
+ } catch (InvocationTargetException e) {
+ throw new JexlException(node, "!~ invocation error",
e.getCause());
+ } catch (Exception e) {
+ throw new JexlException(node, "!~ error", e);
+ }
+ // try iterative comparison
+ Iterator<?> it = uberspect.getIterator(right, node.jjtGetChild(1));
+ if (it != null) {
+ while (it.hasNext()) {
+ Object next = it.next();
+ if (next == left || (next != null && next.equals(left))) {
+ return Boolean.FALSE;
+ }
+ }
+ return Boolean.TRUE;
+ }
+ // defaults to not equal
+ return arithmetic.equals(left, right) ? Boolean.FALSE :
Boolean.TRUE;
} catch (RuntimeException xrt) {
throw new JexlException(node, "!~ error", xrt);
}
Modified:
commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl2/JexlArithmetic.java
URL:
http://svn.apache.org/viewvc/commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl2/JexlArithmetic.java?rev=1153476&r1=1153475&r2=1153476&view=diff
==============================================================================
---
commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl2/JexlArithmetic.java
(original)
+++
commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl2/JexlArithmetic.java
Wed Aug 3 13:17:44 2011
@@ -21,6 +21,8 @@ import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.MathContext;
+import java.util.Collection;
+import java.util.Iterator;
/**
* Perform arithmetic.
@@ -599,8 +601,8 @@ public class JexlArithmetic {
return left.equals(right);
} else if (isFloatingPointType(left, right)) {
return toDouble(left) == toDouble(right);
- } else if (left instanceof Number || right instanceof Number || left
instanceof Character
- || right instanceof Character) {
+ } else if ((left instanceof Number || left instanceof Character)
+ && (right instanceof Character || right instanceof Number)) {
return toLong(left) == toLong(right);
} else if (left instanceof Boolean || right instanceof Boolean) {
return toBoolean(left) == toBoolean(right);
Modified:
commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl2/internal/ArrayListWrapper.java
URL:
http://svn.apache.org/viewvc/commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl2/internal/ArrayListWrapper.java?rev=1153476&r1=1153475&r2=1153476&view=diff
==============================================================================
---
commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl2/internal/ArrayListWrapper.java
(original)
+++
commons/proper/jexl/trunk/src/main/java/org/apache/commons/jexl2/internal/ArrayListWrapper.java
Wed Aug 3 13:17:44 2011
@@ -18,9 +18,18 @@ package org.apache.commons.jexl2.interna
import java.lang.reflect.Array;
import java.util.AbstractList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
/**
- * A class that wraps an array with a List interface.
+ * A class that wraps an array within an AbstractList.
+ * <p>
+ * It overrides all methods because introspection uses this class a a marker
for wrapped arrays; the declared class
+ * for any method is thus always ArrayListWrapper.
+ * </p>
*/
public class ArrayListWrapper extends AbstractList<Object> {
/** the array to wrap. */
@@ -56,4 +65,136 @@ public class ArrayListWrapper extends Ab
public int size() {
return Array.getLength(array);
}
+
+ @Override
+ public Object[] toArray() {
+ final int size = size();
+ Object[] a = new Object[size];
+ for(int i = 0; i < size; ++i) {
+ a[i] = get(i);
+ }
+ return a;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public <T> T[] toArray(T[] a) {
+ int size = size();
+ if (a.length < size) {
+ T[] x = (T[]) Array.newInstance(a.getClass().getComponentType(),
size);
+ System.arraycopy(a, a.length, x, 0, a.length);
+ }
+ for(int i = 0; i < size; ++i) {
+ a[i] = (T) get(i);
+ }
+ if (a.length > size) {
+ a[size] = null;
+ }
+ return a;
+ }
+
+ @Override
+ public int indexOf(Object o) {
+ final int size = size();
+ if (o == null) {
+ for (int i = 0; i < size; i++) {
+ if (get(i) == null) {
+ return i;
+ }
+ }
+ } else {
+ for (int i = 0; i < size; i++) {
+ if (o.equals(get(i))) {
+ return i;
+ }
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ public boolean contains(Object o) {
+ return indexOf(o) != -1;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return super.isEmpty();
+ }
+
+ @Override
+ public Iterator<Object> iterator() {
+ return super.iterator();
+ }
+
+ @Override
+ public boolean containsAll(Collection<?> c) {
+ return super.containsAll(c);
+ }
+
+ @Override
+ public int lastIndexOf(Object o) {
+ return super.lastIndexOf(o);
+ }
+
+ @Override
+ public ListIterator<Object> listIterator() {
+ return super.listIterator();
+ }
+
+ @Override
+ public ListIterator<Object> listIterator(int index) {
+ return super.listIterator(index);
+ }
+
+ @Override
+ public List<Object> subList(int fromIndex, int toIndex) {
+ return super.subList(fromIndex, toIndex);
+ }
+
+ @Override
+ public boolean add(Object o) {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public boolean remove(Object o) {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public boolean addAll(Collection<? extends Object> c) {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public boolean addAll(int index, Collection<? extends Object> c) {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public boolean removeAll(Collection<?> c) {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public boolean retainAll(Collection<?> c) {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public void clear() {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public void add(int index, Object element) {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public Object remove(int index) {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
}
\ No newline at end of file
Modified: commons/proper/jexl/trunk/src/site/xdoc/changes.xml
URL:
http://svn.apache.org/viewvc/commons/proper/jexl/trunk/src/site/xdoc/changes.xml?rev=1153476&r1=1153475&r2=1153476&view=diff
==============================================================================
--- commons/proper/jexl/trunk/src/site/xdoc/changes.xml (original)
+++ commons/proper/jexl/trunk/src/site/xdoc/changes.xml Wed Aug 3 13:17:44 2011
@@ -26,6 +26,9 @@
</properties>
<body>
<release version="2.1" date="unreleased">
+ <action dev="henrib" type="add" issue="JEXL-118" due-to="Max
Tardiveau">
+ Provide an IN operator: =~ / match operator extended to provide IN
behavior (!~ as NOT IN)
+ </action>
<action dev="henrib" type="add" issue="JEXL-116" due-to="Sarel Botha">
Add control over classes, methods, constructors and properties
allowed in scripts
</action>
Modified: commons/proper/jexl/trunk/src/site/xdoc/reference/syntax.xml
URL:
http://svn.apache.org/viewvc/commons/proper/jexl/trunk/src/site/xdoc/reference/syntax.xml?rev=1153476&r1=1153475&r2=1153476&view=diff
==============================================================================
--- commons/proper/jexl/trunk/src/site/xdoc/reference/syntax.xml (original)
+++ commons/proper/jexl/trunk/src/site/xdoc/reference/syntax.xml Wed Aug 3
13:17:44 2011
@@ -450,21 +450,29 @@
</td>
</tr>
<tr>
- <td>Regex match <code>=~</code></td>
+ <td>In or Match<code>=~</code></td>
<td>
- The Perl inspired <code>=~</code> operator can be used to check
that a <code>string</code> matches
+ The syntactically Perl inspired <code>=~</code> operator can be
used to check that a <code>string</code> matches
a regular expression (expressed either a Java String or a
java.util.regex.Pattern).
For example
<code>"abcdef" =~ "abc.*</code> returns <code>true</code>.
+ It also checks whether any collection, set or map (on keys)
contains a value or not; in that case, it behaves
+ as an "in" operator. Note that it also applies to arrays as well
as "duck-typed" collection, ie classes exposing a "contains"
+ method.
+ <code> "a" =~ ["a","b","c","d","e",f"]</code> returns
<code>true</code>.
</td>
</tr>
<tr>
- <td>Regex no-match <code>!~</code></td>
+ <td>Not-In or Not-Match<code>!~</code></td>
<td>
- The Perl inspired <code>!~</code> operator can be used to check
that a <code>string</code> does not
+ The syntactically Perl inspired <code>!~</code> operator can be
used to check that a <code>string</code> does not
match a regular expression (expressed either a Java String or a
java.util.regex.Pattern).
For example
<code>"abcdef" !~ "abc.*</code> returns <code>false</code>.
+ It also checks whether any collection, set or map (on keys) does
not contain a value; in that case, it behaves
+ as "not in" operator. Note that it also applies to arrays as well
as "duck-typed" collection, ie classes exposing a "contains"
+ method.
+ <code> "a" !~ ["a","b","c","d","e",f"]</code> returns
<code>true</code>.
</td>
</tr>
<tr>
Modified:
commons/proper/jexl/trunk/src/test/java/org/apache/commons/jexl2/ArithmeticTest.java
URL:
http://svn.apache.org/viewvc/commons/proper/jexl/trunk/src/test/java/org/apache/commons/jexl2/ArithmeticTest.java?rev=1153476&r1=1153475&r2=1153476&view=diff
==============================================================================
---
commons/proper/jexl/trunk/src/test/java/org/apache/commons/jexl2/ArithmeticTest.java
(original)
+++
commons/proper/jexl/trunk/src/test/java/org/apache/commons/jexl2/ArithmeticTest.java
Wed Aug 3 13:17:44 2011
@@ -19,11 +19,14 @@ package org.apache.commons.jexl2;
import java.util.Map;
import java.math.BigDecimal;
import java.math.BigInteger;
+import java.util.ArrayList;
import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
import org.apache.commons.jexl2.junit.Asserter;
-
public class ArithmeticTest extends JexlTestCase {
private Asserter asserter;
@@ -39,7 +42,7 @@ public class ArithmeticTest extends Jexl
public void testUndefinedVar() throws Exception {
asserter.failExpression("objects[1].status", ".* undefined variable
objects.*");
}
-
+
public void testLeftNullOperand() throws Exception {
asserter.setVariable("left", null);
asserter.setVariable("right", Integer.valueOf(8));
@@ -177,18 +180,18 @@ public class ArithmeticTest extends Jexl
public void testCoercions() throws Exception {
asserter.assertExpression("1", new Integer(1)); // numerics default to
Integer
// asserter.assertExpression("5L", new Long(5)); // TODO when
implemented
-
+
asserter.setVariable("I2", new Integer(2));
asserter.setVariable("L2", new Long(2));
asserter.setVariable("L3", new Long(3));
asserter.setVariable("B10", BigInteger.TEN);
-
+
// Integer & Integer => Integer
asserter.assertExpression("I2 + 2", new Integer(4));
asserter.assertExpression("I2 * 2", new Integer(4));
asserter.assertExpression("I2 - 2", new Integer(0));
asserter.assertExpression("I2 / 2", new Integer(1));
-
+
// Integer & Long => Long
asserter.assertExpression("I2 * L2", new Long(4));
asserter.assertExpression("I2 / L2", new Long(1));
@@ -198,13 +201,27 @@ public class ArithmeticTest extends Jexl
asserter.assertExpression("L2 + L3", new Long(5));
asserter.assertExpression("L2 / L2", new Long(1));
asserter.assertExpression("L2 / 2", new Long(1));
-
+
// BigInteger
asserter.assertExpression("B10 / 10", BigInteger.ONE);
asserter.assertExpression("B10 / I2", new BigInteger("5"));
asserter.assertExpression("B10 / L2", new BigInteger("5"));
}
+ public static class MatchingContainer {
+ private final Set<Integer> values;
+
+ public MatchingContainer(int[] is) {
+ values = new HashSet<Integer>();
+ for (int value : is) {
+ values.add(value);
+ }
+ }
+
+ public boolean contains(int value) {
+ return values.contains(value);
+ }
+ }
public void testRegexp() throws Exception {
asserter.setVariable("str", "abc456");
@@ -222,6 +239,36 @@ public class ArithmeticTest extends Jexl
asserter.assertExpression("str !~ match", Boolean.FALSE);
asserter.assertExpression("str !~ nomatch", Boolean.TRUE);
asserter.assertExpression("str =~ nomatch", Boolean.FALSE);
+ // check the in/not-in variant
+ asserter.assertExpression("'a' =~ ['a','b','c','d','e','f']",
Boolean.TRUE);
+ asserter.assertExpression("'a' !~ ['a','b','c','d','e','f']",
Boolean.FALSE);
+ asserter.assertExpression("'z' =~ ['a','b','c','d','e','f']",
Boolean.FALSE);
+ asserter.assertExpression("'z' !~ ['a','b','c','d','e','f']",
Boolean.TRUE);
+ // check in/not-in on array, list, map, set and duck-type collection
+ int[] ai = {2, 4, 42, 54};
+ List<Integer> al = new ArrayList<Integer>();
+ for(int i : ai) {
+ al.add(i);
+ }
+ Map<Integer, String> am = new HashMap<Integer, String>();
+ am.put(2, "two");
+ am.put(4, "four");
+ am.put(42, "forty-two");
+ am.put(54, "fifty-four");
+ MatchingContainer ad = new MatchingContainer(ai);
+ Set<Integer> as = ad.values;
+ Object[] vars = { ai, al, am, ad, as };
+
+ for(Object var : vars) {
+ asserter.setVariable("container", var);
+ for(int x : ai) {
+ asserter.setVariable("x", x);
+ asserter.assertExpression("x =~ container", Boolean.TRUE);
+ }
+ asserter.setVariable("x", 169);
+ asserter.assertExpression("x !~ container", Boolean.TRUE);
+ }
+
}
/**
@@ -231,7 +278,7 @@ public class ArithmeticTest extends Jexl
* @throws Exception
*/
public void testDivideByZero() throws Exception {
- Map<String,Object> vars = new HashMap<String,Object>();
+ Map<String, Object> vars = new HashMap<String, Object>();
JexlContext context = new MapContext(vars);
vars.put("aByte", new Byte((byte) 1));
vars.put("aShort", new Short((short) 2));
@@ -280,11 +327,11 @@ public class ArithmeticTest extends Jexl
// check we have a zero & incremement zero count
if (nan instanceof Number) {
double zero = ((Number) nan).doubleValue();
- if (zero == 0.0)
+ if (zero == 0.0) {
zeval += 1;
+ }
}
- }
- catch (Exception any) {
+ } catch (Exception any) {
// increment the exception count
zthrow += 1;
}
@@ -293,8 +340,7 @@ public class ArithmeticTest extends Jexl
if (!jexl.isLenient()) {
assertTrue("All expressions should have thrown " + zthrow +
"/" + PERMS,
zthrow == PERMS);
- }
- else {
+ } else {
assertTrue("All expressions should have zeroed " + zeval + "/"
+ PERMS,
zeval == PERMS);
}