Skip to content

Commit

Permalink
Allow document references in inline filters.
Browse files Browse the repository at this point in the history
"store.book[?(@.display-price <= $.max-price)].display-price"
  • Loading branch information
kallestenflo committed Sep 18, 2014
1 parent 8fb322a commit 571654f
Show file tree
Hide file tree
Showing 19 changed files with 302 additions and 120 deletions.
90 changes: 51 additions & 39 deletions json-path/src/main/java/com/jayway/jsonpath/Criteria.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import com.jayway.jsonpath.internal.Path;
import com.jayway.jsonpath.internal.PathCompiler;

import com.jayway.jsonpath.internal.compiler.PredicateContextImpl;
import com.sun.tools.doclets.formats.html.SourceToHTMLConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -34,23 +36,23 @@ public class Criteria implements Predicate {
private static enum CriteriaType {
EQ {
@Override
boolean eval(Object expected, Object actual, Configuration configuration) {
boolean eval(Object expected, Object actual, PredicateContext ctx) {
boolean res = (0 == safeCompare(expected, actual));
logger.debug("[{}] {} [{}] => {}", actual, name(), expected, res);
return res;
}
},
NE {
@Override
boolean eval(Object expected, Object actual, Configuration configuration) {
boolean eval(Object expected, Object actual, PredicateContext ctx) {
boolean res = (0 != safeCompare(expected, actual));
logger.debug("[{}] {} [{}] => {}", actual, name(), expected, res);
return res;
}
},
GT {
@Override
boolean eval(Object expected, Object actual, Configuration configuration) {
boolean eval(Object expected, Object actual, PredicateContext ctx) {
if ((expected == null) ^ (actual == null)) {
return false;
}
Expand All @@ -61,7 +63,7 @@ boolean eval(Object expected, Object actual, Configuration configuration) {
},
GTE {
@Override
boolean eval(Object expected, Object actual, Configuration configuration) {
boolean eval(Object expected, Object actual, PredicateContext ctx) {
if ((expected == null) ^ (actual == null)) {
return false;
}
Expand All @@ -72,7 +74,7 @@ boolean eval(Object expected, Object actual, Configuration configuration) {
},
LT {
@Override
boolean eval(Object expected, Object actual, Configuration configuration) {
boolean eval(Object expected, Object actual, PredicateContext ctx) {
if ((expected == null) ^ (actual == null)) {
return false;
}
Expand All @@ -83,7 +85,7 @@ boolean eval(Object expected, Object actual, Configuration configuration) {
},
LTE {
@Override
boolean eval(Object expected, Object actual, Configuration configuration) {
boolean eval(Object expected, Object actual, PredicateContext ctx) {
if ((expected == null) ^ (actual == null)) {
return false;
}
Expand All @@ -94,7 +96,7 @@ boolean eval(Object expected, Object actual, Configuration configuration) {
},
IN {
@Override
boolean eval(Object expected, Object actual, Configuration configuration) {
boolean eval(Object expected, Object actual, PredicateContext ctx) {
boolean res = false;
Collection exps = (Collection) expected;
for (Object exp : exps) {
Expand All @@ -109,7 +111,7 @@ boolean eval(Object expected, Object actual, Configuration configuration) {
},
NIN {
@Override
boolean eval(Object expected, Object actual, Configuration configuration) {
boolean eval(Object expected, Object actual, PredicateContext ctx) {
Collection nexps = (Collection) expected;
boolean res = !nexps.contains(actual);
logger.debug("[{}] {} [{}] => {}", actual, name(), join(", ", nexps), res);
Expand All @@ -118,13 +120,13 @@ boolean eval(Object expected, Object actual, Configuration configuration) {
},
ALL {
@Override
boolean eval(Object expected, Object actual, Configuration configuration) {
boolean eval(Object expected, Object actual, PredicateContext ctx) {
boolean res = true;
Collection exps = (Collection) expected;
if (configuration.jsonProvider().isArray(actual)) {
if (ctx.configuration().jsonProvider().isArray(actual)) {
for (Object exp : exps) {
boolean found = false;
for (Object check : configuration.jsonProvider().toIterable(actual)) {
for (Object check : ctx.configuration().jsonProvider().toIterable(actual)) {
if (0 == safeCompare(exp, check)) {
found = true;
break;
Expand All @@ -135,7 +137,7 @@ boolean eval(Object expected, Object actual, Configuration configuration) {
break;
}
}
logger.debug("[{}] {} [{}] => {}", join(", ", configuration.jsonProvider().toIterable(actual)), name(), join(", ", exps), res);
logger.debug("[{}] {} [{}] => {}", join(", ", ctx.configuration().jsonProvider().toIterable(actual)), name(), join(", ", exps), res);
} else {
res = false;
logger.debug("[{}] {} [{}] => {}", "<NOT AN ARRAY>", name(), join(", ", exps), res);
Expand All @@ -145,11 +147,11 @@ boolean eval(Object expected, Object actual, Configuration configuration) {
},
SIZE {
@Override
boolean eval(Object expected, Object actual, Configuration configuration) {
boolean eval(Object expected, Object actual, PredicateContext ctx) {
int size = (Integer) expected;
boolean res;
if (configuration.jsonProvider().isArray(actual)) {
int length = configuration.jsonProvider().length(actual);
if (ctx.configuration().jsonProvider().isArray(actual)) {
int length = ctx.configuration().jsonProvider().length(actual);
res = (length == size);
logger.debug("Array with size {} {} {} => {}", length, name(), size, res);
} else if (actual instanceof String) {
Expand All @@ -165,14 +167,14 @@ boolean eval(Object expected, Object actual, Configuration configuration) {
},
EXISTS {
@Override
boolean eval(Object expected, Object actual, Configuration configuration) {
boolean eval(Object expected, Object actual, PredicateContext ctx) {
//This must be handled outside
throw new UnsupportedOperationException();
}
},
TYPE {
@Override
boolean eval(Object expected, Object actual, Configuration configuration) {
boolean eval(Object expected, Object actual, PredicateContext ctx) {
final Class<?> expType = (Class<?>) expected;
final Class<?> actType = actual == null ? null : actual.getClass();

Expand All @@ -181,7 +183,7 @@ boolean eval(Object expected, Object actual, Configuration configuration) {
},
REGEX {
@Override
boolean eval(Object expected, Object actual, Configuration configuration) {
boolean eval(Object expected, Object actual, PredicateContext ctx) {
boolean res = false;
final Pattern pattern = (Pattern) expected;
if (actual != null && actual instanceof String) {
Expand All @@ -193,28 +195,18 @@ boolean eval(Object expected, Object actual, Configuration configuration) {
},
MATCHES {
@Override
boolean eval(Object expected, final Object actual, final Configuration configuration) {
boolean eval(Object expected, final Object actual, final PredicateContext ctx) {
Predicate exp = (Predicate) expected;
return exp.apply(new PredicateContext() {
@Override
public Object target() {
return actual;
}

@Override
public Configuration configuration() {
return configuration;
}
});
return exp.apply(new PredicateContextImpl(actual, ctx.rootDocument(), ctx.configuration()));
}
},
NOT_EMPTY {
@Override
boolean eval(Object expected, Object actual, Configuration configuration) {
boolean eval(Object expected, Object actual, PredicateContext ctx) {
boolean res = false;
if (actual != null) {
if (configuration.jsonProvider().isArray(actual)) {
int len = configuration.jsonProvider().length(actual);
if (ctx.configuration().jsonProvider().isArray(actual)) {
int len = ctx.configuration().jsonProvider().length(actual);
res = (0 != len);
logger.debug("array length = {} {} => {}", len, name(), res);
} else if (actual instanceof String) {
Expand All @@ -227,7 +219,7 @@ boolean eval(Object expected, Object actual, Configuration configuration) {
}
};

abstract boolean eval(Object expected, Object actual, Configuration configuration);
abstract boolean eval(Object expected, Object actual, PredicateContext ctx);

public static CriteriaType parse(String str) {
if ("==".equals(str)) {
Expand Down Expand Up @@ -283,16 +275,23 @@ private boolean eval(PredicateContext ctx) {
boolean exists = ((Boolean) expected);
try {
Configuration c = Configuration.builder().jsonProvider(ctx.configuration().jsonProvider()).options().build();
path.evaluate(ctx.target(), c).getValue();
path.evaluate(ctx.contextDocument(), ctx.rootDocument(), c).getValue();
return exists;
} catch (PathNotFoundException e) {
return !exists;
}
} else {
try {
final Object actual = path.evaluate(ctx.target(), ctx.configuration()).getValue();
final Object actual = path.evaluate(ctx.contextDocument(), ctx.rootDocument(), ctx.configuration()).getValue();

return criteriaType.eval(expected, actual, ctx.configuration());
Object expectedVal = expected;
if(expected instanceof Path){
Path expectedPath = (Path) expected;
Object doc = expectedPath.isRootPath()?ctx.rootDocument():ctx.contextDocument();
expectedVal = expectedPath.evaluate(doc, ctx.rootDocument(), ctx.configuration()).getValue();
}

return criteriaType.eval(expectedVal, actual, ctx);
} catch (ValueCompareException e) {
return false;
} catch (PathNotFoundException e) {
Expand Down Expand Up @@ -571,6 +570,10 @@ public Criteria matches(Predicate p) {

private static int safeCompare(Object expected, Object providerParsed) throws ValueCompareException {

if(expected == providerParsed){
return 0;
}

boolean expNullish = isNullish(expected);
boolean provNullish = isNullish(providerParsed);

Expand Down Expand Up @@ -611,12 +614,21 @@ public static Criteria create(String path, String operator, String expected) {

Path p = PathCompiler.compile(path);

if ("$".equals(path) && (operator == null || operator.isEmpty()) && (expected == null || expected.isEmpty())) {
if (("$".equals(path) || "@".equals(path) )&& (operator == null || operator.isEmpty()) && (expected == null || expected.isEmpty())) {
return new Criteria(p, CriteriaType.NE, null);
} else if (operator.isEmpty()) {
return Criteria.where(path).exists(true);
} else {
return new Criteria(p, CriteriaType.parse(operator), expected);
if(expected.startsWith("$") || expected.startsWith("@")){
Path compile = PathCompiler.compile(expected);
if(!compile.isDefinite()){
throw new InvalidPathException("the predicate path: " + expected + " is not definite");
}
return new Criteria(p, CriteriaType.parse(operator), compile);
} else {
return new Criteria(p, CriteriaType.parse(operator), expected);
}

}
}

Expand Down
4 changes: 2 additions & 2 deletions json-path/src/main/java/com/jayway/jsonpath/JsonPath.java
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,9 @@ public <T> T read(Object jsonObject, Configuration configuration) {

try {
if(optAsPathList){
return (T)path.evaluate(jsonObject, configuration).getPath();
return (T)path.evaluate(jsonObject, jsonObject, configuration).getPath();
} else {
Object res = path.evaluate(jsonObject, configuration).getValue();
Object res = path.evaluate(jsonObject, jsonObject, configuration).getValue();
if(optAlwaysReturnList && path.isDefinite()){
Object array = configuration.jsonProvider().createArray();
configuration.jsonProvider().setProperty(array, 0, res);
Expand Down
17 changes: 15 additions & 2 deletions json-path/src/main/java/com/jayway/jsonpath/Predicate.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,24 @@ public interface Predicate {

boolean apply(PredicateContext ctx);


public interface PredicateContext {

Object target();
/**
* Returns the current element being evaluated by this predicate
* @return current document
*/
Object contextDocument();

/**
* Returns the root document (the complete JSON)
* @return root document
*/
Object rootDocument();

/**
* Configuration to use when evaluating
* @return configuration
*/
Configuration configuration();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,33 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class CompiledPath implements Path {
public class CompiledPath implements Path {

private static final Logger logger = LoggerFactory.getLogger(CompiledPath.class);

private final PathToken root;

private final boolean isRootPath;

public CompiledPath(PathToken root) {

public CompiledPath(PathToken root, boolean isRootPath) {
this.root = root;
this.isRootPath = isRootPath;
}

@Override
public boolean isRootPath() {
return isRootPath;
}

@Override
public EvaluationContext evaluate(Object model, Configuration configuration) {
if(logger.isDebugEnabled()) {
public EvaluationContext evaluate(Object document, Object rootDocument, Configuration configuration) {
if (logger.isDebugEnabled()) {
logger.debug("Evaluating path: {}", toString());
}

EvaluationContextImpl ctx = new EvaluationContextImpl(this, configuration);
root.evaluate("", model, ctx);
EvaluationContextImpl ctx = new EvaluationContextImpl(this, rootDocument, configuration);
root.evaluate("", document, ctx);

return ctx;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ public interface EvaluationContext {
*/
Configuration configuration();

/**
* The json document that is evaluated
*
* @return the document
*/
Object rootDocument();

/**
* This method does not adhere to configuration settings. It will return a single object (not wrapped in a List) even if the
* configuration contains the {@link com.jayway.jsonpath.Option#ALWAYS_RETURN_LIST}
Expand Down
20 changes: 19 additions & 1 deletion json-path/src/main/java/com/jayway/jsonpath/internal/Path.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,26 @@
*/
public interface Path {

EvaluationContext evaluate(Object model, Configuration configuration);
/**
* Evaluates this path
*
* @param document the json document to apply the path on
* @param rootDocument the root json document that started this evaluation
* @param configuration configuration to use
* @return EvaluationContext containing results of evaluation
*/
EvaluationContext evaluate(Object document, Object rootDocument, Configuration configuration);

/**
*
* @return true id this path is definite
*/
boolean isDefinite();

/**
*
* @return true id this path is starts with '$' and false if the path starts with '@'
*/
boolean isRootPath();

}
Loading

0 comments on commit 571654f

Please sign in to comment.