Browse Source

Support OR statements in inline filters.

pull/57/head
Kalle Stenflo 10 years ago
parent
commit
0b530378aa
  1. 1
      changelog.md
  2. 80
      json-path/src/main/java/com/jayway/jsonpath/Criteria.java
  3. 173
      json-path/src/main/java/com/jayway/jsonpath/Filter.java
  4. 201
      json-path/src/main/java/com/jayway/jsonpath/internal/PathCompiler.java
  5. 35
      json-path/src/test/java/com/jayway/jsonpath/FilterTest.java
  6. 29
      json-path/src/test/java/com/jayway/jsonpath/InlineFilterTest.java
  7. 2
      json-path/src/test/java/com/jayway/jsonpath/old/IssuesTest.java

1
changelog.md

@ -3,6 +3,7 @@ In The Pipe
* Added EvaluationListener interface that allows abortion of evaluation if criteria is fulfilled.
this makes it possible to limit the number of results to fetch when a document is scanned. Also
added utility method to limit results `JsonPath.parse(json).limit(1).read("$..title", List.class);`
* Added support for OR in inline filters [?(@.foo == 'bar' || @.foo == 'baz')]
1.1.0 (2014-10-01)

80
json-path/src/main/java/com/jayway/jsonpath/Criteria.java

@ -39,6 +39,15 @@ public class Criteria implements Predicate {
private static final Logger logger = LoggerFactory.getLogger(Criteria.class);
private static final String[] OPERATORS = {
CriteriaType.EQ.toString(),
CriteriaType.GTE.toString(),
CriteriaType.LTE.toString(),
CriteriaType.NE.toString(),
CriteriaType.LT.toString(),
CriteriaType.GT.toString()
};
private final Path path;
private CriteriaType criteriaType;
private Object expected;
@ -53,6 +62,11 @@ public class Criteria implements Predicate {
if(logger.isDebugEnabled()) logger.debug("[{}] {} [{}] => {}", actual, name(), expected, res);
return res;
}
@Override
public String toString() {
return "==";
}
},
NE {
@Override
@ -61,6 +75,11 @@ public class Criteria implements Predicate {
if(logger.isDebugEnabled()) logger.debug("[{}] {} [{}] => {}", actual, name(), expected, res);
return res;
}
@Override
public String toString() {
return "!=";
}
},
GT {
@Override
@ -72,6 +91,10 @@ public class Criteria implements Predicate {
if(logger.isDebugEnabled()) logger.debug("[{}] {} [{}] => {}", actual, name(), expected, res);
return res;
}
@Override
public String toString() {
return ">";
}
},
GTE {
@Override
@ -83,6 +106,10 @@ public class Criteria implements Predicate {
if(logger.isDebugEnabled()) logger.debug("[{}] {} [{}] => {}", actual, name(), expected, res);
return res;
}
@Override
public String toString() {
return ">=";
}
},
LT {
@Override
@ -94,6 +121,10 @@ public class Criteria implements Predicate {
if(logger.isDebugEnabled()) logger.debug("[{}] {} [{}] => {}", actual, name(), expected, res);
return res;
}
@Override
public String toString() {
return "<";
}
},
LTE {
@Override
@ -105,6 +136,10 @@ public class Criteria implements Predicate {
if(logger.isDebugEnabled()) logger.debug("[{}] {} [{}] => {}", actual, name(), expected, res);
return res;
}
@Override
public String toString() {
return "<=";
}
},
IN {
@Override
@ -665,11 +700,46 @@ public class Criteria implements Predicate {
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(path.toString())
.append("|")
.append(criteriaType.name())
.append("|")
.append(expected)
.append("|");
//.append("|")
.append(criteriaType.toString())
//.append("|")
.append(wrapString(expected));
//.append("|");
return sb.toString();
}
private String wrapString(Object o){
if(o == null){
return "null";
}
if(o instanceof String){
return "'" + o.toString() + "'";
} else {
return o.toString();
}
}
public static Criteria parse(String criteria){
int operatorIndex = -1;
String left = "";
String operator = "";
String right = "";
for (int y = 0; y < OPERATORS.length; y++) {
operatorIndex = criteria.indexOf(OPERATORS[y]);
if (operatorIndex != -1) {
operator = OPERATORS[y];
break;
}
}
if (!operator.isEmpty()) {
left = criteria.substring(0, operatorIndex).trim();
right = criteria.substring(operatorIndex + operator.length()).trim();
} else {
left = criteria.trim();
}
return Criteria.create(left, operator, right);
}
}

173
json-path/src/main/java/com/jayway/jsonpath/Filter.java

@ -14,31 +14,17 @@
*/
package com.jayway.jsonpath;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Stack;
import java.util.regex.Pattern;
/**
*
*/
public class Filter implements Predicate {
protected final List<Predicate> criteriaList;
private Filter() {
criteriaList = Collections.emptyList();
}
private Filter(Predicate criteria) {
criteriaList = Collections.singletonList(criteria);
}
private Filter(List<Predicate> criteriaList) {
this.criteriaList = new ArrayList<Predicate>(criteriaList);
}
public abstract class Filter implements Predicate {
private static final Pattern OPERATOR_SPLIT = Pattern.compile("((?<=&&|\\|\\|)|(?=&&|\\|\\|))");
private static final String AND = "&&";
private static final String OR = "||";
/**
* Creates a new Filter based on given criteria
@ -46,45 +32,63 @@ public class Filter implements Predicate {
* @return a new Filter
*/
public static Filter filter(Predicate criteria) {
return new Filter(criteria);
return new SingleFilter(criteria);
}
/**
* Create a new Filter based on given list of criteria.
* @param criteriaList list of criteria
* @return
*/
public static Filter filter(List<Predicate> criteriaList) {
return new Filter(criteriaList);
}
@Override
public boolean apply(PredicateContext ctx) {
for (Predicate criteria : criteriaList) {
if (!criteria.apply(ctx)) {
return false;
}
}
return true;
}
public abstract boolean apply(PredicateContext ctx);
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (Predicate crit : criteriaList) {
sb.append(crit.toString());
}
return sb.toString();
}
public Filter or(final Predicate other){
return new OrFilter(this, other);
}
public Filter and(final Predicate other){
return filter(Arrays.asList(this, other));
return new AndFilter(this, other);
}
private static final class SingleFilter extends Filter {
private final Predicate predicate;
private SingleFilter(Predicate predicate) {
this.predicate = predicate;
}
@Override
public boolean apply(PredicateContext ctx) {
return predicate.apply(ctx);
}
@Override
public String toString() {
return predicate.toString();
}
}
private static final class AndFilter extends Filter {
private final Predicate left;
private final Predicate right;
private AndFilter(Predicate left, Predicate right) {
this.left = left;
this.right = right;
}
@Override
public boolean apply(PredicateContext ctx) {
boolean a = left.apply(ctx);
return a && right.apply(ctx);
}
@Override
public String toString() {
return left.toString() + " && " + right.toString();
}
}
private static final class OrFilter extends Filter {
private final Predicate left;
@ -94,11 +98,84 @@ public class Filter implements Predicate {
this.left = left;
this.right = right;
}
public Filter and(final Predicate other){
return new OrFilter(left, new AndFilter(right, other));
}
@Override
public boolean apply(PredicateContext ctx) {
boolean a = left.apply(ctx);
return a || right.apply(ctx);
}
@Override
public String toString() {
return left.toString() + " || " + right.toString();
}
}
public static Filter parse(String filter){
filter = filter.trim();
if(!filter.startsWith("[") || !filter.endsWith("]")){
throw new InvalidPathException("Filter must start with '[' and end with ']'. " + filter);
}
filter = filter.substring(1, filter.length()-1).trim();
if(!filter.startsWith("?")){
throw new InvalidPathException("Filter must start with '[?' and end with ']'. " + filter);
}
filter = filter.substring(1).trim();
if(!filter.startsWith("(") || !filter.endsWith(")")){
throw new InvalidPathException("Filter must start with '[?(' and end with ')]'. " + filter);
}
filter = filter.substring(1, filter.length()-1).trim();
String[] split = OPERATOR_SPLIT.split(filter);
Stack<String> operators = new Stack<String>();
Stack<Criteria> criteria = new Stack<Criteria>();
for (String exp : split) {
exp = exp.trim();
if(AND.equals(exp) || OR.equals(exp)){
operators.push(exp);
}
else {
criteria.push(Criteria.parse(cleanCriteria(exp)));
}
}
Filter root = new SingleFilter(criteria.pop());
while(!operators.isEmpty()) {
String operator = operators.pop();
if (AND.equals(operator)) {
root = root.and(criteria.pop());
} else {
if(criteria.isEmpty()){
throw new InvalidPathException("Invalid operators " + filter);
}
root = root.or(criteria.pop());
}
}
if(!operators.isEmpty() || !criteria.isEmpty()){
throw new InvalidPathException("Invalid operators " + filter);
}
return root;
}
private static String cleanCriteria(String filter){
int begin = 0;
int end = filter.length() -1;
char c = filter.charAt(begin);
while(c == '[' || c == '?' || c == '(' || c == ' '){
c = filter.charAt(++begin);
}
c = filter.charAt(end);
while( c == ')' || c == ' '){
c = filter.charAt(--end);
}
return filter.substring(begin, end+1);
}
}

201
json-path/src/main/java/com/jayway/jsonpath/internal/PathCompiler.java

@ -14,7 +14,6 @@
*/
package com.jayway.jsonpath.internal;
import com.jayway.jsonpath.Criteria;
import com.jayway.jsonpath.Filter;
import com.jayway.jsonpath.InvalidPathException;
import com.jayway.jsonpath.Predicate;
@ -49,15 +48,12 @@ public class PathCompiler {
private static final char BRACKET_CLOSE = ']';
private static final char SPACE = ' ';
private static final Cache cache = new Cache(200);
private static final String[] OPERATORS = {"==", ">=", "<=", "!=", "<", ">",};
public static Path compile(String path, Predicate... filters) {
notEmpty(path, "Path may not be null empty");
path = path.trim();
LinkedList<Predicate> filterList = new LinkedList<Predicate>(asList(filters));
if (path.charAt(0) != '$' && path.charAt(0) != '@') {
@ -79,7 +75,7 @@ public class PathCompiler {
String cacheKey = path + isRootPath + filterList.toString();
Path p = cache.get(cacheKey);
if (p != null) {
if(logger.isDebugEnabled()) logger.debug("Using cached path");
if(logger.isDebugEnabled()) logger.debug("Using cached path: " + cacheKey);
return p;
}
@ -253,7 +249,7 @@ public class PathCompiler {
switch (current) {
case '?':
return analyzeCriteriaSequence2();
return analyzeCriteriaSequence4();
case '\'':
return analyzeProperty();
default:
@ -270,180 +266,67 @@ public class PathCompiler {
throw new InvalidPathException("Could not analyze path component: " + pathFragment);
}
private int findNext(char charToFind, boolean disregardStringContent) {
int x = i;
boolean inProperty = false;
char analyzing;
boolean found = false;
while(!found){
analyzing = pathFragment.charAt(x);
public PathToken analyzeCriteriaSequence4() {
int[] bounds = findFilterBounds();
i = bounds[1];
if(analyzing == '\'' && disregardStringContent){
inProperty = !inProperty;
}
if(!inProperty){
found = (analyzing == charToFind);
}
x++;
}
return x-1;
return new PredicatePathToken(Filter.parse(pathFragment.substring(bounds[0], bounds[1])));
}
//[?(@.foo)]
//[?(@['foo'])]
//[?(@.foo == 'bar')]
//[?(@['foo']['bar'] == 'bar')]
//[?(@ == 'bar')]
//[?(@.category == $.store.book[0].category)]
//[?(@.category == @['category'])]
private PathToken analyzeCriteriaSequence2() {
List<Predicate> predicates = new ArrayList<Predicate>();
int startPos = findNext('(', true) + 1;
int stopPos = findNext(')', true);
String[] split = pathFragment.substring(startPos, stopPos).split("&&");
int[] findFilterBounds(){
int end = 0;
int start = i;
for (String criteria : split) {
int operatorIndex = -1;
String left = "";
String operator = "";
String right = "";
for (int y = 0; y < OPERATORS.length; y++) {
operatorIndex = criteria.indexOf(OPERATORS[y]);
if (operatorIndex != -1) {
operator = OPERATORS[y];
break;
}
}
if (!operator.isEmpty()) {
left = criteria.substring(0, operatorIndex).trim();
right = criteria.substring(operatorIndex + operator.length()).trim();
} else {
left = criteria.trim();
}
predicates.add(Criteria.create(left, operator, right));
while(pathFragment.charAt(start) != '['){
start--;
}
i = stopPos;
return new PredicatePathToken(Filter.filter(predicates));
}
/*
//[?(@.foo)]
//[?(@['foo'])]
//[?(@.foo == 'bar')]
//[?(@['foo']['bar'] == 'bar')]
//[?(@ == 'bar')]
private PathToken analyzeCriteriaSequence() {
StringBuilder pathBuffer = new StringBuilder();
StringBuilder operatorBuffer = new StringBuilder();
StringBuilder valueBuffer = new StringBuilder();
List<Predicate> criteria = new ArrayList<Predicate>();
int bracketCount = 0;
boolean functionBracketOpened = false;
boolean functionBracketClosed = false;
boolean propertyOpen = false;
current = pathFragment.charAt(++i); //skip the '?'
while (current != ']' || bracketCount != 0) {
switch (current) {
case '[':
bracketCount++;
pathBuffer.append(current);
int mem = ' ';
int curr = start;
boolean inProp = false;
int openSquareBracket = 0;
int openBrackets = 0;
while(end == 0){
char c = pathFragment.charAt(curr);
switch (c){
case '(':
if(!inProp) openBrackets++;
break;
case ']':
bracketCount--;
pathBuffer.append(current);
case ')':
if(!inProp) openBrackets--;
break;
case '@':
pathBuffer.append('$');
case '[':
if(!inProp) openSquareBracket++;
break;
case '(':
if (!propertyOpen) {
functionBracketOpened = true;
break;
case ']':
if(!inProp){
openSquareBracket--;
if(openBrackets == 0){
end = curr + 1;
}
}
case ')':
if (!propertyOpen) {
functionBracketClosed = true;
break;
case '\'':
if(mem == '\\') {
break;
}
inProp = !inProp;
break;
default:
if ('\'' == current) {
if (propertyOpen) {
propertyOpen = false;
} else {
propertyOpen = true;
}
}
if (bracketCount == 0 && isOperatorChar(current)) {
operatorBuffer.append(current);
} else if (bracketCount == 0 && isLogicOperatorChar(current)) {
if (isLogicOperatorChar(pathFragment.charAt(i + 1))) {
++i;
}
criteria.add(Criteria.create(pathBuffer.toString().trim(), operatorBuffer.toString().trim(), valueBuffer.toString().trim()));
pathBuffer.setLength(0);
operatorBuffer.setLength(0);
valueBuffer.setLength(0);
} else if (operatorBuffer.length() > 0) {
valueBuffer.append(current);
} else {
pathBuffer.append(current);
}
break;
}
current = pathFragment.charAt(++i);
mem = c;
curr++;
}
if (!functionBracketOpened || !functionBracketClosed) {
throw new InvalidPathException("Function wrapping brackets are not matching. A filter function must match [?(<statement>)]");
if(openBrackets != 0 || openSquareBracket != 0){
throw new InvalidPathException("Filter brackets are not balanced");
}
criteria.add(Criteria.create(pathBuffer.toString().trim(), operatorBuffer.toString().trim(), valueBuffer.toString().trim()));
Filter filter2 = Filter.filter(criteria);
return new PredicatePathToken(filter2);
}
*/
private static boolean isAnd(char c) {
return c == '&';
return new int[]{start, end};
}
private static boolean isOr(char c) {
if (c == '|') {
throw new UnsupportedOperationException("OR operator is not supported.");
}
return false;
}
private static boolean isLogicOperatorChar(char c) {
return isAnd(c) || isOr(c);
}
private static boolean isOperatorChar(char c) {
return c == '=' || c == '!' || c == '<' || c == '>';
}
//"['foo']"
private PathToken analyzeProperty() {
@ -607,4 +490,6 @@ public class PathCompiler {
}
}
}

35
json-path/src/test/java/com/jayway/jsonpath/FilterTest.java

@ -1,5 +1,6 @@
package com.jayway.jsonpath;
import org.assertj.core.api.Assertions;
import org.junit.Test;
import java.util.HashMap;
@ -410,4 +411,38 @@ public class FilterTest extends BaseTest {
assertThat(isFoo.and(isBar).apply(createPredicateContext(model))).isFalse();
}
@Test
public void a_filter_can_be_parsed() {
Filter.parse("[?(@.foo)]");
Filter.parse("[?(@.foo == 1)]");
Filter.parse("[?(@.foo == 1 || @['bar'])]");
Filter.parse("[?(@.foo == 1 && @['bar'])]");
}
@Test
public void an_invalid_filter_can_not_be_parsed() {
try {
Filter.parse("[?(@.foo == 1)");
Assertions.fail("expected " + InvalidPathException.class.getName());
} catch (InvalidPathException ipe){}
try {
Filter.parse("[?(@.foo == 1) ||]");
Assertions.fail("expected " + InvalidPathException.class.getName());
} catch (InvalidPathException ipe){}
try {
Filter.parse("[(@.foo == 1)]");
Assertions.fail("expected " + InvalidPathException.class.getName());
} catch (InvalidPathException ipe){}
try {
Filter.parse("[?@.foo == 1)]");
Assertions.fail("expected " + InvalidPathException.class.getName());
} catch (InvalidPathException ipe){}
}
}

29
json-path/src/test/java/com/jayway/jsonpath/InlineFilterTest.java

@ -46,4 +46,33 @@ public class InlineFilterTest extends BaseTest {
//System.out.println(read);
}
@Test
public void simple_inline_or_statement_evaluates() {
List a = reader.read("store.book[ ?(@.author == 'Nigel Rees' || @.author == 'Evelyn Waugh') ].author", List.class);
assertThat(a).containsExactly("Nigel Rees", "Evelyn Waugh");
List b = reader.read("store.book[ ?(@.author == 'Nigel Rees' || @.author == 'Evelyn Waugh' && @.category == 'fiction') ].author", List.class);
assertThat(b).containsExactly("Nigel Rees", "Evelyn Waugh");
List c = reader.read("store.book[ ?(@.author == 'Nigel Rees' || @.author == 'Evelyn Waugh' && @.category == 'xxx') ].author", List.class);
assertThat(c).containsExactly("Nigel Rees");
List d = reader.read("store.book[ ?((@.author == 'Nigel Rees') || (@.author == 'Evelyn Waugh' && @.category == 'xxx')) ].author", List.class);
assertThat(d).containsExactly("Nigel Rees");
List e = reader.read("$.store.book[?(@.category == 'fiction' && @.author == 'Evelyn Waugh' || @.display-price == 8.95 )].author", List.class);
assertThat(e).containsOnly("Evelyn Waugh", "Nigel Rees");
List f = reader.read("$.store.book[?(@.display-price == 8.95 || @.category == 'fiction' && @.author == 'Evelyn Waugh')].author", List.class);
assertThat(f).containsOnly("Evelyn Waugh", "Nigel Rees");
List g = reader.read("$.store.book[?(@.display-price == 8.95 || @.display-price == 8.99 || @.display-price == 22.99 )].author", List.class);
assertThat(g).containsOnly("Nigel Rees", "Herman Melville", "J. R. R. Tolkien");
List h = reader.read("$.store.book[?(@.display-price == 8.95 || @.display-price == 8.99 || (@.display-price == 22.99 && @.category == 'reference') )].author", List.class);
assertThat(h).containsOnly("Nigel Rees", "Herman Melville");
}
}

2
json-path/src/test/java/com/jayway/jsonpath/old/IssuesTest.java

@ -126,7 +126,7 @@ public class IssuesTest {
is.close();
} catch (Exception e) {
e.printStackTrace();
//e.printStackTrace();
Utils.closeQuietly(is);
}

Loading…
Cancel
Save