You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
634 lines
20 KiB
634 lines
20 KiB
package com.jayway.jsonpath; |
|
|
|
import com.jayway.jsonpath.internal.Path; |
|
import com.jayway.jsonpath.internal.PathCompiler; |
|
|
|
import org.slf4j.Logger; |
|
import org.slf4j.LoggerFactory; |
|
|
|
import java.math.BigDecimal; |
|
import java.util.Arrays; |
|
import java.util.Collection; |
|
import java.util.LinkedList; |
|
import java.util.List; |
|
import java.util.regex.Pattern; |
|
|
|
import static com.jayway.jsonpath.internal.Utils.join; |
|
import static com.jayway.jsonpath.internal.Utils.notNull; |
|
|
|
|
|
/** |
|
* |
|
*/ |
|
@SuppressWarnings("unchecked") |
|
public class Criteria implements Predicate { |
|
|
|
private static final Logger logger = LoggerFactory.getLogger(Criteria.class); |
|
|
|
private final Path path; |
|
private CriteriaType criteriaType; |
|
private Object expected; |
|
|
|
private final List<Criteria> criteriaChain; |
|
|
|
private static enum CriteriaType { |
|
EQ { |
|
@Override |
|
boolean eval(Object expected, Object actual, Configuration configuration) { |
|
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 res = (0 != safeCompare(expected, actual)); |
|
logger.debug("[{}] {} [{}] => {}", actual, name(), expected, res); |
|
return res; |
|
} |
|
}, |
|
GT { |
|
@Override |
|
boolean eval(Object expected, Object actual, Configuration configuration) { |
|
if ((expected == null) ^ (actual == null)) { |
|
return false; |
|
} |
|
boolean res = (0 > safeCompare(expected, actual)); |
|
logger.debug("[{}] {} [{}] => {}", actual, name(), expected, res); |
|
return res; |
|
} |
|
}, |
|
GTE { |
|
@Override |
|
boolean eval(Object expected, Object actual, Configuration configuration) { |
|
if ((expected == null) ^ (actual == null)) { |
|
return false; |
|
} |
|
boolean res = (0 >= safeCompare(expected, actual)); |
|
logger.debug("[{}] {} [{}] => {}", actual, name(), expected, res); |
|
return res; |
|
} |
|
}, |
|
LT { |
|
@Override |
|
boolean eval(Object expected, Object actual, Configuration configuration) { |
|
if ((expected == null) ^ (actual == null)) { |
|
return false; |
|
} |
|
boolean res = (0 < safeCompare(expected, actual)); |
|
logger.debug("[{}] {} [{}] => {}", actual, name(), expected, res); |
|
return res; |
|
} |
|
}, |
|
LTE { |
|
@Override |
|
boolean eval(Object expected, Object actual, Configuration configuration) { |
|
if ((expected == null) ^ (actual == null)) { |
|
return false; |
|
} |
|
boolean res = (0 <= safeCompare(expected, actual)); |
|
logger.debug("[{}] {} [{}] => {}", actual, name(), expected, res); |
|
return res; |
|
} |
|
}, |
|
IN { |
|
@Override |
|
boolean eval(Object expected, Object actual, Configuration configuration) { |
|
boolean res = false; |
|
Collection exps = (Collection) expected; |
|
for (Object exp : exps) { |
|
if (0 == safeCompare(exp, actual)) { |
|
res = true; |
|
break; |
|
} |
|
} |
|
logger.debug("[{}] {} [{}] => {}", actual, name(), join(", ", exps), res); |
|
return res; |
|
} |
|
}, |
|
NIN { |
|
@Override |
|
boolean eval(Object expected, Object actual, Configuration configuration) { |
|
Collection nexps = (Collection) expected; |
|
boolean res = !nexps.contains(actual); |
|
logger.debug("[{}] {} [{}] => {}", actual, name(), join(", ", nexps), res); |
|
return res; |
|
} |
|
}, |
|
ALL { |
|
@Override |
|
boolean eval(Object expected, Object actual, Configuration configuration) { |
|
boolean res = true; |
|
Collection exps = (Collection) expected; |
|
if (configuration.jsonProvider().isArray(actual)) { |
|
for (Object exp : exps) { |
|
boolean found = false; |
|
for (Object check : configuration.jsonProvider().toIterable(actual)) { |
|
if (0 == safeCompare(exp, check)) { |
|
found = true; |
|
break; |
|
} |
|
} |
|
if (!found) { |
|
res = false; |
|
break; |
|
} |
|
} |
|
logger.debug("[{}] {} [{}] => {}", join(", ", configuration.jsonProvider().toIterable(actual)), name(), join(", ", exps), res); |
|
} else { |
|
res = false; |
|
logger.debug("[{}] {} [{}] => {}", "<NOT AN ARRAY>", name(), join(", ", exps), res); |
|
} |
|
return res; |
|
} |
|
}, |
|
SIZE { |
|
@Override |
|
boolean eval(Object expected, Object actual, Configuration configuration) { |
|
int size = (Integer) expected; |
|
boolean res; |
|
if (configuration.jsonProvider().isArray(actual)) { |
|
int length = configuration.jsonProvider().length(actual); |
|
res = (length == size); |
|
logger.debug("Array with size {} {} {} => {}", length, name(), size, res); |
|
} else if (actual instanceof String) { |
|
int length = ((String) actual).length(); |
|
res = length == size; |
|
logger.debug("String with length {} {} {} => {}", length, name(), size, res); |
|
} else { |
|
res = false; |
|
logger.debug("{} {} {} => {}", actual == null ? "null" : actual.getClass().getName(), name(), size, res); |
|
} |
|
return res; |
|
} |
|
}, |
|
EXISTS { |
|
@Override |
|
boolean eval(Object expected, Object actual, Configuration configuration) { |
|
//This must be handled outside |
|
throw new UnsupportedOperationException(); |
|
} |
|
}, |
|
TYPE { |
|
@Override |
|
boolean eval(Object expected, Object actual, Configuration configuration) { |
|
final Class<?> expType = (Class<?>) expected; |
|
final Class<?> actType = actual == null ? null : actual.getClass(); |
|
|
|
return actType != null && expType.isAssignableFrom(actType); |
|
} |
|
}, |
|
REGEX { |
|
@Override |
|
boolean eval(Object expected, Object actual, Configuration configuration) { |
|
boolean res = false; |
|
final Pattern pattern = (Pattern) expected; |
|
if (actual != null && actual instanceof String) { |
|
res = pattern.matcher(actual.toString()).matches(); |
|
} |
|
logger.debug("[{}] {} [{}] => {}", actual, name(), expected.toString(), res); |
|
return res; |
|
} |
|
}, |
|
MATCHES { |
|
@Override |
|
boolean eval(Object expected, final Object actual, final Configuration configuration) { |
|
Predicate exp = (Predicate) expected; |
|
return exp.apply(new PredicateContext() { |
|
@Override |
|
public Object target() { |
|
return actual; |
|
} |
|
|
|
@Override |
|
public Configuration configuration() { |
|
return configuration; |
|
} |
|
}); |
|
} |
|
}, |
|
NOT_EMPTY { |
|
@Override |
|
boolean eval(Object expected, Object actual, Configuration configuration) { |
|
boolean res = false; |
|
if (actual != null) { |
|
if (configuration.jsonProvider().isArray(actual)) { |
|
int len = configuration.jsonProvider().length(actual); |
|
res = (0 != len); |
|
logger.debug("array length = {} {} => {}", len, name(), res); |
|
} else if (actual instanceof String) { |
|
int len = ((String) actual).length(); |
|
res = (0 != len); |
|
logger.debug("string length = {} {} => {}", len, name(), res); |
|
} |
|
} |
|
return res; |
|
} |
|
}; |
|
|
|
abstract boolean eval(Object expected, Object actual, Configuration configuration); |
|
|
|
public static CriteriaType parse(String str) { |
|
if ("==".equals(str)) { |
|
return EQ; |
|
} else if (">".equals(str)) { |
|
return GT; |
|
} else if (">=".equals(str)) { |
|
return GTE; |
|
} else if ("<".equals(str)) { |
|
return LT; |
|
} else if ("<=".equals(str)) { |
|
return LTE; |
|
} else if ("!=".equals(str)) { |
|
return NE; |
|
} else { |
|
throw new UnsupportedOperationException("CriteriaType " + str + " can not be parsed"); |
|
} |
|
} |
|
} |
|
|
|
private Criteria(List<Criteria> criteriaChain, Path path) { |
|
if (!path.isDefinite()) { |
|
throw new InvalidCriteriaException("A criteria path must be definite. The path " + path.toString() + " is not!"); |
|
} |
|
this.path = path; |
|
this.criteriaChain = criteriaChain; |
|
this.criteriaChain.add(this); |
|
} |
|
|
|
private Criteria(Path path) { |
|
this(new LinkedList<Criteria>(), path); |
|
} |
|
|
|
private Criteria(Path path, CriteriaType criteriaType, Object expected) { |
|
this(new LinkedList<Criteria>(), path); |
|
this.criteriaType = criteriaType; |
|
this.expected = expected; |
|
} |
|
|
|
|
|
@Override |
|
public boolean apply(PredicateContext ctx) { |
|
for (Criteria criteria : criteriaChain) { |
|
if (!criteria.eval(ctx)) { |
|
return false; |
|
} |
|
} |
|
return true; |
|
} |
|
|
|
private boolean eval(PredicateContext ctx) { |
|
if (CriteriaType.EXISTS == criteriaType) { |
|
boolean exists = ((Boolean) expected); |
|
try { |
|
Configuration c = Configuration.builder().jsonProvider(ctx.configuration().jsonProvider()).options().build(); |
|
path.evaluate(ctx.target(), c).getValue(); |
|
return exists; |
|
} catch (PathNotFoundException e) { |
|
return !exists; |
|
} |
|
} else { |
|
try { |
|
final Object actual = path.evaluate(ctx.target(), ctx.configuration()).getValue(); |
|
|
|
return criteriaType.eval(expected, actual, ctx.configuration()); |
|
} catch (ValueCompareException e) { |
|
return false; |
|
} catch (PathNotFoundException e) { |
|
return false; |
|
} |
|
} |
|
} |
|
|
|
|
|
/** |
|
* Static factory method to create a Criteria using the provided key |
|
* |
|
* @param key filed name |
|
* @return the new criteria |
|
*/ |
|
public static Criteria where(Path key) { |
|
return new Criteria(key); |
|
} |
|
|
|
/** |
|
* Static factory method to create a Criteria using the provided key |
|
* |
|
* @param key filed name |
|
* @return the new criteria |
|
*/ |
|
|
|
public static Criteria where(String key) { |
|
return where(PathCompiler.compile(key)); |
|
} |
|
|
|
/** |
|
* Static factory method to create a Criteria using the provided key |
|
* |
|
* @param key ads new filed to criteria |
|
* @return the criteria builder |
|
*/ |
|
public Criteria and(String key) { |
|
return new Criteria(this.criteriaChain, PathCompiler.compile(key)); |
|
} |
|
|
|
/** |
|
* Creates a criterion using equality |
|
* |
|
* @param o |
|
* @return the criteria |
|
*/ |
|
public Criteria is(Object o) { |
|
this.criteriaType = CriteriaType.EQ; |
|
this.expected = o; |
|
return this; |
|
} |
|
|
|
/** |
|
* Creates a criterion using equality |
|
* |
|
* @param o |
|
* @return the criteria |
|
*/ |
|
public Criteria eq(Object o) { |
|
return is(o); |
|
} |
|
|
|
/** |
|
* Creates a criterion using the <b>!=</b> operator |
|
* |
|
* @param o |
|
* @return the criteria |
|
*/ |
|
public Criteria ne(Object o) { |
|
this.criteriaType = CriteriaType.NE; |
|
this.expected = o; |
|
return this; |
|
} |
|
|
|
/** |
|
* Creates a criterion using the <b><</b> operator |
|
* |
|
* @param o |
|
* @return the criteria |
|
*/ |
|
public Criteria lt(Object o) { |
|
this.criteriaType = CriteriaType.LT; |
|
this.expected = o; |
|
return this; |
|
} |
|
|
|
/** |
|
* Creates a criterion using the <b><=</b> operator |
|
* |
|
* @param o |
|
* @return the criteria |
|
*/ |
|
public Criteria lte(Object o) { |
|
this.criteriaType = CriteriaType.LTE; |
|
this.expected = o; |
|
return this; |
|
} |
|
|
|
/** |
|
* Creates a criterion using the <b>></b> operator |
|
* |
|
* @param o |
|
* @return the criteria |
|
*/ |
|
public Criteria gt(Object o) { |
|
this.criteriaType = CriteriaType.GT; |
|
this.expected = o; |
|
return this; |
|
} |
|
|
|
/** |
|
* Creates a criterion using the <b>>=</b> operator |
|
* |
|
* @param o |
|
* @return the criteria |
|
*/ |
|
public Criteria gte(Object o) { |
|
this.criteriaType = CriteriaType.GTE; |
|
this.expected = o; |
|
return this; |
|
} |
|
|
|
/** |
|
* Creates a criterion using a Regex |
|
* |
|
* @param pattern |
|
* @return the criteria |
|
*/ |
|
public Criteria regex(Pattern pattern) { |
|
notNull(pattern, "pattern can not be null"); |
|
this.criteriaType = CriteriaType.REGEX; |
|
this.expected = pattern; |
|
return this; |
|
} |
|
|
|
/** |
|
* The <code>in</code> operator is analogous to the SQL IN modifier, allowing you |
|
* to specify an array of possible matches. |
|
* |
|
* @param o the values to match against |
|
* @return the criteria |
|
*/ |
|
public Criteria in(Object... o) { |
|
return in(Arrays.asList(o)); |
|
} |
|
|
|
/** |
|
* The <code>in</code> operator is analogous to the SQL IN modifier, allowing you |
|
* to specify an array of possible matches. |
|
* |
|
* @param c the collection containing the values to match against |
|
* @return the criteria |
|
*/ |
|
public Criteria in(Collection<?> c) { |
|
notNull(c, "collection can not be null"); |
|
this.criteriaType = CriteriaType.IN; |
|
this.expected = c; |
|
return this; |
|
} |
|
|
|
/** |
|
* The <code>nin</code> operator is similar to $in except that it selects objects for |
|
* which the specified field does not have any value in the specified array. |
|
* |
|
* @param o the values to match against |
|
* @return the criteria |
|
*/ |
|
public Criteria nin(Object... o) { |
|
return nin(Arrays.asList(o)); |
|
} |
|
|
|
/** |
|
* The <code>nin</code> operator is similar to $in except that it selects objects for |
|
* which the specified field does not have any value in the specified array. |
|
* |
|
* @param c the values to match against |
|
* @return the criteria |
|
*/ |
|
public Criteria nin(Collection<?> c) { |
|
notNull(c, "collection can not be null"); |
|
this.criteriaType = CriteriaType.NIN; |
|
this.expected = c; |
|
return this; |
|
} |
|
|
|
/** |
|
* The <code>all</code> operator is similar to $in, but instead of matching any value |
|
* in the specified array all values in the array must be matched. |
|
* |
|
* @param o |
|
* @return the criteria |
|
*/ |
|
public Criteria all(Object... o) { |
|
return all(Arrays.asList(o)); |
|
} |
|
|
|
/** |
|
* The <code>all</code> operator is similar to $in, but instead of matching any value |
|
* in the specified array all values in the array must be matched. |
|
* |
|
* @param c |
|
* @return the criteria |
|
*/ |
|
public Criteria all(Collection<?> c) { |
|
notNull(c, "collection can not be null"); |
|
this.criteriaType = CriteriaType.ALL; |
|
this.expected = c; |
|
return this; |
|
} |
|
|
|
/** |
|
* The <code>size</code> operator matches: |
|
* <p/> |
|
* <ol> |
|
* <li>array with the specified number of elements.</li> |
|
* <li>string with given length.</li> |
|
* </ol> |
|
* |
|
* @param size |
|
* @return the criteria |
|
*/ |
|
public Criteria size(int size) { |
|
this.criteriaType = CriteriaType.SIZE; |
|
this.expected = size; |
|
return this; |
|
} |
|
|
|
|
|
/** |
|
* Check for existence (or lack thereof) of a field. |
|
* |
|
* @param b |
|
* @return the criteria |
|
*/ |
|
public Criteria exists(boolean b) { |
|
this.criteriaType = CriteriaType.EXISTS; |
|
this.expected = b; |
|
return this; |
|
} |
|
|
|
/** |
|
* The $type operator matches values based on their Java type. |
|
* |
|
* @param t |
|
* @return the criteria |
|
*/ |
|
public Criteria type(Class<?> t) { |
|
notNull(t, "type can not be null"); |
|
this.criteriaType = CriteriaType.TYPE; |
|
this.expected = t; |
|
return this; |
|
} |
|
|
|
/** |
|
* The <code>notEmpty</code> operator checks that an array or String is not empty. |
|
* |
|
* @return the criteria |
|
*/ |
|
public Criteria notEmpty() { |
|
this.criteriaType = CriteriaType.NOT_EMPTY; |
|
this.expected = null; |
|
return this; |
|
} |
|
|
|
/** |
|
* The <code>matches</code> operator checks that an object matches the given predicate. |
|
* |
|
* @param p |
|
* @return the criteria |
|
*/ |
|
public Criteria matches(Predicate p) { |
|
this.criteriaType = CriteriaType.MATCHES; |
|
this.expected = p; |
|
return this; |
|
} |
|
|
|
private static int safeCompare(Object expected, Object providerParsed) throws ValueCompareException { |
|
|
|
boolean expNullish = isNullish(expected); |
|
boolean provNullish = isNullish(providerParsed); |
|
|
|
if (expNullish && !provNullish) { |
|
return -1; |
|
} else if (!expNullish && provNullish) { |
|
return 1; |
|
} else if (expNullish && provNullish) { |
|
return 0; |
|
} else if (expected instanceof String && providerParsed instanceof String) { |
|
return ((String) expected).compareTo((String) providerParsed); |
|
} else if (expected instanceof Number && providerParsed instanceof Number) { |
|
return new BigDecimal(expected.toString()).compareTo(new BigDecimal(providerParsed.toString())); |
|
} else if (expected instanceof String && providerParsed instanceof Number) { |
|
return new BigDecimal(expected.toString()).compareTo(new BigDecimal(providerParsed.toString())); |
|
} else if (expected instanceof String && providerParsed instanceof Boolean) { |
|
Boolean e = Boolean.valueOf((String) expected); |
|
Boolean a = (Boolean) providerParsed; |
|
return e.compareTo(a); |
|
} else if (expected instanceof Boolean && providerParsed instanceof Boolean) { |
|
Boolean e = (Boolean) expected; |
|
Boolean a = (Boolean) providerParsed; |
|
return e.compareTo(a); |
|
} else { |
|
logger.debug("Can not compare a {} with a {}", expected.getClass().getName(), providerParsed.getClass().getName()); |
|
throw new ValueCompareException(); |
|
} |
|
} |
|
|
|
private static boolean isNullish(Object o) { |
|
return (o == null || ((o instanceof String) && ("null".equals(o)))); |
|
} |
|
|
|
public static Criteria create(String path, String operator, String expected) { |
|
if (!expected.isEmpty() && expected.charAt(0) == '\'' && expected.charAt(expected.length() - 1) == '\'') { |
|
expected = expected.substring(1, expected.length() - 1); |
|
} |
|
|
|
Path p = PathCompiler.compile(path); |
|
|
|
if ("$".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); |
|
} |
|
} |
|
|
|
@Override |
|
public String toString() { |
|
StringBuilder sb = new StringBuilder(); |
|
sb.append(path.toString()) |
|
.append("|") |
|
.append(criteriaType.name()) |
|
.append("|") |
|
.append(expected) |
|
.append("|"); |
|
return sb.toString(); |
|
} |
|
}
|
|
|