Skip to content

Commit 3c5f57f

Browse files
committed
Merge pull request #177 from zline/master
Fix for logical operator priorities and issue #175
2 parents 51379c6 + 0d235b9 commit 3c5f57f

File tree

8 files changed

+155
-68
lines changed

8 files changed

+155
-68
lines changed

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,25 @@ public char nextSignificantChar(int startPosition) {
198198
}
199199
}
200200

201+
public void readSignificantChar(char c) {
202+
if (skipBlanks().currentChar() != c) {
203+
throw new InvalidPathException(String.format("Expected character: %c", c));
204+
}
205+
incrementPosition(1);
206+
}
207+
208+
public void readSignificantSubSequence(CharSequence s) {
209+
skipBlanks();
210+
if (! inBounds(position + s.length() - 1)) {
211+
throw new InvalidPathException(String.format("End of string reached while expecting: %s", s));
212+
}
213+
if (! subSequence(position, position + s.length()).equals(s)) {
214+
throw new InvalidPathException(String.format("Expected: %s", s));
215+
}
216+
217+
incrementPosition(s.length());
218+
}
219+
201220
public int indexOfPreviousSignificantChar(int startPosition){
202221
int readPosition = startPosition - 1;
203222
while (!isOutOfBounds(readPosition) && charAt(readPosition) == SPACE) {

json-path/src/main/java/com/jayway/jsonpath/internal/filter/EvaluatorFactory.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,9 @@ public boolean evaluate(ValueNode left, ValueNode right, Predicate.PredicateCont
138138
private static class SizeEvaluator implements Evaluator {
139139
@Override
140140
public boolean evaluate(ValueNode left, ValueNode right, Predicate.PredicateContext ctx) {
141+
if (! right.isNumberNode()) {
142+
return false;
143+
}
141144
int expectedSize = right.asNumberNode().getNumber().intValue();
142145

143146
if(left.isStringNode()){

json-path/src/main/java/com/jayway/jsonpath/internal/filter/FilterCompiler.java

Lines changed: 85 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
import org.slf4j.Logger;
88
import org.slf4j.LoggerFactory;
99

10-
import java.util.Stack;
10+
import java.util.ArrayList;
11+
import java.util.List;
1112

1213
public class FilterCompiler {
1314
private static final Logger logger = LoggerFactory.getLogger(FilterCompiler.class);
@@ -71,64 +72,14 @@ private FilterCompiler(String filterString) {
7172

7273
public Predicate compile() {
7374
try {
74-
Stack<LogicalOperator> opsStack = new Stack<LogicalOperator>();
75-
Stack<ExpressionNode> expStack = new Stack<ExpressionNode>();
76-
77-
int unbalancedParenthesis = 0;
78-
79-
while (filter.skipBlanks().inBounds()) {
80-
int pos = filter.position();
81-
82-
switch (filter.currentChar()) {
83-
case OPEN_PARENTHESIS:
84-
unbalancedParenthesis++;
85-
filter.incrementPosition(1);
86-
break;
87-
case CLOSE_PARENTHESIS:
88-
unbalancedParenthesis--;
89-
filter.incrementPosition(1);
90-
ExpressionNode expressionNode = expStack.pop();
91-
if (!opsStack.isEmpty()) {
92-
if (expStack.isEmpty()) {
93-
throw new InvalidPathException("Expected expression on right hand side of operator " + opsStack.peek().getOperatorString() + " in filter " + filter);
94-
}
95-
ExpressionNode right = expStack.pop();
96-
expressionNode = ExpressionNode.createExpressionNode(expressionNode, opsStack.pop(), right);
97-
while (!opsStack.isEmpty()) {
98-
expressionNode = ExpressionNode.createExpressionNode(expressionNode, opsStack.pop(), expStack.pop());
99-
}
100-
}
101-
expStack.push(expressionNode);
102-
break;
103-
case NOT:
104-
filter.incrementPosition(1);
105-
break;
106-
case OR:
107-
case AND:
108-
LogicalOperator operatorNode = readLogicalOperator();
109-
opsStack.push(operatorNode);
110-
break;
111-
default:
112-
if(expStack.size() > 0 && opsStack.isEmpty()){
113-
throw new InvalidPathException("Expected logical operator (&&, ||) to follow expression " + expStack.peek().toString());
114-
}
115-
RelationalExpressionNode relationalExpressionNode = readExpression();
116-
expStack.push(relationalExpressionNode);
117-
break;
118-
}
119-
if (pos >= filter.position()) {
120-
throw new InvalidPathException("Failed to parse filter " + filter.toString());
121-
}
122-
}
123-
if (unbalancedParenthesis != 0) {
124-
throw new InvalidPathException("Failed to parse filter. Parenthesis are not balanced. " + filter.toString());
125-
}
126-
127-
Predicate predicate = expStack.pop();
128-
129-
if (logger.isTraceEnabled()) logger.trace(predicate.toString());
130-
131-
return predicate;
75+
final ExpressionNode result = readLogicalOR();
76+
filter.skipBlanks();
77+
if (filter.inBounds()) {
78+
throw new InvalidPathException(String.format("Expected end of filter expression instead of: %s",
79+
filter.subSequence(filter.position(), filter.length())));
80+
}
81+
82+
return result;
13283
} catch (InvalidPathException e){
13384
throw e;
13485
} catch (Exception e) {
@@ -140,6 +91,13 @@ private ValueNode readValueNode() {
14091
switch (filter.skipBlanks().currentChar()) {
14192
case DOC_CONTEXT : return readPath();
14293
case EVAL_CONTEXT : return readPath();
94+
case NOT:
95+
filter.incrementPosition(1);
96+
switch (filter.skipBlanks().currentChar()) {
97+
case DOC_CONTEXT : return readPath();
98+
case EVAL_CONTEXT : return readPath();
99+
default: throw new InvalidPathException(String.format("Unexpected character: %c", NOT));
100+
}
143101
default : return readLiteral();
144102
}
145103
}
@@ -159,19 +117,80 @@ private ValueNode readLiteral(){
159117
}
160118
}
161119

120+
/*
121+
* LogicalOR = LogicalAND { '||' LogicalAND }
122+
* LogicalAND = LogicalANDOperand { '&&' LogicalANDOperand }
123+
* LogicalANDOperand = RelationalExpression | '(' LogicalOR ')'
124+
* RelationalExpression = Value [ RelationalOperator Value ]
125+
*/
126+
127+
private ExpressionNode readLogicalOR() {
128+
final List<ExpressionNode> ops = new ArrayList<ExpressionNode>();
129+
ops.add(readLogicalAND());
130+
131+
while (true) {
132+
int savepoint = filter.position();
133+
try {
134+
filter.readSignificantSubSequence(LogicalOperator.OR.getOperatorString());
135+
ops.add(readLogicalAND());
136+
}
137+
catch (InvalidPathException exc) {
138+
filter.setPosition(savepoint);
139+
break;
140+
}
141+
}
142+
143+
return 1 == ops.size() ? ops.get(0) : LogicalExpressionNode.createLogicalOr(ops);
144+
}
145+
146+
private ExpressionNode readLogicalAND() {
147+
/// @fixme copy-pasted
148+
final List<ExpressionNode> ops = new ArrayList<ExpressionNode>();
149+
ops.add(readLogicalANDOperand());
150+
151+
while (true) {
152+
int savepoint = filter.position();
153+
try {
154+
filter.readSignificantSubSequence(LogicalOperator.AND.getOperatorString());
155+
ops.add(readLogicalANDOperand());
156+
}
157+
catch (InvalidPathException exc) {
158+
filter.setPosition(savepoint);
159+
break;
160+
}
161+
}
162+
163+
return 1 == ops.size() ? ops.get(0) : LogicalExpressionNode.createLogicalAnd(ops);
164+
}
165+
166+
private ExpressionNode readLogicalANDOperand() {
167+
if (filter.skipBlanks().currentCharIs(OPEN_PARENTHESIS)) {
168+
filter.readSignificantChar(OPEN_PARENTHESIS);
169+
final ExpressionNode op = readLogicalOR();
170+
filter.readSignificantChar(CLOSE_PARENTHESIS);
171+
return op;
172+
}
173+
174+
return readExpression();
175+
}
176+
162177
private RelationalExpressionNode readExpression() {
163178
ValueNode left = readValueNode();
164-
if(expressionIsTerminated()) {
165-
ValueNode.PathNode pathNode = left.asPathNode();
166-
left = pathNode.asExistsCheck(pathNode.shouldExists());
167-
RelationalOperator operator = RelationalOperator.EXISTS;
168-
ValueNode right = left.asPathNode().shouldExists() ? ValueNode.TRUE : ValueNode.FALSE;
169-
return new RelationalExpressionNode(left, operator, right);
170-
} else {
179+
int savepoint = filter.position();
180+
try {
171181
RelationalOperator operator = readRelationalOperator();
172182
ValueNode right = readValueNode();
173183
return new RelationalExpressionNode(left, operator, right);
174184
}
185+
catch (InvalidPathException exc) {
186+
filter.setPosition(savepoint);
187+
}
188+
189+
ValueNode.PathNode pathNode = left.asPathNode();
190+
left = pathNode.asExistsCheck(pathNode.shouldExists());
191+
RelationalOperator operator = RelationalOperator.EXISTS;
192+
ValueNode right = left.asPathNode().shouldExists() ? ValueNode.TRUE : ValueNode.FALSE;
193+
return new RelationalExpressionNode(left, operator, right);
175194
}
176195

177196
private LogicalOperator readLogicalOperator(){

json-path/src/main/java/com/jayway/jsonpath/internal/filter/LogicalExpressionNode.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.jayway.jsonpath.internal.Utils;
44

55
import java.util.ArrayList;
6+
import java.util.Collection;
67
import java.util.List;
78

89
public class LogicalExpressionNode extends ExpressionNode {
@@ -13,16 +14,29 @@ public static LogicalExpressionNode createLogicalOr(ExpressionNode left,Expressi
1314
return new LogicalExpressionNode(left, LogicalOperator.OR, right);
1415
}
1516

17+
public static LogicalExpressionNode createLogicalOr(Collection<ExpressionNode> operands){
18+
return new LogicalExpressionNode(LogicalOperator.OR, operands);
19+
}
20+
1621
public static LogicalExpressionNode createLogicalAnd(ExpressionNode left,ExpressionNode right){
1722
return new LogicalExpressionNode(left, LogicalOperator.AND, right);
1823
}
1924

25+
public static LogicalExpressionNode createLogicalAnd(Collection<ExpressionNode> operands){
26+
return new LogicalExpressionNode(LogicalOperator.AND, operands);
27+
}
28+
2029
private LogicalExpressionNode(ExpressionNode left, LogicalOperator operator, ExpressionNode right) {
2130
chain.add(left);
2231
chain.add(right);
2332
this.operator = operator;
2433
}
2534

35+
private LogicalExpressionNode(LogicalOperator operator, Collection<ExpressionNode> operands) {
36+
chain.addAll(operands);
37+
this.operator = operator;
38+
}
39+
2640
public LogicalExpressionNode and(LogicalExpressionNode other){
2741
return createLogicalAnd(this, other);
2842
}

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,11 @@ public boolean accept(final Object obj, final Object root, final Configuration c
7272
Predicate.PredicateContext ctx = new PredicateContextImpl(obj, root, configuration, evaluationContext.documentEvalCache());
7373

7474
for (Predicate predicate : predicates) {
75-
if (!predicate.apply (ctx)) {
75+
try {
76+
if (!predicate.apply(ctx)) {
77+
return false;
78+
}
79+
} catch (InvalidPathException e) {
7680
return false;
7781
}
7882
}

json-path/src/test/java/com/jayway/jsonpath/FilterCompilerTest.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public void valid_filters_compile() {
2929
assertThat(compile("[?((@.firstname || @.lastname) && @.and)]").toString()).isEqualTo("[?((@['firstname'] || @['lastname']) && @['and'])]");
3030
assertThat(compile("[?((@.a || @.b || @.c) && @.x)]").toString()).isEqualTo("[?((@['a'] || @['b'] || @['c']) && @['x'])]");
3131
assertThat(compile("[?((@.a && @.b && @.c) || @.x)]").toString()).isEqualTo("[?((@['a'] && @['b'] && @['c']) || @['x'])]");
32-
assertThat(compile("[?((@.a && @.b || @.c) || @.x)]").toString()).isEqualTo("[?((@['a'] && (@['b'] || @['c'])) || @['x'])]");
32+
assertThat(compile("[?((@.a && @.b || @.c) || @.x)]").toString()).isEqualTo("[?(((@['a'] && @['b']) || @['c']) || @['x'])]");
3333
assertThat(compile("[?((@.a && @.b) || (@.c && @.d))]").toString()).isEqualTo("[?((@['a'] && @['b']) || (@['c'] && @['d']))]");
3434
assertThat(compile("[?(@.a IN [1,2,3])]").toString()).isEqualTo("[?(@['a'] IN [1,2,3])]");
3535
assertThat(compile("[?(@.a IN {'foo':'bar'})]").toString()).isEqualTo("[?(@['a'] IN {'foo':'bar'})]");
@@ -44,6 +44,7 @@ public void valid_filters_compile() {
4444
assertThat(compile("[?($[\"firstname\"][\"lastname\"])]").toString()).isEqualTo("[?($[\"firstname\"][\"lastname\"])]");
4545
assertThat(compile("[?($[\"firstname\"].lastname)]").toString()).isEqualTo("[?($[\"firstname\"]['lastname'])]");
4646
assertThat(compile("[?($[\"firstname\", \"lastname\"])]").toString()).isEqualTo("[?($[\"firstname\",\"lastname\"])]");
47+
assertThat(compile("[?(((@.a && @.b || @.c)) || @.x)]").toString()).isEqualTo("[?(((@['a'] && @['b']) || @['c']) || @['x'])]");
4748

4849
}
4950

@@ -64,6 +65,12 @@ public void invalid_path_when_string_literal_is_unquoted() {
6465
compile("[?(@.foo == x)]");
6566
}
6667

68+
@Test
69+
public void or_has_lower_priority_than_and() {
70+
assertThat(compile("[?(@.category == 'fiction' && @.author == 'Evelyn Waugh' || @.price > 15)]").toString())
71+
.isEqualTo("[?((@['category'] == 'fiction' && @['author'] == 'Evelyn Waugh') || @['price'] > 15)]");
72+
}
73+
6774
@Test
6875
public void invalid_filters_does_not_compile() {
6976
assertInvalidPathException("[?(@))]");
@@ -73,6 +80,8 @@ public void invalid_filters_does_not_compile() {
7380
assertInvalidPathException("[?(@ == 1' )]");
7481
assertInvalidPathException("[?(@.foo bar == 1)]");
7582
assertInvalidPathException("[?(@.i == 5 @.i == 8)]");
83+
assertInvalidPathException("[?(!5)]");
84+
assertInvalidPathException("[?(!'foo')]");
7685
}
7786

7887

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,4 +209,9 @@ public void escaped_literals2() {
209209
public void escape_pattern() {
210210
assertHasOneResult("[\"x\"]", "$[?(@ =~ /\\/|x/)]", conf);
211211
}
212+
213+
@Test
214+
public void filter_evaluation_does_not_break_path_evaluation() {
215+
assertHasOneResult("[{\"s\": \"fo\", \"expected_size\": \"m\"}, {\"s\": \"lo\", \"expected_size\": 2}]", "$[?(@.s size @.expected_size)]", conf);
216+
}
212217
}

json-path/src/test/java/com/jayway/jsonpath/PathCompilerTest.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,20 @@ public void issue_predicate_brackets_must_change_priorities() {
222222
assertThat(result).isEmpty();
223223
}
224224

225+
@Test
226+
public void issue_predicate_or_has_lower_priority_than_and() {
227+
String json = "{\n"
228+
+ " \"logs\": [\n"
229+
+ " {\n"
230+
+ " \"id\": 2\n"
231+
+ " }\n"
232+
+ " ]\n"
233+
+ "}";
234+
235+
List<String> result = JsonPath.read(json, "$.logs[?(@.x && @.y || @.id)]");
236+
assertThat(result).hasSize(1);
237+
}
238+
225239
@Test
226240
public void issue_predicate_can_have_square_bracket_in_prop() {
227241
String json = "{\n"

0 commit comments

Comments
 (0)