From 454b7d49a34246b3629c7a20adefe503859aad34 Mon Sep 17 00:00:00 2001 From: Kalle Stenflo Date: Sun, 11 Oct 2015 15:39:17 +0200 Subject: [PATCH] PathCompiler simplifications. --- .../java/com/jayway/jsonpath/Criteria.java | 272 ++++-- .../main/java/com/jayway/jsonpath/Filter.java | 41 +- .../java/com/jayway/jsonpath/JsonPath.java | 1 + .../jsonpath/internal/JsonFormatter.java | 148 +-- .../jsonpath/internal/PathCompiler.java | 850 ++++++++++-------- .../com/jayway/jsonpath/internal/Utils.java | 2 +- .../internal/token/ArrayIndexOperation.java | 63 ++ .../internal/token/ArrayPathToken.java | 251 +++--- .../internal/token/ArraySliceOperation.java | 86 ++ .../jsonpath/internal/token/PathToken.java | 6 +- .../internal/token/PathTokenAppender.java | 5 + .../internal/token/PathTokenFactory.java | 47 + .../internal/token/PredicatePathToken.java | 24 +- .../internal/token/PropertyPathToken.java | 6 +- .../internal/token/RootPathToken.java | 25 +- .../internal/token/ScanPathToken.java | 5 +- .../internal/token/WildcardPathToken.java | 5 +- .../java/com/jayway/jsonpath/FilterTest.java | 178 +++- .../com/jayway/jsonpath/PathCompilerTest.java | 131 +++ .../jayway/jsonpath/old/ArraySlicingTest.java | 6 - .../jayway/jsonpath/old/ComplianceTest.java | 1 - .../com/jayway/jsonpath/old/IssuesTest.java | 62 +- .../com/jayway/jsonpath/old/JsonPathTest.java | 2 - .../old/internal/ArrayPathTokenTest.java | 8 - .../old/internal/ScanPathTokenTest.java | 11 - 25 files changed, 1487 insertions(+), 749 deletions(-) create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/token/ArrayIndexOperation.java create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/token/ArraySliceOperation.java create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/token/PathTokenAppender.java create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/token/PathTokenFactory.java create mode 100644 json-path/src/test/java/com/jayway/jsonpath/PathCompilerTest.java diff --git a/json-path/src/main/java/com/jayway/jsonpath/Criteria.java b/json-path/src/main/java/com/jayway/jsonpath/Criteria.java index 3df64a34..e0399d55 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/Criteria.java +++ b/json-path/src/main/java/com/jayway/jsonpath/Criteria.java @@ -16,6 +16,7 @@ package com.jayway.jsonpath; import com.jayway.jsonpath.internal.Path; import com.jayway.jsonpath.internal.PathCompiler; +import com.jayway.jsonpath.internal.Utils; import com.jayway.jsonpath.internal.token.PredicateContextImpl; import com.jayway.jsonpath.spi.json.JsonProvider; import org.slf4j.Logger; @@ -24,6 +25,7 @@ import org.slf4j.LoggerFactory; import java.math.BigDecimal; import java.util.Arrays; import java.util.Collection; +import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.regex.Pattern; @@ -40,14 +42,26 @@ public class Criteria implements Predicate { private static final Logger logger = LoggerFactory.getLogger(Criteria.class); + private static final String CRITERIA_WRAPPER_CHAR = "¦"; + private static final String[] OPERATORS = { - CriteriaType.EQ.toString(), CriteriaType.GTE.toString(), CriteriaType.LTE.toString(), + CriteriaType.EQ.toString(), CriteriaType.NE.toString(), CriteriaType.LT.toString(), CriteriaType.GT.toString(), - CriteriaType.REGEX.toString() + CriteriaType.REGEX.toString(), + CriteriaType.NIN.toString(), + CriteriaType.IN.toString(), + CriteriaType.CONTAINS.toString(), + CriteriaType.ALL.toString(), + CriteriaType.SIZE.toString(), + CriteriaType.EXISTS.toString(), + CriteriaType.TYPE.toString(), + CriteriaType.MATCHES.toString(), + CriteriaType.NOT_EMPTY.toString(), + }; private static final char BS = '\\'; @@ -148,6 +162,34 @@ public class Criteria implements Predicate { return "<="; } }, + 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 "=~"; + } + }, IN { @Override boolean eval(Object expected, Object model, PredicateContext ctx) { @@ -162,6 +204,11 @@ public class Criteria implements Predicate { if (logger.isDebugEnabled()) logger.debug("[{}] {} [{}] => {}", model, name(), join(", ", exps), res); return res; } + + @Override + public String toString() { + return CRITERIA_WRAPPER_CHAR + name() + CRITERIA_WRAPPER_CHAR; + } }, NIN { @Override @@ -171,6 +218,11 @@ public class Criteria implements Predicate { if (logger.isDebugEnabled()) logger.debug("[{}] {} [{}] => {}", model, name(), join(", ", nexps), res); return res; } + + @Override + public String toString() { + return CRITERIA_WRAPPER_CHAR + name() + CRITERIA_WRAPPER_CHAR; + } }, CONTAINS { @Override @@ -183,16 +235,21 @@ public class Criteria implements Predicate { break; } } - } else if(model instanceof String){ - if(isNullish(expected) || !(expected instanceof String)){ + } else if (model instanceof String) { + if (isNullish(expected) || !(expected instanceof String)) { res = false; } else { - res = ((String) model).contains((String)expected); + res = ((String) model).contains((String) expected); } } if (logger.isDebugEnabled()) logger.debug("[{}] {} [{}] => {}", model, name(), expected, res); return res; } + + @Override + public String toString() { + return CRITERIA_WRAPPER_CHAR + name() + CRITERIA_WRAPPER_CHAR; + } }, ALL { @Override @@ -221,6 +278,11 @@ public class Criteria implements Predicate { } return res; } + + @Override + public String toString() { + return CRITERIA_WRAPPER_CHAR + name() + CRITERIA_WRAPPER_CHAR; + } }, SIZE { @Override @@ -242,6 +304,11 @@ public class Criteria implements Predicate { } return res; } + + @Override + public String toString() { + return CRITERIA_WRAPPER_CHAR + name() + CRITERIA_WRAPPER_CHAR; + } }, EXISTS { @Override @@ -249,6 +316,11 @@ public class Criteria implements Predicate { //This must be handled outside throw new UnsupportedOperationException(); } + + @Override + public String toString() { + return CRITERIA_WRAPPER_CHAR + name() + CRITERIA_WRAPPER_CHAR; + } }, TYPE { @Override @@ -258,33 +330,10 @@ public class Criteria implements Predicate { 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 "=~"; + return CRITERIA_WRAPPER_CHAR + name() + CRITERIA_WRAPPER_CHAR; } }, MATCHES { @@ -294,6 +343,11 @@ public class Criteria implements Predicate { Predicate exp = (Predicate) expected; return exp.apply(new PredicateContextImpl(model, ctx.root(), ctx.configuration(), pci.documentPathCache())); } + + @Override + public String toString() { + return CRITERIA_WRAPPER_CHAR + name() + CRITERIA_WRAPPER_CHAR; + } }, NOT_EMPTY { @Override @@ -312,38 +366,37 @@ public class Criteria implements Predicate { } return res; } + + @Override + public String toString() { + return CRITERIA_WRAPPER_CHAR + name() + CRITERIA_WRAPPER_CHAR; + } + }, + NOOP { + @Override + boolean eval(Object expected, Object model, PredicateContext ctx) { + return true; + } + + @Override + public String toString() { + return CRITERIA_WRAPPER_CHAR + name() + CRITERIA_WRAPPER_CHAR; + } }; 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"); + for (CriteriaType criteriaType : values()) { + if (criteriaType.toString().equals(str)) { + return criteriaType; + } } + 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); @@ -746,21 +799,24 @@ public class Criteria implements Predicate { public static Criteria parse(String criteria) { int operatorIndex = -1; String left = ""; - String operator = ""; + CriteriaType operator = null; String right = ""; + + //can not iterate values() because the need to be checked in order eg '>=' before '>' for (int y = 0; y < OPERATORS.length; y++) { operatorIndex = criteria.indexOf(OPERATORS[y]); if (operatorIndex != -1) { - operator = OPERATORS[y]; + operator = CriteriaType.parse(OPERATORS[y]); break; } } - if (!operator.isEmpty()) { + if (operator != null) { left = criteria.substring(0, operatorIndex).trim(); - right = criteria.substring(operatorIndex + operator.length()).trim(); + right = criteria.substring(operatorIndex + operator.toString().length()).trim(); } else { left = criteria.trim(); } + return Criteria.create(left, operator, right); } @@ -770,7 +826,10 @@ public class Criteria implements Predicate { private static class JsonValue { final String value; volatile Object jsonValue; - JsonValue(String value) { this.value = value; } + + JsonValue(String value) { + this.value = value; + } Object parsed(PredicateContext ctx) { if (jsonValue == null) { @@ -794,7 +853,7 @@ public class Criteria implements Predicate { * @param right expected value * @return a new Criteria */ - public static Criteria create(String left, String operator, String right) { + private static Criteria create(String left, CriteriaType operator, String right) { Object leftPrepared = left; Object rightPrepared = right; Path leftPath = null; @@ -828,16 +887,18 @@ public class Criteria implements Predicate { rightPrepared = rightPath; } else if (isString(right)) { rightPrepared = right.substring(1, right.length() - 1); + } else if(Utils.isNumeric(right)){ + rightPrepared = new BigDecimal(right); } else if (isJson(right)) { rightPrepared = new JsonValue(right); } else if (isPattern(right)) { rightPrepared = compilePattern(right); } - if (leftPath != null && operator.isEmpty()) { + if (leftPath != null && (operator == null)) { return Criteria.where(leftPath).exists(existsCheck); } else { - return new Criteria(leftPrepared, CriteriaType.parse(operator), rightPrepared); + return new Criteria(leftPrepared, operator, rightPrepared); } } @@ -846,7 +907,7 @@ public class Criteria implements Predicate { } private static String unescape(String s) { - if (s.indexOf(BS) == - 1) + if (s.indexOf(BS) == -1) return s; StringBuilder sb = new StringBuilder(s.length()); for (int i = 0; i < s.length(); i++) { @@ -854,15 +915,25 @@ public class Criteria implements Predicate { if (c == BS) { char c2 = s.charAt(++i); switch (c2) { - case 'b': c2 = '\b'; break; - case 'f': c2 = '\f'; break; - case 'n': c2 = '\n'; break; - case 'r': c2 = '\r'; break; - case 't': c2 = '\t'; break; + case 'b': + c2 = '\b'; + break; + case 'f': + c2 = '\f'; + break; + case 'n': + c2 = '\n'; + break; + case 'r': + c2 = '\r'; + break; + case 't': + c2 = '\t'; + break; case 'u': try { String hex = s.substring(i + 1, i + 5); - c2 = (char)Integer.parseInt(hex, 16); + c2 = (char) Integer.parseInt(hex, 16); i += 4; } catch (Exception e) { throw new ValueCompareException("\\u parse failed", e); @@ -897,6 +968,8 @@ public class Criteria implements Predicate { return expected.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 Number && right instanceof BigDecimal) { + return new BigDecimal(left.toString()).compareTo((BigDecimal)right); } 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) { @@ -924,9 +997,42 @@ public class Criteria implements Predicate { @Override public String toString() { StringBuilder sb = new StringBuilder(); - sb.append(left.toString()) - .append(criteriaType.toString()) - .append(wrapString(right)); + + Iterator i = criteriaChain.iterator(); + while (i.hasNext()) { + Criteria c = i.next(); + + if (CriteriaType.NOT_EMPTY == c.criteriaType) { + sb.append(c.left.toString()) + .append(" ") + .append(c.criteriaType.toString()); + + } else if (CriteriaType.EXISTS == c.criteriaType) { + Boolean exists = (Boolean)c.right; + if(exists.booleanValue()){ + sb.append(c.left.toString()); + } else { + sb.append("!").append(c.left.toString()); + } + } else if (CriteriaType.TYPE == c.criteriaType) { + Class tp = (Class) c.right; + sb.append(c.left.toString()) + .append(" ") + .append(c.criteriaType.toString()) + .append(" ") + .append(tp.getCanonicalName()); + } else { + sb.append(c.left.toString()) + .append(" ") + .append(c.criteriaType.toString()) + .append(" ") + .append(wrapString(c.right)); + } + + if (i.hasNext()) { + sb.append(" && "); + } + } return sb.toString(); } @@ -935,11 +1041,29 @@ public class Criteria implements Predicate { return "null"; } if (o instanceof String) { - return "'" + o.toString() + "'"; - } else { - return o.toString(); + String s = o.toString(); + return "'" + s + "'"; +// if(Utils.isNumeric(s)){ +// return s; +// } else { +// return "'" + s + "'"; +// } + } + if (o instanceof Collection) { + StringBuilder sb = new StringBuilder(); + sb.append("["); + + Iterator i = ((Collection) o).iterator(); + while (i.hasNext()) { + sb.append(wrapString(i.next())); + if (i.hasNext()) { + sb.append(","); + } + } + sb.append("]"); + return sb.toString(); } - } - + return o.toString(); + } } diff --git a/json-path/src/main/java/com/jayway/jsonpath/Filter.java b/json-path/src/main/java/com/jayway/jsonpath/Filter.java index 26c7ed1e..8d75761a 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/Filter.java +++ b/json-path/src/main/java/com/jayway/jsonpath/Filter.java @@ -14,12 +14,12 @@ */ package com.jayway.jsonpath; -import com.jayway.jsonpath.internal.Utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Collection; +import java.util.Iterator; import java.util.Stack; import java.util.regex.Pattern; @@ -82,7 +82,7 @@ public abstract class Filter implements Predicate { @Override public String toString() { - return predicate.toString(); + return "[?(" + predicate.toString() + ")]"; } } @@ -116,7 +116,23 @@ public abstract class Filter implements Predicate { @Override public String toString() { - return "(" + Utils.join(" && ", predicates) + ")"; + Iterator i = predicates.iterator(); + StringBuilder sb = new StringBuilder(); + sb.append("[?("); + while (i.hasNext()){ + String p = i.next().toString(); + + if(p.startsWith("[?(")){ + p = p.substring(3, p.length() - 2); + } + sb.append(p); + + if(i.hasNext()){ + sb.append(" && "); + } + } + sb.append(")]"); + return sb.toString(); } } @@ -142,8 +158,25 @@ public abstract class Filter implements Predicate { @Override public String toString() { - return "(" + left.toString() + " || " + right.toString() + ")"; + StringBuilder sb = new StringBuilder(); + sb.append("[?("); + + String l = left.toString(); + String r = right.toString(); + + if(l.startsWith("[?(")){ + l = l.substring(3, l.length() - 2); + } + if(r.startsWith("[?(")){ + r = r.substring(3, r.length() - 2); + } + + sb.append(l).append(" || ").append(r); + + sb.append(")]"); + return sb.toString(); } + } diff --git a/json-path/src/main/java/com/jayway/jsonpath/JsonPath.java b/json-path/src/main/java/com/jayway/jsonpath/JsonPath.java index 47c4a9f5..f423e2d0 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/JsonPath.java +++ b/json-path/src/main/java/com/jayway/jsonpath/JsonPath.java @@ -528,6 +528,7 @@ public class JsonPath { * @param provider jsonProvider to use when parsing JSON * @return a parsing context based on given jsonProvider */ + @Deprecated public static ParseContext using(JsonProvider provider) { return new JsonReader(Configuration.builder().jsonProvider(provider).build()); } diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/JsonFormatter.java b/json-path/src/main/java/com/jayway/jsonpath/internal/JsonFormatter.java index 01f043c0..f6d7b79d 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/JsonFormatter.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/JsonFormatter.java @@ -20,11 +20,11 @@ public class JsonFormatter { private static final String NEW_LINE = System.getProperty("line.separator"); - private static final int MODE_SINGLE = 100; - private static final int MODE_DOUBLE = 101; - private static final int MODE_ESCAPE_SINGLE = 102; - private static final int MODE_ESCAPE_DOUBLE = 103; - private static final int MODE_BETWEEN = 104; + private static final int MODE_SINGLE = 100; + private static final int MODE_DOUBLE = 101; + private static final int MODE_ESCAPE_SINGLE = 102; + private static final int MODE_ESCAPE_DOUBLE = 103; + private static final int MODE_BETWEEN = 104; private static void appendIndent(StringBuilder sb, int count) { for (; count > 0; --count) sb.append(INDENT); @@ -35,80 +35,80 @@ public class JsonFormatter { input = input.replaceAll("[\\r\\n]", ""); StringBuilder output = new StringBuilder(input.length() * 2); - int mode = MODE_BETWEEN; + int mode = MODE_BETWEEN; int depth = 0; for (int i = 0; i < input.length(); ++i) { char ch = input.charAt(i); - switch (mode) { - case MODE_BETWEEN: - switch (ch) { - case '{': - case '[': - output.append(ch); - output.append(NEW_LINE); - appendIndent(output, ++depth); - break; - case '}': - case ']': - output.append(NEW_LINE); - appendIndent(output, --depth); - output.append(ch); - break; - case ',': - output.append(ch); - output.append(NEW_LINE); - appendIndent(output, depth); - break; - case ':': - output.append(" : "); - break; - case '\'': - output.append(ch); - mode = MODE_SINGLE; - break; - case '"': - output.append(ch); - mode = MODE_DOUBLE; - break; - case ' ': - break; - default: - output.append(ch); - break; - } - break; - case MODE_ESCAPE_SINGLE: - output.append(ch); - mode = MODE_SINGLE; - break; - case MODE_ESCAPE_DOUBLE: - output.append(ch); - mode = MODE_DOUBLE; - break; - case MODE_SINGLE: - output.append(ch); - switch (ch) { - case '\'': - mode = MODE_BETWEEN; - break; - case '\\': - mode = MODE_ESCAPE_SINGLE; - break; - } - break; - case MODE_DOUBLE: - output.append(ch); - switch (ch) { - case '"': - mode = MODE_BETWEEN; - break; - case '\\': - mode = MODE_ESCAPE_DOUBLE; - break; - } - break; + switch (mode) { + case MODE_BETWEEN: + switch (ch) { + case '{': + case '[': + output.append(ch); + output.append(NEW_LINE); + appendIndent(output, ++depth); + break; + case '}': + case ']': + output.append(NEW_LINE); + appendIndent(output, --depth); + output.append(ch); + break; + case ',': + output.append(ch); + output.append(NEW_LINE); + appendIndent(output, depth); + break; + case ':': + output.append(" : "); + break; + case '\'': + output.append(ch); + mode = MODE_SINGLE; + break; + case '"': + output.append(ch); + mode = MODE_DOUBLE; + break; + case ' ': + break; + default: + output.append(ch); + break; + } + break; + case MODE_ESCAPE_SINGLE: + output.append(ch); + mode = MODE_SINGLE; + break; + case MODE_ESCAPE_DOUBLE: + output.append(ch); + mode = MODE_DOUBLE; + break; + case MODE_SINGLE: + output.append(ch); + switch (ch) { + case '\'': + mode = MODE_BETWEEN; + break; + case '\\': + mode = MODE_ESCAPE_SINGLE; + break; + } + break; + case MODE_DOUBLE: + output.append(ch); + switch (ch) { + case '"': + mode = MODE_BETWEEN; + break; + case '\\': + mode = MODE_ESCAPE_DOUBLE; + break; + } + break; } } return output.toString(); diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/PathCompiler.java b/json-path/src/main/java/com/jayway/jsonpath/internal/PathCompiler.java index e6b73385..f9056d78 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/PathCompiler.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/PathCompiler.java @@ -1,505 +1,567 @@ -/* - * 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.internal; import com.jayway.jsonpath.Filter; import com.jayway.jsonpath.InvalidPathException; import com.jayway.jsonpath.Predicate; -import com.jayway.jsonpath.internal.token.ArrayPathToken; -import com.jayway.jsonpath.internal.token.PathToken; -import com.jayway.jsonpath.internal.token.PredicatePathToken; -import com.jayway.jsonpath.internal.token.PropertyPathToken; +import com.jayway.jsonpath.internal.token.ArrayIndexOperation; +import com.jayway.jsonpath.internal.token.ArraySliceOperation; +import com.jayway.jsonpath.internal.token.PathTokenAppender; +import com.jayway.jsonpath.internal.token.PathTokenFactory; import com.jayway.jsonpath.internal.token.RootPathToken; -import com.jayway.jsonpath.internal.token.ScanPathToken; -import com.jayway.jsonpath.internal.token.WildcardPathToken; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; +import java.util.Collection; import java.util.LinkedList; import java.util.List; -import java.util.regex.Pattern; -import static com.jayway.jsonpath.internal.Utils.notEmpty; +import static java.lang.Character.isDigit; +import static java.lang.Math.min; import static java.util.Arrays.asList; public class PathCompiler { private static final Logger logger = LoggerFactory.getLogger(PathCompiler.class); - private static final String PROPERTY_OPEN = "['"; - private static final String PROPERTY_CLOSE = "']"; - private static final char DOCUMENT = '$'; - private static final char ANY = '*'; + private static final char DOC_CONTEXT = '$'; + private static final char EVAL_CONTEXT = '@'; + private static final char OPEN_SQUARE_BRACKET = '['; + private static final char CLOSE_SQUARE_BRACKET = ']'; + private static final char OPEN_BRACKET = '('; + private static final char CLOSE_BRACKET = ')'; + private static final char WILDCARD = '*'; private static final char PERIOD = '.'; - private static final char BRACKET_OPEN = '['; - private static final char BRACKET_CLOSE = ']'; private static final char SPACE = ' '; - private static final Cache cache = new Cache(200); - - - public static Path compile(final String path, final Predicate... filters) { + private static final char QUESTIONMARK = '?'; + private static final char COMMA = ','; + private static final char SPLIT = ':'; + private static final char MINUS = '-'; + private static final char ESCAPE = '\\'; + private static final char TICK = '\''; - notEmpty(path, "Path may not be null empty"); - try { - String trimmedPath = path.trim(); + private static final Cache cache = new Cache(200); - if (trimmedPath.endsWith("..")) { - throw new InvalidPathException("A path can not end with a scan."); - } + private final LinkedList filterStack; + private final CharacterIndex path; - LinkedList filterList = new LinkedList(asList(filters)); + private PathCompiler(String path, LinkedList filterStack) { + this.filterStack = filterStack; + this.path = new CharacterIndex(path); + } - if (trimmedPath.charAt(0) != '$' && trimmedPath.charAt(0) != '@') { - trimmedPath = Utils.concat("$.", trimmedPath); - } + private Path compile() { + RootPathToken root = readContextToken(); + return new CompiledPath(root, root.getPathFragment().equals("$")); + } - boolean isRootPath = (trimmedPath.charAt(0) == '$'); + public static Path compile(String path, final Predicate... filters) { + try { + path = path.trim(); - if (trimmedPath.charAt(0) == '@') { - trimmedPath = Utils.concat("$", trimmedPath.substring(1)); + if(!path.startsWith("$") && !path.startsWith("@")){ + path = "$." + path; } - - if (trimmedPath.length() > 1 && - trimmedPath.charAt(1) != '.' && - trimmedPath.charAt(1) != '[') { - throw new InvalidPathException("Invalid path " + trimmedPath); + if(path.endsWith("..")){ + fail("Path must not end wid a scan operation '..'"); } - - String cacheKey = Utils.concat(trimmedPath, Boolean.toString(isRootPath), filterList.toString()); + LinkedList filterStack = new LinkedList(asList(filters)); + String cacheKey = Utils.concat(path, filterStack.toString()); Path p = cache.get(cacheKey); - if (p != null) { - if (logger.isDebugEnabled()) logger.debug("Using cached path: {}", cacheKey); - return p; + if (p == null) { + p = new PathCompiler(path.trim(), filterStack).compile(); + cache.put(cacheKey, p); + } + return p; + } catch (Exception e) { + InvalidPathException ipe; + if (e instanceof InvalidPathException) { + ipe = (InvalidPathException) e; + } else { + ipe = new InvalidPathException(e); } + throw ipe; + } + } - RootPathToken root = null; - - - int i = 0; - int positions; - String fragment = ""; - - do { - char current = trimmedPath.charAt(i); - - switch (current) { - case SPACE: - throw new InvalidPathException("Space not allowed in path"); - case DOCUMENT: - fragment = "$"; - i++; - break; - case BRACKET_OPEN: - positions = fastForwardUntilClosed(trimmedPath, i); - fragment = trimmedPath.substring(i, i + positions); - i += positions; - break; - case PERIOD: - i++; - if ( i < trimmedPath.length() && trimmedPath.charAt(i) == PERIOD) { - //This is a deep scan - fragment = ".."; - i++; - } else { - positions = fastForward(trimmedPath, i); - if (positions == 0) { - continue; - - } else if (positions == 1 && trimmedPath.charAt(i) == '*') { - fragment = new String("[*]"); - } else { - assertValidFieldChars(trimmedPath, i, positions); - - fragment = Utils.concat(PROPERTY_OPEN, trimmedPath.substring(i, i + positions), PROPERTY_CLOSE); - } - i += positions; - } - break; - case ANY: - fragment = new String("[*]"); - i++; - break; - default: - positions = fastForward(trimmedPath, i); - - fragment = Utils.concat(PROPERTY_OPEN, trimmedPath.substring(i, i + positions), PROPERTY_CLOSE); - i += positions; - break; - } - if (root == null) { - root = (RootPathToken) PathComponentAnalyzer.analyze(fragment, filterList); - } else { - root.append(PathComponentAnalyzer.analyze(fragment, filterList)); - } + //[$ | @] + private RootPathToken readContextToken() { - } while (i < trimmedPath.length()); + if (!path.currentCharIs(DOC_CONTEXT) && !path.currentCharIs(EVAL_CONTEXT)) { + throw new InvalidPathException("Path must start with '$' or '@'"); + } - Path pa = new CompiledPath(root, isRootPath); + RootPathToken pathToken = PathTokenFactory.createRootPathToken(path.currentChar()); + PathTokenAppender appender = pathToken.getPathTokenAppender(); - cache.put(cacheKey, pa); + if (path.currentIsTail()) { + return pathToken; + } - return pa; + path.incrementPosition(1); - } catch (Exception ex){ - throw new InvalidPathException(ex); + if(path.currentChar() != PERIOD && path.currentChar() != OPEN_SQUARE_BRACKET){ + fail("Illegal character at position " + path.position + " expected '.' or '["); } + + readNextToken(appender); + + return pathToken; } - private static void assertValidFieldChars(String s, int start, int positions) { - /* - int i = start; - while (i < start + positions) { - char c = s.charAt(i); + // + // + // + private boolean readNextToken(PathTokenAppender appender) { + + char c = path.currentChar(); + + switch (c) { + case OPEN_SQUARE_BRACKET: + return readBracketPropertyToken(appender) || + readArrayToken(appender) || + readWildCardToken(appender) || + readFilterToken(appender) || + readPlaceholderToken(appender) || + fail("Could not parse bracket statement at position " + path.position); + case PERIOD: + return readDotSeparatorToken(appender) || + readScanToken(appender) || + fail("Could not parse token at position " + path.position); + case WILDCARD: + return readWildCardToken(appender) || + fail("Could not parse token at position " + path.position); + default: + return readPropertyToken(appender) || + fail("Could not parse token at position " + path.position); + } + } - if (!Character.isLetterOrDigit(c) && c != '-' && c != '_' && c != '$' && c != '@') { - throw new InvalidPathException("Invalid field name! Use bracket notation if your filed names does not match pattern: ([a-zA-Z@][a-zA-Z0-9@\\$_\\-]*)$"); - } - i++; + // + // . + // + private boolean readDotSeparatorToken(PathTokenAppender appender) { + if (!path.currentCharIs('.') || path.nextCharIs('.')) { + return false; + } + if (!path.hasMoreCharacters()) { + throw new InvalidPathException("Path must not end with a '."); } - */ +// if (path.nextSignificantCharIs('[')) { +// throw new InvalidPathException("A bracket may not follow a '."); +// } + + path.incrementPosition(1); + + return readNextToken(appender); } - private static int fastForward(String s, int index) { - int skipCount = 0; - while (index < s.length()) { - char current = s.charAt(index); - if (current == PERIOD || current == BRACKET_OPEN || current == SPACE) { + // + // fooBar + // + private boolean readPropertyToken(PathTokenAppender appender) { + if (path.currentCharIs(OPEN_SQUARE_BRACKET) || path.currentCharIs(WILDCARD) || path.currentCharIs(PERIOD) || path.currentCharIs(SPACE)) { + return false; + } + int startPosition = path.position; + int readPosition = startPosition; + int endPosition = 0; + + while (path.inBounds(readPosition)) { + char c = path.charAt(readPosition); + if (c == SPACE) { + throw new InvalidPathException("Use bracket notion ['my prop'] if your property contains blank characters. position: " + path.position); + } + if (c == PERIOD || c == OPEN_SQUARE_BRACKET) { + endPosition = readPosition; break; } - index++; - skipCount++; + readPosition++; } - return skipCount; + if (endPosition == 0) { + endPosition = path.length(); + } + + path.setPosition(endPosition); + + String property = path.subSequence(startPosition, endPosition).toString(); + + appender.appendPathToken(PathTokenFactory.createSinglePropertyPathToken(property)); + + return path.currentIsTail() || readNextToken(appender); } - private static int fastForwardUntilClosed(String s, int index) { - int skipCount = 0; - int nestedBrackets = 0; + // + // [?], [?,?, ..] + // + private boolean readPlaceholderToken(PathTokenAppender appender) { - //First char is always '[' no need to check it - index++; - skipCount++; + if (!path.currentCharIs(OPEN_SQUARE_BRACKET)) { + return false; + } + int questionmarkIndex = path.indexOfNextSignificantChar(QUESTIONMARK); + if (questionmarkIndex == -1) { + return false; + } + char nextSignificantChar = path.nextSignificantChar(questionmarkIndex); + if (nextSignificantChar != CLOSE_SQUARE_BRACKET && nextSignificantChar != COMMA) { + return false; + } - while (index < s.length()) { - char current = s.charAt(index); + int expressionBeginIndex = path.position + 1; + int expressionEndIndex = path.nextIndexOf(expressionBeginIndex, CLOSE_SQUARE_BRACKET); - index++; - skipCount++; + if (expressionEndIndex == -1) { + return false; + } - if (current == BRACKET_CLOSE && nestedBrackets == 0) { - break; - } - if (current == BRACKET_OPEN) { - nestedBrackets++; - } - if (current == BRACKET_CLOSE) { - nestedBrackets--; + String expression = path.subSequence(expressionBeginIndex, expressionEndIndex).toString(); + + String[] tokens = expression.split(","); + + if (filterStack.size() < tokens.length) { + throw new InvalidPathException("Not enough predicates supplied for filter [" + expression + "] at position " + path.position); + } + + Collection predicates = new ArrayList(); + for (String token : tokens) { + token = token != null ? token.trim() : token; + if (!"?".equals(token == null ? "" : token)) { + throw new InvalidPathException("Expected '?' but found " + token); } + predicates.add(filterStack.pop()); } - return skipCount; + + appender.appendPathToken(PathTokenFactory.createPredicatePathToken(predicates)); + + path.setPosition(expressionEndIndex + 1); + + return path.currentIsTail() || readNextToken(appender); } + // + // [?(...)] + // + private boolean readFilterToken(PathTokenAppender appender) { + if (!path.currentCharIs(OPEN_SQUARE_BRACKET) && !path.nextSignificantCharIs(QUESTIONMARK)) { + return false; + } + + int openStatementBracketIndex = path.position; + int questionMarkIndex = path.indexOfNextSignificantChar(QUESTIONMARK); + if (questionMarkIndex == -1) { + return false; + } + int openBracketIndex = path.indexOfNextSignificantChar(questionMarkIndex, OPEN_BRACKET); + if (openBracketIndex == -1) { + return false; + } + int closeBracketIndex = path.indexOfClosingBracket(openBracketIndex + 1, true); + if (closeBracketIndex == -1) { + return false; + } + if (!path.nextSignificantCharIs(closeBracketIndex, CLOSE_SQUARE_BRACKET)) { + return false; + } + int closeStatementBracketIndex = path.indexOfNextSignificantChar(closeBracketIndex, CLOSE_SQUARE_BRACKET); + + String criteria = path.subSequence(openStatementBracketIndex, closeStatementBracketIndex + 1).toString(); + + appender.appendPathToken(PathTokenFactory.createPredicatePathToken(Filter.parse(criteria))); + + path.setPosition(closeStatementBracketIndex + 1); + + return path.currentIsTail() || readNextToken(appender); + + } - //--------------------------------------------- // + // [*] + // * // + private boolean readWildCardToken(PathTokenAppender appender) { + + boolean inBracket = path.currentCharIs(OPEN_SQUARE_BRACKET); + + if (inBracket && !path.nextSignificantCharIs(WILDCARD)) { + return false; + } + if (!path.currentCharIs(WILDCARD) && path.isOutOfBounds(path.position + 1)) { + return false; + } + if (inBracket) { + int wildCardIndex = path.indexOfNextSignificantChar(WILDCARD); + if (!path.nextSignificantCharIs(wildCardIndex, CLOSE_SQUARE_BRACKET)) { + throw new InvalidPathException("Expected wildcard token to end with ']' on position " + wildCardIndex + 1); + } + int bracketCloseIndex = path.indexOfNextSignificantChar(wildCardIndex, CLOSE_SQUARE_BRACKET); + path.setPosition(bracketCloseIndex + 1); + } else { + path.incrementPosition(1); + } + + appender.appendPathToken(PathTokenFactory.createWildCardPathToken()); + + return path.currentIsTail() || readNextToken(appender); + } + + // + // [1], [1,2, n], [1:], [1:2], [:2] // - //--------------------------------------------- - static class PathComponentAnalyzer { + private boolean readArrayToken(PathTokenAppender appender) { - private static final Pattern FILTER_PATTERN = Pattern.compile("^\\[\\s*\\?\\s*[,\\s*\\?]*?\\s*]$"); //[?] or [?, ?, ...] - private int i; - private char current; + if (!path.currentCharIs(OPEN_SQUARE_BRACKET)) { + return false; + } + char nextSignificantChar = path.nextSignificantChar(); + if (!isDigit(nextSignificantChar) && nextSignificantChar != MINUS && nextSignificantChar != SPLIT) { + return false; + } - private final LinkedList filterList; - private final String pathFragment; + int expressionBeginIndex = path.position + 1; + int expressionEndIndex = path.nextIndexOf(expressionBeginIndex, CLOSE_SQUARE_BRACKET); - PathComponentAnalyzer(String pathFragment, LinkedList filterList) { - this.pathFragment = pathFragment; - this.filterList = filterList; + if (expressionEndIndex == -1) { + return false; } - static PathToken analyze(String pathFragment, LinkedList filterList) { - return new PathComponentAnalyzer(pathFragment, filterList).analyze(); + String expression = path.subSequence(expressionBeginIndex, expressionEndIndex).toString().replace(" ", ""); + + if ("*".equals(expression)) { + return false; } - public PathToken analyze() { + //check valid chars + for (int i = 0; i < expression.length(); i++) { + char c = expression.charAt(i); + if (!isDigit(c) && c != COMMA && c != MINUS && c != SPLIT) { + return false; + } + } + + boolean isSliceOperation = expression.contains(":"); + + if (isSliceOperation) { + ArraySliceOperation arraySliceOperation = ArraySliceOperation.parse(expression); + appender.appendPathToken(PathTokenFactory.createSliceArrayPathToken(arraySliceOperation)); + } else { + ArrayIndexOperation arrayIndexOperation = ArrayIndexOperation.parse(expression); + appender.appendPathToken(PathTokenFactory.createIndexArrayPathToken(arrayIndexOperation)); + } - if ("$".equals(pathFragment)) return new RootPathToken(); - else if ("..".equals(pathFragment)) return new ScanPathToken(); - else if ("[*]".equals(pathFragment)) return new WildcardPathToken(); - else if (".*".equals(pathFragment)) return new WildcardPathToken(); - else if ("[?]".equals(pathFragment)) return new PredicatePathToken(filterList.poll()); + path.setPosition(expressionEndIndex + 1); - else if (FILTER_PATTERN.matcher(pathFragment).matches()) { - final int criteriaCount = Utils.countMatches(pathFragment, "?"); - List filters = new ArrayList(criteriaCount); - for (int i = 0; i < criteriaCount; i++) { - filters.add(filterList.poll()); + return path.currentIsTail() || readNextToken(appender); + } + + // + // ['foo'] + // + private boolean readBracketPropertyToken(PathTokenAppender appender) { + if (!path.currentCharIs(OPEN_SQUARE_BRACKET) || !path.nextSignificantCharIs(TICK)) { + return false; + } + + List properties = new ArrayList(); + + int startPosition = path.position + 1; + int readPosition = startPosition; + int endPosition = 0; + boolean inProperty = false; + + while (path.inBounds(readPosition)) { + char c = path.charAt(readPosition); + + if (c == CLOSE_SQUARE_BRACKET) { + if (inProperty) { + throw new InvalidPathException("Expected property to be closed at position " + readPosition); + } + break; + } else if (c == TICK) { + if (inProperty) { + endPosition = readPosition; + properties.add(path.subSequence(startPosition, endPosition).toString()); + inProperty = false; + } else { + startPosition = readPosition + 1; + inProperty = true; } - return new PredicatePathToken(filters); } + readPosition++; + } - this.i = 0; - do { - current = pathFragment.charAt(i); - - switch (current) { - case '?': - return analyzeCriteriaSequence4(); - case '\'': - return analyzeProperty(); - default: - if (Character.isDigit(current) || current == ':' || current == '-' || current == '@') { - return analyzeArraySequence(); - } - i++; - break; - } + int endBracketIndex = path.indexOfNextSignificantChar(endPosition, CLOSE_SQUARE_BRACKET) + 1; + path.setPosition(endBracketIndex); - } while (i < pathFragment.length()); + appender.appendPathToken(PathTokenFactory.createPropertyPathToken(properties)); - throw new InvalidPathException("Could not analyze path component: " + pathFragment); + return path.currentIsTail() || readNextToken(appender); + } + + // + // .. + // + private boolean readScanToken(PathTokenAppender appender) { + if (!path.currentCharIs(PERIOD) || !path.nextCharIs(PERIOD)) { + return false; } + appender.appendPathToken(PathTokenFactory.crateScanToken()); + path.incrementPosition(2); + + return readNextToken(appender); + } - public PathToken analyzeCriteriaSequence4() { - int[] bounds = findFilterBounds(); - i = bounds[1]; + public static boolean fail(String message) { + throw new InvalidPathException(message); + } - return new PredicatePathToken(Filter.parse(pathFragment.substring(bounds[0], bounds[1]))); - } - int[] findFilterBounds(){ - int end = 0; - int start = i; + private static class CharacterIndex { - while(pathFragment.charAt(start) != '['){ - start--; - } + private final CharSequence charSequence; + private int position; - int mem = ' '; - int curr = start; - boolean inProp = false; - int openSquareBracket = 0; - int openBrackets = 0; - while(end == 0){ - char c = pathFragment.charAt(curr); - switch (c){ - case '(': - if(!inProp) openBrackets++; - break; - case ')': - if(!inProp) openBrackets--; - break; - case '[': - if(!inProp) openSquareBracket++; - break; - case ']': - if(!inProp){ - openSquareBracket--; - if(openBrackets == 0){ - end = curr + 1; - } - } - break; - case '\'': - if(mem == '\\') { - break; - } - inProp = !inProp; - break; - default: - break; - } - mem = c; - curr++; - } - if(openBrackets != 0 || openSquareBracket != 0){ - throw new InvalidPathException("Filter brackets are not balanced"); - } - return new int[]{start, end}; + private CharacterIndex(CharSequence charSequence) { + this.charSequence = charSequence; + this.position = 0; } + private int length() { + return charSequence.length(); + } + private char charAt(int idx) { + return charSequence.charAt(idx); + } + private char currentChar() { + return charSequence.charAt(position); + } - //"['foo']" - private PathToken analyzeProperty() { - List properties = new ArrayList(); - StringBuilder buffer = new StringBuilder(); + private boolean currentCharIs(char c) { + return (charSequence.charAt(position) == c); + } - boolean propertyIsOpen = false; + private boolean nextCharIs(char c) { + return inBounds(position + 1) && (charSequence.charAt(position + 1) == c); + } - while (current != ']') { - switch (current) { - case '\'': - if (propertyIsOpen) { - properties.add(buffer.toString()); - buffer.setLength(0); - propertyIsOpen = false; - } else { - propertyIsOpen = true; - } - break; - default: - if (propertyIsOpen) { - buffer.append(current); + private int incrementPosition(int charCount) { + return setPosition(position + charCount); + } + + private int setPosition(int newPosition) { + position = min(newPosition, charSequence.length() - 1); + return position; + } + + private int indexOfClosingBracket(int startPosition, boolean skipStrings) { + int opened = 1; + int readPosition = startPosition; + while (inBounds(readPosition)) { + if (skipStrings) { + if (charAt(readPosition) == TICK) { + while (inBounds(readPosition)) { + readPosition++; + if (charAt(readPosition) == TICK && charAt(readPosition - 1) != ESCAPE) { + readPosition++; + break; + } } - break; - } - current = pathFragment.charAt(++i); - } - return new PropertyPathToken(properties); - } - - - //"[-1:]" sliceFrom - //"[:1]" sliceTo - //"[0:5]" sliceBetween - //"[1]" - //"[1,2,3]" - //"[(@.length - 1)]" - private PathToken analyzeArraySequence() { - StringBuilder buffer = new StringBuilder(); - List numbers = new ArrayList(); - - boolean contextSize = (current == '@'); - boolean sliceTo = false; - boolean sliceFrom = false; - boolean sliceBetween = false; - boolean indexSequence = false; - boolean singleIndex = false; - - if (contextSize) { - - current = pathFragment.charAt(++i); - current = pathFragment.charAt(++i); - while (current != '-') { - if (current == ' ' || current == '(' || current == ')') { - current = pathFragment.charAt(++i); - continue; } - buffer.append(current); - current = pathFragment.charAt(++i); } - String function = buffer.toString(); - buffer.setLength(0); - if (!function.equals("size") && !function.equals("length")) { - throw new InvalidPathException("Invalid function: @." + function + ". Supported functions are: [(@.length - n)] and [(@.size() - n)]"); + if (charAt(readPosition) == OPEN_BRACKET) { + opened++; } - while (current != ')') { - if (current == ' ') { - current = pathFragment.charAt(++i); - continue; + if (charAt(readPosition) == CLOSE_BRACKET) { + opened--; + if(opened == 0){ + return readPosition; } - buffer.append(current); - current = pathFragment.charAt(++i); } + readPosition++; + } + return -1; + } - } else { + public int indexOfNextSignificantChar(char c) { + return indexOfNextSignificantChar(position, c); + } + public int indexOfNextSignificantChar(int startPosition, char c) { + int readPosition = startPosition + 1; + while (!isOutOfBounds(readPosition) && charAt(readPosition) == SPACE) { + readPosition++; + } + if (charAt(readPosition) == c) { + return readPosition; + } else { + return -1; + } + } - while (Character.isDigit(current) || current == ',' || current == ' ' || current == ':' || current == '-') { - - switch (current) { - case ' ': - break; - case ':': - if (buffer.length() == 0) { - //this is a tail slice [:12] - sliceTo = true; - current = pathFragment.charAt(++i); - while (Character.isDigit(current) || current == ' ' || current == '-') { - if (current != ' ') { - buffer.append(current); - } - current = pathFragment.charAt(++i); - } - numbers.add(Integer.parseInt(buffer.toString())); - buffer.setLength(0); - } else { - //we now this starts with [12:??? - numbers.add(Integer.parseInt(buffer.toString())); - buffer.setLength(0); - current = pathFragment.charAt(++i); - - //this is a tail slice [:12] - while (Character.isDigit(current) || current == ' ' || current == '-') { - if (current != ' ') { - buffer.append(current); - } - current = pathFragment.charAt(++i); - } - - if (buffer.length() == 0) { - sliceFrom = true; - } else { - sliceBetween = true; - numbers.add(Integer.parseInt(buffer.toString())); - buffer.setLength(0); - } - } - break; - case ',': - numbers.add(Integer.parseInt(buffer.toString())); - buffer.setLength(0); - indexSequence = true; - break; - default: - buffer.append(current); - break; - } - if (current == ']') { - break; - } - current = pathFragment.charAt(++i); + public int nextIndexOf(int startPosition, char c) { + int readPosition = startPosition; + while (!isOutOfBounds(readPosition)) { + if (charAt(readPosition) == c) { + return readPosition; } + readPosition++; + } + return -1; + } + + public boolean nextSignificantCharIs(int startPosition, char c) { + int readPosition = startPosition + 1; + while (!isOutOfBounds(readPosition) && charAt(readPosition) == SPACE) { + readPosition++; } - if (buffer.length() > 0) { - numbers.add(Integer.parseInt(buffer.toString())); + return !isOutOfBounds(readPosition) && charAt(readPosition) == c; + } + + public boolean nextSignificantCharIs(char c) { + return nextSignificantCharIs(position, c); + } + + public char nextSignificantChar() { + return nextSignificantChar(position); + } + + public char nextSignificantChar(int startPosition) { + int readPosition = startPosition + 1; + while (!isOutOfBounds(readPosition) && charAt(readPosition) == SPACE) { + readPosition++; } - singleIndex = (numbers.size() == 1) && !sliceTo && !sliceFrom && !contextSize; - - if (logger.isTraceEnabled()) { - logger.debug("numbers are : {}", numbers.toString()); - logger.debug("sequence is singleNumber : {}", singleIndex); - logger.debug("sequence is numberSequence : {}", indexSequence); - logger.debug("sequence is sliceFrom : {}", sliceFrom); - logger.debug("sequence is sliceTo : {}", sliceTo); - logger.debug("sequence is sliceBetween : {}", sliceBetween); - logger.debug("sequence is contextFetch : {}", contextSize); - logger.debug("---------------------------------------------"); + if (!isOutOfBounds(readPosition)) { + return charAt(readPosition); + } else { + return ' '; } - ArrayPathToken.Operation operation = null; + } + + private boolean currentIsTail() { + return isOutOfBounds(position + 1); + } - if (singleIndex) operation = ArrayPathToken.Operation.SINGLE_INDEX; - else if (indexSequence) operation = ArrayPathToken.Operation.INDEX_SEQUENCE; - else if (sliceFrom) operation = ArrayPathToken.Operation.SLICE_FROM; - else if (sliceTo) operation = ArrayPathToken.Operation.SLICE_TO; - else if (sliceBetween) operation = ArrayPathToken.Operation.SLICE_BETWEEN; - else if (contextSize) operation = ArrayPathToken.Operation.CONTEXT_SIZE; + private boolean hasMoreCharacters() { + return inBounds(position + 1); + } - assert operation != null; + private boolean inBounds(int idx) { + return (idx >= 0) && (idx < charSequence.length()); + } - return new ArrayPathToken(numbers, operation); + private boolean isOutOfBounds(int idx) { + return !inBounds(idx); + } + private CharSequence subSequence(int start, int end) { + return charSequence.subSequence(start, end); } } +} + -} \ No newline at end of file diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/Utils.java b/json-path/src/main/java/com/jayway/jsonpath/internal/Utils.java index dd46f40b..5a1029ff 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/Utils.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/Utils.java @@ -145,7 +145,7 @@ public final class Utils { } public static boolean isNumeric(String str) { - if (str == null) { + if (str == null || str.trim().isEmpty()) { return false; } int sz = str.length(); diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/token/ArrayIndexOperation.java b/json-path/src/main/java/com/jayway/jsonpath/internal/token/ArrayIndexOperation.java new file mode 100644 index 00000000..0a54c09d --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/token/ArrayIndexOperation.java @@ -0,0 +1,63 @@ +package com.jayway.jsonpath.internal.token; + +import com.jayway.jsonpath.InvalidPathException; +import com.jayway.jsonpath.internal.Utils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static java.lang.Character.isDigit; + +public class ArrayIndexOperation { + + private final List indexes; + + private ArrayIndexOperation(List indexes) { + this.indexes = Collections.unmodifiableList(indexes); + } + + public List indexes() { + return indexes; + } + + public boolean isSingleIndexOperation(){ + return indexes.size() == 1; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("["); + sb.append(Utils.join(",", indexes)); + sb.append("]"); + + return sb.toString(); + } + + public static ArrayIndexOperation parse(String operation) { + //check valid chars + for (int i = 0; i < operation.length(); i++) { + char c = operation.charAt(i); + if (!isDigit(c) && c != ',') { + throw new InvalidPathException("Failed to parse ArrayIndexOperation: " + operation); + } + } + String[] tokens = operation.split(","); + + List tempIndexes = new ArrayList(); + for (String token : tokens) { + tempIndexes.add(parseInteger(token)); + } + + return new ArrayIndexOperation(tempIndexes); + } + + private static Integer parseInteger(String token) { + try { + return Integer.parseInt(token); + } catch (Exception e){ + throw new InvalidPathException("Failed to parse token in ArrayIndexOperation: " + token, e); + } + } +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/token/ArrayPathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/token/ArrayPathToken.java index 1928d91d..af6f1077 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/token/ArrayPathToken.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/token/ArrayPathToken.java @@ -17,12 +17,9 @@ package com.jayway.jsonpath.internal.token; import com.jayway.jsonpath.InvalidPathException; import com.jayway.jsonpath.PathNotFoundException; import com.jayway.jsonpath.internal.PathRef; -import com.jayway.jsonpath.internal.Utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.List; - import static java.lang.String.format; /** @@ -32,23 +29,17 @@ public class ArrayPathToken extends PathToken { private static final Logger logger = LoggerFactory.getLogger(ArrayPathToken.class); - public static enum Operation { - CONTEXT_SIZE, - SLICE_TO, - SLICE_FROM, - SLICE_BETWEEN, - INDEX_SEQUENCE, - SINGLE_INDEX; - } + private final ArraySliceOperation arraySliceOperation; + private final ArrayIndexOperation arrayIndexOperation; - private final List criteria; - private final Operation operation; - private final boolean isDefinite; + ArrayPathToken(final ArraySliceOperation arraySliceOperation) { + this.arraySliceOperation = arraySliceOperation; + this.arrayIndexOperation = null; + } - public ArrayPathToken(List criteria, Operation operation) { - this.criteria = criteria; - this.operation = operation; - this.isDefinite = (Operation.SINGLE_INDEX == operation || Operation.CONTEXT_SIZE == operation); + ArrayPathToken(final ArrayIndexOperation arrayIndexOperation) { + this.arrayIndexOperation = arrayIndexOperation; + this.arraySliceOperation = null; } @Override @@ -59,127 +50,125 @@ public class ArrayPathToken extends PathToken { if (!ctx.jsonProvider().isArray(model)) { throw new InvalidPathException(format("Filter: %s can only be applied to arrays. Current context is: %s", toString(), model)); } + if(arraySliceOperation != null){ + evaluateSliceOperation(currentPath, parent, model, ctx); + } else { + evaluateIndexOperation(currentPath, parent, model, ctx); + } + + } + + public void evaluateIndexOperation(String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx) { + + if (model == null) { + throw new PathNotFoundException("The path " + currentPath + " is null"); + } + if (!ctx.jsonProvider().isArray(model)) { + throw new InvalidPathException(format("Filter: %s can only be applied to arrays. Current context is: %s", toString(), model)); + } + + if(arrayIndexOperation.isSingleIndexOperation()){ + handleArrayIndex(arrayIndexOperation.indexes().get(0), currentPath, model, ctx); + } else { + for (Integer index : arrayIndexOperation.indexes()) { + handleArrayIndex(index, currentPath, model, ctx); + } + } + } + + public void evaluateSliceOperation(String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx) { + + if (model == null) { + throw new PathNotFoundException("The path " + currentPath + " is null"); + } + if (!ctx.jsonProvider().isArray(model)) { + throw new InvalidPathException(format("Filter: %s can only be applied to arrays. Current context is: %s", toString(), model)); + } - try { - int idx; - int input; - int length; - int from; - int to; - - switch (operation){ - case SINGLE_INDEX: - handleArrayIndex(criteria.get(0), currentPath, model, ctx); - break; - - case INDEX_SEQUENCE: - for (Integer i : criteria) { - handleArrayIndex(i, currentPath, model, ctx); - } - break; - - case CONTEXT_SIZE: - length = ctx.jsonProvider().length(model); - idx = length + criteria.get(0); - handleArrayIndex(idx, currentPath, model, ctx); - break; - - case SLICE_FROM: //[2:] - input = criteria.get(0); - length = ctx.jsonProvider().length(model); - from = input; - if (from < 0) { - //calculate slice start from array length - from = length + from; - } - from = Math.max(0, from); - - logger.debug("Slice from index on array with length: {}. From index: {} to: {}. Input: {}", length, from, length - 1, toString()); - - if (length == 0 || from >= length) { - return; - } - for (int i = from; i < length; i++) { - handleArrayIndex(i, currentPath, model, ctx); - } - break; - - case SLICE_TO : //[:2] - input = criteria.get(0); - length = ctx.jsonProvider().length(model); - to = input; - if (to < 0) { - //calculate slice end from array length - to = length + to; - } - to = Math.min(length, to); - - logger.debug("Slice to index on array with length: {}. From index: 0 to: {}. Input: {}", length, to, toString()); - - if (length == 0) { - return; - } - for (int i = 0; i < to; i++) { - handleArrayIndex(i, currentPath, model, ctx); - } - break; - - case SLICE_BETWEEN : //[2:4] - from = criteria.get(0); - to = criteria.get(1); - length = ctx.jsonProvider().length(model); - - to = Math.min(length, to); - - if (from >= to || length == 0) { - return; - } - - logger.debug("Slice between indexes on array with length: {}. From index: {} to: {}. Input: {}", length, from, to, toString()); - - for (int i = from; i < to; i++) { - handleArrayIndex(i, currentPath, model, ctx); - } - break; - } - } catch (IndexOutOfBoundsException e) { - throw new PathNotFoundException("Index out of bounds when evaluating path " + currentPath); + switch (arraySliceOperation.operation()) { + case SLICE_FROM: + sliceFrom(arraySliceOperation, currentPath, parent, model, ctx); + break; + case SLICE_BETWEEN: + sliceBetween(arraySliceOperation, currentPath, parent, model, ctx); + break; + case SLICE_TO: + sliceTo(arraySliceOperation, currentPath, parent, model, ctx); + break; + } + } + + public void sliceFrom(ArraySliceOperation operation, String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx) { + int length = ctx.jsonProvider().length(model); + int from = operation.from(); + if (from < 0) { + //calculate slice start from array length + from = length + from; + } + from = Math.max(0, from); + + logger.debug("Slice from index on array with length: {}. From index: {} to: {}. Input: {}", length, from, length - 1, toString()); + + if (length == 0 || from >= length) { + return; + } + for (int i = from; i < length; i++) { + handleArrayIndex(i, currentPath, model, ctx); + } + } + + public void sliceBetween(ArraySliceOperation operation, String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx) { + int length = ctx.jsonProvider().length(model); + int from = operation.from(); + int to = operation.to(); + + to = Math.min(length, to); + + if (from >= to || length == 0) { + return; + } + + logger.debug("Slice between indexes on array with length: {}. From index: {} to: {}. Input: {}", length, from, to, toString()); + + for (int i = from; i < to; i++) { + handleArrayIndex(i, currentPath, model, ctx); + } + } + + public void sliceTo(ArraySliceOperation operation, String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx) { + int length = ctx.jsonProvider().length(model); + if (length == 0) { + return; + } + int to = operation.to(); + if (to < 0) { + //calculate slice end from array length + to = length + to; + } + to = Math.min(length, to); + + logger.debug("Slice to index on array with length: {}. From index: 0 to: {}. Input: {}", length, to, toString()); + + for (int i = 0; i < to; i++) { + handleArrayIndex(i, currentPath, model, ctx); } } @Override public String getPathFragment() { - StringBuilder sb = new StringBuilder(); - if (Operation.SINGLE_INDEX == operation || Operation.INDEX_SEQUENCE == operation) { - sb.append("[") - .append(Utils.join(",", "", criteria)) - .append("]"); - } else if (Operation.CONTEXT_SIZE == operation) { - sb.append("[@.size()") - .append(criteria.get(0)) - .append("]"); - } else if (Operation.SLICE_FROM == operation) { - sb.append("[") - .append(criteria.get(0)) - .append(":]"); - } else if (Operation.SLICE_TO == operation) { - sb.append("[:") - .append(criteria.get(0)) - .append("]"); - } else if (Operation.SLICE_BETWEEN == operation) { - sb.append("[") - .append(criteria.get(0)) - .append(":") - .append(criteria.get(1)) - .append("]"); - } else - sb.append("NOT IMPLEMENTED"); - - return sb.toString(); + if(arrayIndexOperation != null){ + return arrayIndexOperation.toString(); + } else { + return arraySliceOperation.toString(); + } } @Override - boolean isTokenDefinite() { - return isDefinite; + public boolean isTokenDefinite() { + if(arrayIndexOperation != null){ + return arrayIndexOperation.isSingleIndexOperation(); + } else { + return false; + } } } diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/token/ArraySliceOperation.java b/json-path/src/main/java/com/jayway/jsonpath/internal/token/ArraySliceOperation.java new file mode 100644 index 00000000..f21f6f83 --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/token/ArraySliceOperation.java @@ -0,0 +1,86 @@ +package com.jayway.jsonpath.internal.token; + +import com.jayway.jsonpath.InvalidPathException; + +import static java.lang.Character.isDigit; + +public class ArraySliceOperation { + + public enum Operation { + SLICE_FROM, + SLICE_TO, + SLICE_BETWEEN + } + + private final Integer from; + private final Integer to; + private final Operation operation; + + private ArraySliceOperation(Integer from, Integer to, Operation operation) { + this.from = from; + this.to = to; + this.operation = operation; + } + + public Integer from() { + return from; + } + + public Integer to() { + return to; + } + + public Operation operation() { + return operation; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("["); + sb.append(from == null ? "" : from.toString()); + sb.append(":"); + sb.append(to == null ? "" : to.toString()); + sb.append("]"); + + return sb.toString(); + } + + public static ArraySliceOperation parse(String operation){ + //check valid chars + for (int i = 0; i < operation.length(); i++) { + char c = operation.charAt(i); + if( !isDigit(c) && c != '-' && c != ':'){ + throw new InvalidPathException("Failed to parse SliceOperation: " + operation); + } + } + String[] tokens = operation.split(":"); + + Integer tempFrom = tryRead(tokens, 0); + Integer tempTo = tryRead(tokens, 1); + Operation tempOperpation; + + if(tempFrom != null && tempTo == null){ + tempOperpation = Operation.SLICE_FROM; + } else if(tempFrom != null && tempTo != null){ + tempOperpation = Operation.SLICE_BETWEEN; + } else if(tempFrom == null && tempTo != null){ + tempOperpation = Operation.SLICE_TO; + } else { + throw new InvalidPathException("Failed to parse SliceOperation: " + operation); + } + + return new ArraySliceOperation(tempFrom, tempTo, tempOperpation); + } + + private static Integer tryRead(String[] tokens, int idx){ + if(tokens.length > idx){ + if(tokens[idx].equals("")){ + return null; + } + return Integer.parseInt(tokens[idx]); + } else { + return null; + } + } +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/token/PathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/token/PathToken.java index 54339fd6..122d94da 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/token/PathToken.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/token/PathToken.java @@ -115,7 +115,7 @@ public abstract class PathToken { } - void handleArrayIndex(int index, String currentPath, Object model, EvaluationContextImpl ctx) { + protected void handleArrayIndex(int index, String currentPath, Object model, EvaluationContextImpl ctx) { String evalPath = Utils.concat(currentPath, "[", String.valueOf(index), "]"); PathRef pathRef = ctx.forUpdate() ? PathRef.create(model, index) : PathRef.NO_OP; try { @@ -204,8 +204,8 @@ public abstract class PathToken { public abstract void evaluate(String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx); - abstract boolean isTokenDefinite(); + public abstract boolean isTokenDefinite(); - abstract String getPathFragment(); + protected abstract String getPathFragment(); } diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/token/PathTokenAppender.java b/json-path/src/main/java/com/jayway/jsonpath/internal/token/PathTokenAppender.java new file mode 100644 index 00000000..a7be89d1 --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/token/PathTokenAppender.java @@ -0,0 +1,5 @@ +package com.jayway.jsonpath.internal.token; + +public interface PathTokenAppender { + PathTokenAppender appendPathToken(PathToken next); +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/token/PathTokenFactory.java b/json-path/src/main/java/com/jayway/jsonpath/internal/token/PathTokenFactory.java new file mode 100644 index 00000000..0dd53bb5 --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/token/PathTokenFactory.java @@ -0,0 +1,47 @@ +package com.jayway.jsonpath.internal.token; + +import com.jayway.jsonpath.Predicate; + +import java.util.Collection; +import java.util.List; + +import static java.util.Collections.singletonList; + +public class PathTokenFactory { + + public static RootPathToken createRootPathToken(char token) { + return new RootPathToken(token); + } + + public static PathToken createSinglePropertyPathToken(String property) { + return new PropertyPathToken(singletonList(property)); + } + + public static PathToken createPropertyPathToken(List properties) { + return new PropertyPathToken(properties); + } + + public static PathToken createSliceArrayPathToken(final ArraySliceOperation arraySliceOperation) { + return new ArrayPathToken(arraySliceOperation); + } + + public static PathToken createIndexArrayPathToken(final ArrayIndexOperation arrayIndexOperation) { + return new ArrayPathToken(arrayIndexOperation); + } + + public static PathToken createWildCardPathToken() { + return new WildcardPathToken(); + } + + public static PathToken crateScanToken() { + return new ScanPathToken(); + } + + public static PathToken createPredicatePathToken(Collection predicates) { + return new PredicatePathToken(predicates); + } + + public static PathToken createPredicatePathToken(Predicate predicate) { + return new PredicatePathToken(predicate); + } +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/token/PredicatePathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/token/PredicatePathToken.java index bd6c5100..df7af013 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/token/PredicatePathToken.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/token/PredicatePathToken.java @@ -29,21 +29,14 @@ import static java.util.Arrays.asList; */ public class PredicatePathToken extends PathToken { - private static final String[] FRAGMENTS = { - "[?]", - "[?,?]", - "[?,?,?]", - "[?,?,?,?]", - "[?,?,?,?,?]" - }; private final Collection predicates; - public PredicatePathToken(Predicate filter) { + PredicatePathToken(Predicate filter) { this.predicates = asList(filter); } - public PredicatePathToken(Collection predicates) { + PredicatePathToken(Collection predicates) { this.predicates = predicates; } @@ -86,11 +79,20 @@ public class PredicatePathToken extends PathToken { @Override public String getPathFragment() { - return FRAGMENTS[predicates.size() - 1]; + StringBuilder sb = new StringBuilder(); + sb.append("["); + for(int i = 0; i < predicates.size(); i++){ + if(i != 0){ + sb.append(","); + } + sb.append("?"); + } + sb.append("]"); + return sb.toString(); } @Override - boolean isTokenDefinite() { + public boolean isTokenDefinite() { return false; } diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/token/PropertyPathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/token/PropertyPathToken.java index f7824036..a5851d8c 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/token/PropertyPathToken.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/token/PropertyPathToken.java @@ -23,7 +23,7 @@ import java.util.List; /** * */ -public class PropertyPathToken extends PathToken { +class PropertyPathToken extends PathToken { private final List properties; @@ -45,7 +45,7 @@ public class PropertyPathToken extends PathToken { } @Override - boolean isTokenDefinite() { + public boolean isTokenDefinite() { return true; } @@ -53,7 +53,7 @@ public class PropertyPathToken extends PathToken { public String getPathFragment() { return new StringBuilder() .append("[") - .append(Utils.join(", ", "'", properties)) + .append(Utils.join(",", "'", properties)) .append("]").toString(); } } diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/token/RootPathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/token/RootPathToken.java index ce4c1ad4..9495aa64 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/token/RootPathToken.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/token/RootPathToken.java @@ -23,8 +23,11 @@ public class RootPathToken extends PathToken { private PathToken tail; private int tokenCount; + private final String rootToken; - public RootPathToken() { + + RootPathToken(char rootToken) { + this.rootToken = Character.toString(rootToken);; this.tail = this; this.tokenCount = 1; } @@ -40,23 +43,35 @@ public class RootPathToken extends PathToken { return this; } + public PathTokenAppender getPathTokenAppender(){ + return new PathTokenAppender(){ + @Override + public PathTokenAppender appendPathToken(PathToken next) { + append(next); + return this; + } + }; + } + @Override public void evaluate(String currentPath, PathRef pathRef, Object model, EvaluationContextImpl ctx) { if (isLeaf()) { PathRef op = ctx.forUpdate() ? pathRef : PathRef.NO_OP; - ctx.addResult("$", op, model); + ctx.addResult(rootToken, op, model); } else { - next().evaluate("$", pathRef, model, ctx); + next().evaluate(rootToken, pathRef, model, ctx); } } @Override public String getPathFragment() { - return "$"; + return rootToken; } @Override - boolean isTokenDefinite() { + public boolean isTokenDefinite() { return true; } + + } diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/token/ScanPathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/token/ScanPathToken.java index 5bb0d0e5..e6286ed7 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/token/ScanPathToken.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/token/ScanPathToken.java @@ -24,6 +24,9 @@ import java.util.Collection; */ public class ScanPathToken extends PathToken { + ScanPathToken() { + } + @Override public void evaluate(String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx) { @@ -98,7 +101,7 @@ public class ScanPathToken extends PathToken { @Override - boolean isTokenDefinite() { + public boolean isTokenDefinite() { return false; } diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/token/WildcardPathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/token/WildcardPathToken.java index 243c00c7..1ce86ddc 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/token/WildcardPathToken.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/token/WildcardPathToken.java @@ -25,6 +25,9 @@ import static java.util.Arrays.asList; */ public class WildcardPathToken extends PathToken { + WildcardPathToken() { + } + @Override public void evaluate(String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx) { if (ctx.jsonProvider().isMap(model)) { @@ -46,7 +49,7 @@ public class WildcardPathToken extends PathToken { @Override - boolean isTokenDefinite() { + public boolean isTokenDefinite() { return false; } diff --git a/json-path/src/test/java/com/jayway/jsonpath/FilterTest.java b/json-path/src/test/java/com/jayway/jsonpath/FilterTest.java index be99d514..edb91c17 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/FilterTest.java +++ b/json-path/src/test/java/com/jayway/jsonpath/FilterTest.java @@ -10,6 +10,9 @@ import java.util.regex.Pattern; import static com.jayway.jsonpath.Criteria.where; import static com.jayway.jsonpath.Filter.filter; +import static com.jayway.jsonpath.Filter.parse; +import static java.lang.System.out; +import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; public class FilterTest extends BaseTest { @@ -252,9 +255,9 @@ public class FilterTest extends BaseTest { String json = "{\"foo\":" + arr + ", \"bar\":" + nest + "}"; Object tree = Configuration.defaultConfiguration().jsonProvider().parse(json); Predicate.PredicateContext context = createPredicateContext(tree); - Filter farr = Filter.parse("[?(@.foo == " + arr + ")]"); - Filter fobjF = Filter.parse("[?(@.foo == " + nest + ")]"); - Filter fobjT = Filter.parse("[?(@.bar == " + nest + ")]"); + Filter farr = parse("[?(@.foo == " + arr + ")]"); + Filter fobjF = parse("[?(@.foo == " + nest + ")]"); + Filter fobjT = parse("[?(@.bar == " + nest + ")]"); assertThat(farr.apply(context)).isEqualTo(true); assertThat(fobjF.apply(context)).isEqualTo(false); assertThat(fobjT.apply(context)).isEqualTo(true); @@ -424,45 +427,194 @@ public class FilterTest extends BaseTest { Filter isBar = filter(where("bar").is(true)); - Filter fooOrBar = filter(where("foo").exists(true)).or(where("bar").exists(true)); - Filter fooAndBar = filter(where("foo").exists(true)).and(where("bar").exists(true)); + Filter fooOrBar = filter(where("foo").is(true)).or(where("bar").is(true)); + Filter fooAndBar = filter(where("foo").is(true)).and(where("bar").is(true)); assertThat(isFoo.or(isBar).apply(createPredicateContext(model))).isTrue(); assertThat(isFoo.and(isBar).apply(createPredicateContext(model))).isFalse(); + assertThat(fooOrBar.apply(createPredicateContext(model))).isTrue(); + assertThat(fooAndBar.apply(createPredicateContext(model))).isFalse(); } @Test public void a_filter_can_be_parsed() { - Filter.parse("[?(@.foo)]"); - Filter.parse("[?(@.foo == 1)]"); - Filter.parse("[?(@.foo == 1 || @['bar'])]"); - Filter.parse("[?(@.foo == 1 && @['bar'])]"); + parse("[?(@.foo)]"); + parse("[?(@.foo == 1)]"); + parse("[?(@.foo == 1 || @['bar'])]"); + parse("[?(@.foo == 1 && @['bar'])]"); } @Test public void an_invalid_filter_can_not_be_parsed() { try { - Filter.parse("[?(@.foo == 1)"); + parse("[?(@.foo == 1)"); Assertions.fail("expected " + InvalidPathException.class.getName()); } catch (InvalidPathException ipe){} try { - Filter.parse("[?(@.foo == 1) ||]"); + parse("[?(@.foo == 1) ||]"); Assertions.fail("expected " + InvalidPathException.class.getName()); } catch (InvalidPathException ipe){} try { - Filter.parse("[(@.foo == 1)]"); + parse("[(@.foo == 1)]"); Assertions.fail("expected " + InvalidPathException.class.getName()); } catch (InvalidPathException ipe){} try { - Filter.parse("[?@.foo == 1)]"); + parse("[?@.foo == 1)]"); Assertions.fail("expected " + InvalidPathException.class.getName()); } catch (InvalidPathException ipe){} } + @Test + public void a_gte_filter_can_be_serialized() { + + System.out.println(filter(where("a").gte(1)).toString()); + + assertThat(filter(where("a").gte(1)).toString()).isEqualTo(parse("[?(@['a'] >= 1)]").toString()); + } + + @Test + public void a_lte_filter_can_be_serialized() { + assertThat(filter(where("a").lte(1)).toString()).isEqualTo("[?(@['a'] <= 1)]"); + } + + @Test + public void a_eq_filter_can_be_serialized() { + assertThat(filter(where("a").eq(1)).toString()).isEqualTo("[?(@['a'] == 1)]"); + } + + @Test + public void a_ne_filter_can_be_serialized() { + assertThat(filter(where("a").ne(1)).toString()).isEqualTo("[?(@['a'] != 1)]"); + } + + @Test + public void a_lt_filter_can_be_serialized() { + assertThat(filter(where("a").lt(1)).toString()).isEqualTo("[?(@['a'] < 1)]"); + } + + @Test + public void a_gt_filter_can_be_serialized() { + assertThat(filter(where("a").gt(1)).toString()).isEqualTo("[?(@['a'] > 1)]"); + } + + @Test + public void a_regex_filter_can_be_serialized() { + assertThat(filter(where("a").regex(Pattern.compile("/.*?/i"))).toString()).isEqualTo("[?(@['a'] =~ /.*?/i)]"); + } + + @Test + public void a_nin_filter_can_be_serialized() { + assertThat(filter(where("a").nin(1)).toString()).isEqualTo("[?(@['a'] ¦NIN¦ [1])]"); + } + + @Test + public void a_in_filter_can_be_serialized() { + assertThat(filter(where("a").in("a")).toString()).isEqualTo("[?(@['a'] ¦IN¦ ['a'])]"); + } + + @Test + public void a_contains_filter_can_be_serialized() { + assertThat(filter(where("a").contains("a")).toString()).isEqualTo("[?(@['a'] ¦CONTAINS¦ 'a')]"); + } + + @Test + public void a_all_filter_can_be_serialized() { + assertThat(filter(where("a").all("a", "b")).toString()).isEqualTo("[?(@['a'] ¦ALL¦ ['a','b'])]"); + } + + @Test + public void a_size_filter_can_be_serialized() { + assertThat(filter(where("a").size(5)).toString()).isEqualTo("[?(@['a'] ¦SIZE¦ 5)]"); + } + + @Test + public void a_exists_filter_can_be_serialized() { + assertThat(filter(where("a").exists(true)).toString()).isEqualTo("[?(@['a'])]"); + } + + @Test + public void a_not_exists_filter_can_be_serialized() { + assertThat(filter(where("a").exists(false)).toString()).isEqualTo("[?(!@['a'])]"); + } + + @Test + public void a_type_filter_can_be_serialized() { + assertThat(filter(where("a").type(String.class)).toString()).isEqualTo("[?(@['a'] ¦TYPE¦ java.lang.String)]"); + } + + @Test + public void a_matches_filter_can_be_serialized() { + Filter a = filter(where("x").eq(1000)); + + assertThat(filter(where("a").matches(a)).toString()).isEqualTo("[?(@['a'] ¦MATCHES¦ [?(@['x'] == 1000)])]"); + } + + @Test + public void a_not_empty_filter_can_be_serialized() { + assertThat(filter(where("a").notEmpty()).toString()).isEqualTo("[?(@['a'] ¦NOT_EMPTY¦)]"); + } + + @Test + public void and_filter_can_be_serialized() { + assertThat(filter(where("a").eq(1).and("b").eq(2)).toString()).isEqualTo("[?(@['a'] == 1 && @['b'] == 2)]"); + } + + @Test + public void in_string_filter_can_be_serialized() { + assertThat(filter(where("a").in("1","2")).toString()).isEqualTo("[?(@['a'] ¦IN¦ ['1','2'])]"); + } + + @Test + public void a_deep_path_filter_can_be_serialized() { + assertThat(filter(where("a.b.c").in("1","2")).toString()).isEqualTo("[?(@['a']['b']['c'] ¦IN¦ ['1','2'])]"); + } + + @Test + public void a_doc_ref_filter_can_be_serialized() { + assertThat(parse("[?(@.display-price <= $.max-price)]").toString()).isEqualTo("[?(@['display-price'] <= $['max-price'])]"); + } + + @Test + public void and_combined_filters_can_be_serialized() { + + Filter a = filter(where("a").eq(1)); + Filter b = filter(where("b").eq(2)); + Filter c = a.and(b); + + assertThat(c.toString()).isEqualTo("[?(@['a'] == 1 && @['b'] == 2)]"); + } + + @Test + public void or_combined_filters_can_be_serialized() { + + Filter a = filter(where("a").eq(1)); + Filter b = filter(where("b").eq(2)); + Filter c = a.or(b); + + assertThat(c.toString()).isEqualTo("[?(@['a'] == 1 || @['b'] == 2)]"); + } + + + @Test + public void a_____() { + // :2 + // 1:2 + // -2: + //2: + + + + out.println(asList(":2".split(":"))); //[, 2] + out.println(asList("1:2".split(":"))); //[1, 2] + out.println(asList("-2:".split(":"))); //[-2] + out.println(asList("2:".split(":"))); //[2] + out.println(asList(":2".split(":")).get(0).equals("")); //true + + } } diff --git a/json-path/src/test/java/com/jayway/jsonpath/PathCompilerTest.java b/json-path/src/test/java/com/jayway/jsonpath/PathCompilerTest.java new file mode 100644 index 00000000..5a1d05b6 --- /dev/null +++ b/json-path/src/test/java/com/jayway/jsonpath/PathCompilerTest.java @@ -0,0 +1,131 @@ +package com.jayway.jsonpath; + +import org.junit.Ignore; +import org.junit.Test; + +import static com.jayway.jsonpath.internal.PathCompiler.compile; +import static org.assertj.core.api.Assertions.assertThat; + +public class PathCompilerTest { + + + @Ignore("Backward compatibility <= 2.0.0") + @Test(expected = InvalidPathException.class) + public void a_path_must_start_with_$_or_at() { + compile("x"); + } + + @Ignore("Backward compatibility <= 2.0.0") + @Test(expected = InvalidPathException.class) + public void a_square_bracket_may_not_follow_a_period() { + compile("$.["); + } + + @Test(expected = InvalidPathException.class) + public void a_root_path_must_be_followed_by_period_or_bracket() { + compile("$X"); + } + + @Test + public void a_root_path_can_be_compiled() { + assertThat(compile("$").toString()).isEqualTo("$"); + assertThat(compile("@").toString()).isEqualTo("@"); + } + + @Test(expected = InvalidPathException.class) + public void a_path_may_not_end_with_period() { + assertThat(compile("$.").toString()); + assertThat(compile("$.prop.").toString()); + } + + @Test(expected = InvalidPathException.class) + public void a_path_may_not_end_with_scan() { + assertThat(compile("$..").toString()); + assertThat(compile("$.prop..").toString()); + } + + @Test + public void a_property_token_can_be_compiled() { + assertThat(compile("$.prop").toString()).isEqualTo("$['prop']"); + assertThat(compile("$.1prop").toString()).isEqualTo("$['1prop']"); + assertThat(compile("$.@prop").toString()).isEqualTo("$['@prop']"); + } + + @Test + public void a_bracket_notation_property_token_can_be_compiled() { + assertThat(compile("$['prop']").toString()).isEqualTo("$['prop']"); + assertThat(compile("$['1prop']").toString()).isEqualTo("$['1prop']"); + assertThat(compile("$['@prop']").toString()).isEqualTo("$['@prop']"); + assertThat(compile("$[ '@prop' ]").toString()).isEqualTo("$['@prop']"); + } + + @Test + public void a_multi_property_token_can_be_compiled() { + assertThat(compile("$['prop0', 'prop1']").toString()).isEqualTo("$['prop0','prop1']"); + assertThat(compile("$[ 'prop0' , 'prop1' ]").toString()).isEqualTo("$['prop0','prop1']"); + } + + @Test + public void a_property_chain_can_be_compiled() { + assertThat(compile("$.abc").toString()).isEqualTo("$['abc']"); + assertThat(compile("$.aaa.bbb").toString()).isEqualTo("$['aaa']['bbb']"); + assertThat(compile("$.aaa.bbb.ccc").toString()).isEqualTo("$['aaa']['bbb']['ccc']"); + } + + @Test(expected = InvalidPathException.class) + public void a_property_may_not_contain_blanks() { + assertThat(compile("$.foo bar").toString()); + } + + @Test + public void a_wildcard_can_be_compiled() { + assertThat(compile("$.*").toString()).isEqualTo("$[*]"); + assertThat(compile("$[*]").toString()).isEqualTo("$[*]"); + assertThat(compile("$[ * ]").toString()).isEqualTo("$[*]"); + } + + @Test + public void a_wildcard_can_follow_a_property() { + assertThat(compile("$.prop[*]").toString()).isEqualTo("$['prop'][*]"); + assertThat(compile("$['prop'][*]").toString()).isEqualTo("$['prop'][*]"); + } + + @Test + public void an_array_index_path_can_be_compiled() { + assertThat(compile("$[1]").toString()).isEqualTo("$[1]"); + assertThat(compile("$[1,2,3]").toString()).isEqualTo("$[1,2,3]"); + assertThat(compile("$[ 1 , 2 , 3 ]").toString()).isEqualTo("$[1,2,3]"); + } + + @Test + public void an_array_slice_path_can_be_compiled() { + assertThat(compile("$[-1:]").toString()).isEqualTo("$[-1:]"); + assertThat(compile("$[1:2]").toString()).isEqualTo("$[1:2]"); + assertThat(compile("$[:2]").toString()).isEqualTo("$[:2]"); + } + + @Test + public void an_inline_criteria_can_be_parsed() { + assertThat(compile("$[?(@.foo == 'bar')]").toString()).isEqualTo("$[?]"); + } + + @Test + public void a_placeholder_criteria_can_be_parsed() { + + Predicate p = new Predicate() { + @Override + public boolean apply(PredicateContext ctx) { + return false; + } + }; + assertThat(compile("$[?]", p).toString()).isEqualTo("$[?]"); + assertThat(compile("$[?,?]", p, p).toString()).isEqualTo("$[?,?]"); + assertThat(compile("$[?,?,?]", p, p, p).toString()).isEqualTo("$[?,?,?]"); + } + + @Test + public void a_scan_token_can_be_parsed() { + assertThat(compile("$..['prop']..[*]").toString()).isEqualTo("$..['prop']..[*]"); + } + +} diff --git a/json-path/src/test/java/com/jayway/jsonpath/old/ArraySlicingTest.java b/json-path/src/test/java/com/jayway/jsonpath/old/ArraySlicingTest.java index e96a63a2..ce09099b 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/old/ArraySlicingTest.java +++ b/json-path/src/test/java/com/jayway/jsonpath/old/ArraySlicingTest.java @@ -83,12 +83,6 @@ public class ArraySlicingTest { assertThat(result, Matchers.contains(7, 8, 13, 20)); } - @Test - public void get_from_tail_length(){ - Integer result = JsonPath.read(JSON_ARRAY, "$[(@.length -3)]"); - assertEquals(8, result.intValue()); - } - @Test public void get_indexes(){ List result = JsonPath.read(JSON_ARRAY, "$[0,1,2]"); diff --git a/json-path/src/test/java/com/jayway/jsonpath/old/ComplianceTest.java b/json-path/src/test/java/com/jayway/jsonpath/old/ComplianceTest.java index 52f3813d..f6e4be45 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/old/ComplianceTest.java +++ b/json-path/src/test/java/com/jayway/jsonpath/old/ComplianceTest.java @@ -73,7 +73,6 @@ public class ComplianceTest { assertThat(JsonPath.>read(json, "$.points[?(@.id == 'i4')].x"), hasItem(-6)); assertThat(JsonPath.>read(json, "$.points[*].x"), hasItems(4, -2, 8, -6, 0, 1)); assertThat(JsonPath.>read(json, "$.points[?(@.z)].id"), hasItems("i2", "i5")); - assertThat(JsonPath.read(json, "$.points[(@.length - 1)].id"), equalTo("i6")); } @Test diff --git a/json-path/src/test/java/com/jayway/jsonpath/old/IssuesTest.java b/json-path/src/test/java/com/jayway/jsonpath/old/IssuesTest.java index 991d1614..c491e6b2 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/old/IssuesTest.java +++ b/json-path/src/test/java/com/jayway/jsonpath/old/IssuesTest.java @@ -2,7 +2,6 @@ package com.jayway.jsonpath.old; import com.jayway.jsonpath.BaseTest; import com.jayway.jsonpath.Configuration; -import com.jayway.jsonpath.Criteria; import com.jayway.jsonpath.DocumentContext; import com.jayway.jsonpath.Filter; import com.jayway.jsonpath.JsonPath; @@ -15,6 +14,7 @@ import com.jayway.jsonpath.internal.Utils; import com.jayway.jsonpath.spi.json.GsonJsonProvider; import com.jayway.jsonpath.spi.json.JsonProvider; import com.jayway.jsonpath.spi.mapper.GsonMappingProvider; +import com.jayway.jsonpath.spi.mapper.MappingException; import net.minidev.json.JSONAware; import net.minidev.json.parser.JSONParser; import org.assertj.core.api.Assertions; @@ -23,9 +23,13 @@ import org.junit.Test; import java.io.InputStream; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; +import static com.jayway.jsonpath.Criteria.PredicateContext; +import static com.jayway.jsonpath.Criteria.where; +import static com.jayway.jsonpath.Filter.filter; import static com.jayway.jsonpath.JsonPath.read; import static junit.framework.Assert.assertNull; import static junit.framework.Assert.assertTrue; @@ -49,8 +53,6 @@ public class IssuesTest extends BaseTest { assertEquals(1, fullOnes.size()); assertEquals("full", fullOnes.get(0).get("kind")); - - } @Test @@ -171,7 +173,7 @@ public class IssuesTest extends BaseTest { " }\n" + "]"; - List result = read(json, "$.[?(@.compatible == true)].sku"); + List result = read(json, "$[?(@.compatible == true)].sku"); Assertions.assertThat(result).containsExactly("SKU-005", "SKU-003"); } @@ -285,7 +287,7 @@ public class IssuesTest extends BaseTest { @Test public void issue_29_b() throws Exception { String json = "{\"list\": [ { \"a\":\"atext\", \"b\":{ \"b-a\":\"batext\", \"b-b\":\"bbtext\" } }, { \"a\":\"atext2\", \"b\":{ \"b-a\":\"batext2\", \"b-b\":\"bbtext2\" } } ] }"; - List result = read(json, "$.list[?]", Filter.filter(Criteria.where("b.b-a").eq("batext2"))); + List result = read(json, "$.list[?]", filter(where("b.b-a").eq("batext2"))); assertTrue(result.size() == 1); } @@ -545,7 +547,7 @@ public class IssuesTest extends BaseTest { "}"; - Filter filter = Filter.filter(Criteria.where("authors[*].lastName").contains("Waugh")); + Filter filter = filter(where("authors[*].lastName").contains("Waugh")); Object read = JsonPath.parse(json).read("$.store.book[?]", filter); @@ -715,4 +717,52 @@ public class IssuesTest extends BaseTest { System.out.println(keys); } + + + @Test + public void issue_129() throws Exception { + + final Map match = new HashMap(); + match.put("a", 1); + match.put("b", 2); + + Map noMatch = new HashMap(); + noMatch.put("a", -1); + noMatch.put("b", -2); + + Filter orig = filter(where("a").eq(1).and("b").eq(2)); + + String filterAsString = orig.toString(); + + Filter parsed = Filter.parse(filterAsString); + + Assertions.assertThat(orig.apply(createPredicateContext(match))).isTrue(); + Assertions.assertThat(parsed.apply(createPredicateContext(match))).isTrue(); + Assertions.assertThat(orig.apply(createPredicateContext(noMatch))).isFalse(); + Assertions.assertThat(parsed.apply(createPredicateContext(noMatch))).isFalse(); + } + + private PredicateContext createPredicateContext(final Map map){ + return new PredicateContext() { + @Override + public Object item() { + return map; + } + + @Override + public T item(Class clazz) throws MappingException { + return (T)map; + } + + @Override + public Object root() { + return map; + } + + @Override + public Configuration configuration() { + return Configuration.defaultConfiguration(); + } + }; + } } diff --git a/json-path/src/test/java/com/jayway/jsonpath/old/JsonPathTest.java b/json-path/src/test/java/com/jayway/jsonpath/old/JsonPathTest.java index ecec7ea4..e0b919fe 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/old/JsonPathTest.java +++ b/json-path/src/test/java/com/jayway/jsonpath/old/JsonPathTest.java @@ -7,7 +7,6 @@ import com.jayway.jsonpath.JsonPath; import com.jayway.jsonpath.Option; import com.jayway.jsonpath.PathNotFoundException; import com.jayway.jsonpath.internal.PathCompiler; - import org.assertj.core.api.Assertions; import org.junit.Test; @@ -242,7 +241,6 @@ public class JsonPathTest extends BaseTest { @Test public void access_array_by_index_from_tail() throws Exception { - assertThat(JsonPath.>read(DOCUMENT, "$..book[(@.length-1)].author"), hasItems("J. R. R. Tolkien")); assertThat(JsonPath.>read(DOCUMENT, "$..book[1:].author"), hasItems("Evelyn Waugh", "Herman Melville", "J. R. R. Tolkien")); } diff --git a/json-path/src/test/java/com/jayway/jsonpath/old/internal/ArrayPathTokenTest.java b/json-path/src/test/java/com/jayway/jsonpath/old/internal/ArrayPathTokenTest.java index 880b3683..d89d3cc3 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/old/internal/ArrayPathTokenTest.java +++ b/json-path/src/test/java/com/jayway/jsonpath/old/internal/ArrayPathTokenTest.java @@ -8,18 +8,10 @@ import java.util.Map; import static com.jayway.jsonpath.JsonPath.read; import static java.util.Collections.singletonMap; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.data.MapEntry.entry; public class ArrayPathTokenTest extends TestBase { - @Test - public void array_can_select_single_index_by_context_length() { - - Map result = read(ARRAY, "$[(@.length-1)]"); - - assertThat(result).contains(entry("foo", "foo-val-6")); - } @Test public void array_can_select_multiple_indexes() { diff --git a/json-path/src/test/java/com/jayway/jsonpath/old/internal/ScanPathTokenTest.java b/json-path/src/test/java/com/jayway/jsonpath/old/internal/ScanPathTokenTest.java index 41ab2385..ca288dff 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/old/internal/ScanPathTokenTest.java +++ b/json-path/src/test/java/com/jayway/jsonpath/old/internal/ScanPathTokenTest.java @@ -200,16 +200,5 @@ public class ScanPathTokenTest { "$['store']['book'][2]"); } - @Test - public void a_document_can_be_scanned_for_array_indexes() { - - List result = PathCompiler.compile("$..[(@.length - 1)]").evaluate(DOCUMENT, DOCUMENT, Configuration.defaultConfiguration()).getPathList(); - - assertThat(result).containsOnly( - "$['store']['bicycle']['items'][5]", - "$['store']['bicycle']['items'][0][2]", - "$['store']['book'][2]"); - - } }