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, configuration)); logger.debug("[{}] {} [{}] => {}", actual, name(), expected, res); return res; } }, NE { @Override boolean eval(Object expected, Object actual, Configuration configuration) { boolean res = (0 != safeCompare(expected, actual, configuration)); 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, configuration)); 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, configuration)); 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, configuration)); 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, configuration)); 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, configuration)) { 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.getProvider().isArray(actual)) { for (Object exp : exps) { boolean found = false; for (Object check : configuration.getProvider().toIterable(actual)) { if (0 == safeCompare(exp, check, configuration)) { found = true; break; } } if (!found) { res = false; break; } } logger.debug("[{}] {} [{}] => {}", join(", ", configuration.getProvider().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.getProvider().isArray(actual)) { int length = configuration.getProvider().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.getProvider().isArray(actual)) { int len = configuration.getProvider().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 = ctx.configuration(); if(c.containsOption(Option.ALWAYS_RETURN_LIST) || c.containsOption(Option.SUPPRESS_EXCEPTIONS)){ c = c.options(); } 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 (CompareException 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 actual, Configuration configuration) { if (isNullish(expected) && !isNullish(actual)) { return -1; } else if (!isNullish(expected) && isNullish(actual)) { return 1; } else if (isNullish(expected) && isNullish(actual)) { return 0; } else if (expected instanceof String && actual instanceof String) { return ((String) expected).compareTo((String) actual); } else if (expected instanceof Number && actual instanceof Number) { return new BigDecimal(expected.toString()).compareTo(new BigDecimal(actual.toString())); } else if (expected instanceof String && actual instanceof Number) { return new BigDecimal(expected.toString()).compareTo(new BigDecimal(actual.toString())); } else if (expected instanceof String && actual instanceof Boolean) { Boolean e = Boolean.valueOf((String)expected); Boolean a = (Boolean) actual; return e.compareTo(a); } else if (expected instanceof Boolean && actual instanceof Boolean) { Boolean e = (Boolean) expected; Boolean a = (Boolean) actual; return e.compareTo(a); } else { logger.debug("Can not compare a {} with a {}", expected.getClass().getName(), actual.getClass().getName()); throw new CompareException(); } } private static boolean isNullish(Object o){ return (o == null || ((o instanceof String) && ("null".equals(o)))); } private static class CompareException extends RuntimeException { } public static Criteria create(String path, String operator, String expected) { if (expected.startsWith("'") && expected.endsWith("'")) { 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(); } }