All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.jayway.jsonpath.Criteria Maven / Gradle / Ivy

There is a newer version: 2.9.0
Show newest version
/*
 * Copyright 2011 the original author or authors.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.jayway.jsonpath;

import com.jayway.jsonpath.internal.Path;
import com.jayway.jsonpath.internal.PathCompiler;
import com.jayway.jsonpath.internal.token.PredicateContextImpl;
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 static final String[] OPERATORS = {
            CriteriaType.EQ.toString(),
            CriteriaType.GTE.toString(),
            CriteriaType.LTE.toString(),
            CriteriaType.NE.toString(),
            CriteriaType.LT.toString(),
            CriteriaType.GT.toString(),
            CriteriaType.REGEX.toString()
    };

    private Object left;
    private CriteriaType criteriaType;
    private Object right;

    private final List criteriaChain;

    private static enum CriteriaType {
        EQ {
            @Override
            boolean eval(Object expected, Object model, PredicateContext ctx) {
                boolean res = (0 == safeCompare(expected, model));
                if (logger.isDebugEnabled()) logger.debug("[{}] {} [{}] => {}", model, name(), expected, res);
                return res;
            }

            @Override
            public String toString() {
                return "==";
            }
        },
        NE {
            @Override
            boolean eval(Object expected, Object model, PredicateContext ctx) {
                boolean res = (0 != safeCompare(expected, model));
                if (logger.isDebugEnabled()) logger.debug("[{}] {} [{}] => {}", model, name(), expected, res);
                return res;
            }

            @Override
            public String toString() {
                return "!=";
            }
        },
        GT {
            @Override
            boolean eval(Object expected, Object model, PredicateContext ctx) {
                if ((expected == null) ^ (model == null)) {
                    return false;
                }
                boolean res = (0 > safeCompare(expected, model));
                if (logger.isDebugEnabled()) logger.debug("[{}] {} [{}] => {}", model, name(), expected, res);
                return res;
            }

            @Override
            public String toString() {
                return ">";
            }
        },
        GTE {
            @Override
            boolean eval(Object expected, Object model, PredicateContext ctx) {
                if ((expected == null) ^ (model == null)) {
                    return false;
                }
                boolean res = (0 >= safeCompare(expected, model));
                if (logger.isDebugEnabled()) logger.debug("[{}] {} [{}] => {}", model, name(), expected, res);
                return res;
            }

            @Override
            public String toString() {
                return ">=";
            }
        },
        LT {
            @Override
            boolean eval(Object expected, Object model, PredicateContext ctx) {
                if ((expected == null) ^ (model == null)) {
                    return false;
                }
                boolean res = (0 < safeCompare(expected, model));
                if (logger.isDebugEnabled()) logger.debug("[{}] {} [{}] => {}", model, name(), expected, res);
                return res;
            }

            @Override
            public String toString() {
                return "<";
            }
        },
        LTE {
            @Override
            boolean eval(Object expected, Object model, PredicateContext ctx) {
                if ((expected == null) ^ (model == null)) {
                    return false;
                }
                boolean res = (0 <= safeCompare(expected, model));
                if (logger.isDebugEnabled()) logger.debug("[{}] {} [{}] => {}", model, name(), expected, res);
                return res;
            }

            @Override
            public String toString() {
                return "<=";
            }
        },
        IN {
            @Override
            boolean eval(Object expected, Object model, PredicateContext ctx) {
                boolean res = false;
                Collection exps = (Collection) expected;
                for (Object exp : exps) {
                    if (0 == safeCompare(exp, model)) {
                        res = true;
                        break;
                    }
                }
                if (logger.isDebugEnabled()) logger.debug("[{}] {} [{}] => {}", model, name(), join(", ", exps), res);
                return res;
            }
        },
        NIN {
            @Override
            boolean eval(Object expected, Object model, PredicateContext ctx) {
                Collection nexps = (Collection) expected;
                boolean res = !nexps.contains(model);
                if (logger.isDebugEnabled()) logger.debug("[{}] {} [{}] => {}", model, name(), join(", ", nexps), res);
                return res;
            }
        },
        CONTAINS {
            @Override
            boolean eval(Object expected, Object model, PredicateContext ctx) {
                boolean res = false;
                if (ctx.configuration().jsonProvider().isArray(model)) {
                    for (Object o : ctx.configuration().jsonProvider().toIterable(model)) {
                        if (expected.equals(o)) {
                            res = true;
                            break;
                        }
                    }
                } else if(model instanceof String){
                    if(isNullish(expected) || !(expected instanceof String)){
                        res = false;
                    } else {
                        res = ((String) model).contains((String)expected);
                    }
                }
                if (logger.isDebugEnabled()) logger.debug("[{}] {} [{}] => {}", model, name(), expected, res);
                return res;
            }
        },
        ALL {
            @Override
            boolean eval(Object expected, Object model, PredicateContext ctx) {
                boolean res = true;
                Collection exps = (Collection) expected;
                if (ctx.configuration().jsonProvider().isArray(model)) {
                    for (Object exp : exps) {
                        boolean found = false;
                        for (Object check : ctx.configuration().jsonProvider().toIterable(model)) {
                            if (0 == safeCompare(exp, check)) {
                                found = true;
                                break;
                            }
                        }
                        if (!found) {
                            res = false;
                            break;
                        }
                    }
                    if (logger.isDebugEnabled())
                        logger.debug("[{}] {} [{}] => {}", join(", ", ctx.configuration().jsonProvider().toIterable(model)), name(), join(", ", exps), res);
                } else {
                    res = false;
                    if (logger.isDebugEnabled()) logger.debug("[{}] {} [{}] => {}", "", name(), join(", ", exps), res);
                }
                return res;
            }
        },
        SIZE {
            @Override
            boolean eval(Object expected, Object model, PredicateContext ctx) {
                int size = (Integer) expected;
                boolean res;
                if (ctx.configuration().jsonProvider().isArray(model)) {
                    int length = ctx.configuration().jsonProvider().length(model);
                    res = (length == size);
                    if (logger.isDebugEnabled()) logger.debug("Array with size {} {} {} => {}", length, name(), size, res);
                } else if (model instanceof String) {
                    int length = ((String) model).length();
                    res = length == size;
                    if (logger.isDebugEnabled()) logger.debug("String with length {} {} {} => {}", length, name(), size, res);
                } else {
                    res = false;
                    if (logger.isDebugEnabled())
                        logger.debug("{} {} {} => {}", model == null ? "null" : model.getClass().getName(), name(), size, res);
                }
                return res;
            }
        },
        EXISTS {
            @Override
            boolean eval(Object expected, Object model, PredicateContext ctx) {
                //This must be handled outside
                throw new UnsupportedOperationException();
            }
        },
        TYPE {
            @Override
            boolean eval(Object expected, Object model, PredicateContext ctx) {
                final Class expType = (Class) expected;
                final Class actType = model == null ? null : model.getClass();

                return actType != null && expType.isAssignableFrom(actType);
            }
        },
        REGEX {
            @Override
            boolean eval(Object expected, Object model, PredicateContext ctx) {
                boolean res = false;
                Pattern pattern;
                Object target;

                if (model instanceof Pattern) {
                    pattern = (Pattern) model;
                    target = expected;
                } else {
                    pattern = (Pattern) expected;
                    target = model;
                }

                if (target != null) {
                    res = pattern.matcher(target.toString()).matches();
                }
                if (logger.isDebugEnabled())
                    logger.debug("[{}] {} [{}] => {}", model == null ? "null" : model.toString(), name(), expected == null ? "null" : expected.toString(), res);
                return res;
            }

            @Override
            public String toString() {
                return "=~";
            }
        },
        MATCHES {
            @Override
            boolean eval(Object expected, final Object model, final PredicateContext ctx) {
                PredicateContextImpl pci = (PredicateContextImpl) ctx;
                Predicate exp = (Predicate) expected;
                return exp.apply(new PredicateContextImpl(model, ctx.root(), ctx.configuration(), pci.documentPathCache()));
            }
        },
        NOT_EMPTY {
            @Override
            boolean eval(Object expected, Object model, PredicateContext ctx) {
                boolean res = false;
                if (model != null) {
                    if (ctx.configuration().jsonProvider().isArray(model)) {
                        int len = ctx.configuration().jsonProvider().length(model);
                        res = (0 != len);
                        if (logger.isDebugEnabled()) logger.debug("array length = {} {} => {}", len, name(), res);
                    } else if (model instanceof String) {
                        int len = ((String) model).length();
                        res = (0 != len);
                        if (logger.isDebugEnabled()) logger.debug("string length = {} {} => {}", len, name(), res);
                    }
                }
                return res;
            }
        };

        abstract boolean eval(Object expected, Object model, PredicateContext ctx);

        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 if ("=~".equals(str)) {
                return REGEX;
            } else {
                throw new UnsupportedOperationException("CriteriaType " + str + " can not be parsed");
            }
        }
    }

    private Criteria(List criteriaChain, Object left) {
        /*
        if(left instanceof Path) {
            if (!((Path)left).isDefinite()) {
                throw new InvalidCriteriaException("A criteria path must be definite. The path " + left.toString() + " is not!");
            }
        }*/
        this.left = left;
        this.criteriaChain = criteriaChain;
        this.criteriaChain.add(this);
    }

    private Criteria(Object left) {
        this(new LinkedList(), left);
    }

    private Criteria(Object left, CriteriaType criteriaType, Object right) {
        this(new LinkedList(), left);
        this.criteriaType = criteriaType;
        this.right = right;
    }


    @Override
    public boolean apply(PredicateContext ctx) {
        for (Criteria criteria : criteriaChain) {
            if (!criteria.eval(ctx)) {
                return false;
            }
        }
        return true;
    }

    private Object evaluateIfPath(Object target, PredicateContext ctx) {
        Object res = target;
        if (res instanceof Path) {
            Path leftPath = (Path) target;

            if (ctx instanceof PredicateContextImpl) {
                //This will use cache for document ($) queries
                PredicateContextImpl ctxi = (PredicateContextImpl) ctx;
                res = ctxi.evaluate(leftPath);
            } else {
                Object doc = leftPath.isRootPath() ? ctx.root() : ctx.item();
                res = leftPath.evaluate(doc, ctx.root(), ctx.configuration()).getValue();
            }
        }
        return res == null ? null : ctx.configuration().jsonProvider().unwrap(res);
    }

    private boolean eval(PredicateContext ctx) {
        if (CriteriaType.EXISTS == criteriaType) {
            boolean exists = ((Boolean) right);
            try {
                Configuration c = Configuration.builder().jsonProvider(ctx.configuration().jsonProvider()).options(Option.REQUIRE_PROPERTIES).build();
                Object value = ((Path) left).evaluate(ctx.item(), ctx.root(), c).getValue();
                if (exists) {
                    return (value != null);
                } else {
                    return (value == null);
                }

            } catch (PathNotFoundException e) {
                return !exists;
            }
        } else {
            try {
                Object leftVal = evaluateIfPath(left, ctx);
                Object rightVal = evaluateIfPath(right, ctx);

                return criteriaType.eval(rightVal, leftVal, ctx);
            } 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) {
        if (!key.startsWith("$") && !key.startsWith("@")) {
            key = "@." + 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) {
        if (!key.startsWith("$") && !key.startsWith("@")) {
            key = "@." + 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.right = 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 != operator
     *
     * @param o
     * @return the criteria
     */
    public Criteria ne(Object o) {
        this.criteriaType = CriteriaType.NE;
        this.right = o;
        return this;
    }

    /**
     * Creates a criterion using the < operator
     *
     * @param o
     * @return the criteria
     */
    public Criteria lt(Object o) {
        this.criteriaType = CriteriaType.LT;
        this.right = o;
        return this;
    }

    /**
     * Creates a criterion using the <= operator
     *
     * @param o
     * @return the criteria
     */
    public Criteria lte(Object o) {
        this.criteriaType = CriteriaType.LTE;
        this.right = o;
        return this;
    }

    /**
     * Creates a criterion using the > operator
     *
     * @param o
     * @return the criteria
     */
    public Criteria gt(Object o) {
        this.criteriaType = CriteriaType.GT;
        this.right = o;
        return this;
    }

    /**
     * Creates a criterion using the >= operator
     *
     * @param o
     * @return the criteria
     */
    public Criteria gte(Object o) {
        this.criteriaType = CriteriaType.GTE;
        this.right = 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.right = pattern;
        return this;
    }

    /**
     * The in 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 in 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.right = c;
        return this;
    }

    /**
     * The contains operator asserts that the provided object is contained
     * in the result. The object that should contain the input can be either an object or a String.
     *
     * @param o that should exists in given collection or
     * @return the criteria
     */
    public Criteria contains(Object o) {
        this.criteriaType = CriteriaType.CONTAINS;
        this.right = o;
        return this;
    }

    /**
     * The nin 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 nin 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.right = c;
        return this;
    }

    /**
     * The all 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 all 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.right = c;
        return this;
    }

    /**
     * The size operator matches:
     * 

*

    *
  1. array with the specified number of elements.
  2. *
  3. string with given length.
  4. *
* * @param size * @return the criteria */ public Criteria size(int size) { this.criteriaType = CriteriaType.SIZE; this.right = 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.right = 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.right = t; return this; } /** * The notEmpty operator checks that an array or String is not empty. * * @return the criteria */ public Criteria notEmpty() { this.criteriaType = CriteriaType.NOT_EMPTY; this.right = null; return this; } /** * The matches operator checks that an object matches the given predicate. * * @param p * @return the criteria */ public Criteria matches(Predicate p) { this.criteriaType = CriteriaType.MATCHES; this.right = p; return this; } private static boolean isPath(String string) { return (string != null && (string.startsWith("$") || string.startsWith("@") || string.startsWith("!@"))); } private static boolean isString(String string) { return (string != null && !string.isEmpty() && string.charAt(0) == '\'' && string.charAt(string.length() - 1) == '\''); } private static boolean isPattern(String string) { return (string != null && !string.isEmpty() && string.charAt(0) == '/' && (string.charAt(string.length() - 1) == '/' || (string.charAt(string.length() - 2) == '/' && string.charAt(string.length() - 1) == 'i')) ); } private static Pattern compilePattern(String string) { int lastIndex = string.lastIndexOf('/'); boolean ignoreCase = string.endsWith("i"); String regex = string.substring(1, lastIndex); int flags = ignoreCase ? Pattern.CASE_INSENSITIVE : 0; return Pattern.compile(regex, flags); } /** * Parse the provided criteria * * @param criteria * @return a criteria */ 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); } /** * Creates a new criteria * * @param left path to evaluate in criteria * @param operator operator * @param right expected value * @return a new Criteria */ public static Criteria create(String left, String operator, String right) { Object leftPrepared = left; Object rightPrepared = right; Path leftPath = null; Path rightPath = null; boolean existsCheck = true; if (isPath(left)) { if (left.charAt(0) == '!') { existsCheck = false; left = left.substring(1); } leftPath = PathCompiler.compile(left); if (!leftPath.isDefinite()) { throw new InvalidPathException("the predicate path: " + left + " is not definite"); } leftPrepared = leftPath; } else if (isString(left)) { leftPrepared = left.substring(1, left.length() - 1); } else if (isPattern(left)) { leftPrepared = compilePattern(left); } if (isPath(right)) { if (right.charAt(0) == '!') { throw new InvalidPathException("Invalid negation! Can only be used for existence check e.g [?([email protected])]"); } rightPath = PathCompiler.compile(right); if (!rightPath.isDefinite()) { throw new InvalidPathException("the predicate path: " + right + " is not definite"); } rightPrepared = rightPath; } else if (isString(right)) { rightPrepared = right.substring(1, right.length() - 1); } else if (isPattern(right)) { rightPrepared = compilePattern(right); } if (leftPath != null && operator.isEmpty()) { return Criteria.where(leftPath).exists(existsCheck); } else { return new Criteria(leftPrepared, CriteriaType.parse(operator), rightPrepared); } } private static int safeCompare(Object left, Object right) throws ValueCompareException { if (left == right) { return 0; } boolean leftNullish = isNullish(left); boolean rightNullish = isNullish(right); if (leftNullish && !rightNullish) { return -1; } else if (!leftNullish && rightNullish) { return 1; } else if (leftNullish && rightNullish) { return 0; } else if (left instanceof String && right instanceof String) { String exp = (String) left; if (exp.contains("\'")) { exp = exp.replace("\\'", "'"); } return exp.compareTo((String) right); } else if (left instanceof Number && right instanceof Number) { return new BigDecimal(left.toString()).compareTo(new BigDecimal(right.toString())); } else if (left instanceof String && right instanceof Number) { return new BigDecimal(left.toString()).compareTo(new BigDecimal(right.toString())); } else if (left instanceof String && right instanceof Boolean) { Boolean e = Boolean.valueOf((String) left); Boolean a = (Boolean) right; return e.compareTo(a); } else if (left instanceof Boolean && right instanceof Boolean) { Boolean e = (Boolean) left; Boolean a = (Boolean) right; return e.compareTo(a); } else { logger.debug("Can not compare a {} with a {}", left.getClass().getName(), right.getClass().getName()); throw new ValueCompareException(); } } private static boolean isNullish(Object o) { return (o == null || ((o instanceof String) && ("null".equals(o)))); } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(left.toString()) .append(criteriaType.toString()) .append(wrapString(right)); return sb.toString(); } private static String wrapString(Object o) { if (o == null) { return "null"; } if (o instanceof String) { return "'" + o.toString() + "'"; } else { return o.toString(); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy