From b5f68f2830856ff841bf23ed8960ace8064e3720 Mon Sep 17 00:00:00 2001 From: Rick Osborne Date: Sat, 12 Dec 2015 20:23:24 -0800 Subject: [PATCH] Decouple the Path representation from its evaluation Extracted all of the PathToken::evaluate methods out to *Evaluator classes, now called by a (configurable) DefaultEvaluator. If you want to write your own custom evaluator, you now need to implement the EvaluatorDispatcher interface (one function) and then traverse the PathToken nodes as fits your need. This presumes, of course, that you want to use the `EvaluationContext` as-is. Otherwise, get the root node and do your own thing. --- .../internal/path/ArrayPathToken.java | 189 --------------- .../jsonpath/internal/path/CompiledPath.java | 18 +- .../internal/path/EvaluatorDispatcher.java | 8 + .../jsonpath/internal/path/PathCompiler.java | 3 + .../jsonpath/internal/path/PathToken.java | 217 ------------------ .../internal/path/PathTokenAppender.java | 2 + .../internal/path/PathTokenFactory.java | 7 +- .../jsonpath/internal/path/ScanPathToken.java | 206 ----------------- .../internal/path/WildcardPathToken.java | 60 ----- .../internal/path/eval/DefaultEvaluator.java | 52 +++++ .../path/eval/FunctionTokenEvaluator.java | 21 ++ .../internal/path/eval/ITokenEvaluator.java | 9 + .../path/eval/IndexArrayTokenEvaluator.java | 36 +++ .../path/eval/PredicateTokenEvaluator.java | 43 ++++ .../path/eval/PropertyTokenEvaluator.java | 50 ++++ .../path/eval/RootTokenEvaluator.java | 22 ++ .../path/eval/ScanTokenEvaluator.java | 95 ++++++++ .../path/eval/SliceArrayTokenEvaluator.java | 99 ++++++++ .../internal/path/eval/TokenEvaluator.java | 156 +++++++++++++ .../path/eval/WildcardTokenEvaluator.java | 35 +++ .../{ => operation}/ArrayIndexOperation.java | 2 +- .../{ => operation}/ArraySliceOperation.java | 28 +-- .../path/operation/SliceBetweenOperation.java | 12 + .../path/operation/SliceFromOperation.java | 12 + .../path/operation/SliceToOperation.java | 12 + .../predicate/ArrayPathTokenPredicate.java | 16 ++ .../predicate/FilterPathTokenPredicate.java | 20 ++ .../path/predicate/PathTokenPredicate.java | 5 + .../predicate/PropertyPathTokenPredicate.java | 52 +++++ .../predicate/WildcardPathTokenPredicate.java | 8 + .../internal/path/token/ArrayPathToken.java | 22 ++ .../path/{ => token}/FunctionPathToken.java | 15 +- .../path/token/IndexArrayPathToken.java | 27 +++ .../internal/path/token/PathToken.java | 110 +++++++++ .../path/{ => token}/PredicatePathToken.java | 40 +--- .../path/{ => token}/PropertyPathToken.java | 41 +--- .../path/{ => token}/RootPathToken.java | 16 +- .../internal/path/token/ScanPathToken.java | 35 +++ .../path/token/SliceArrayPathToken.java | 27 +++ .../path/token/WildcardPathToken.java | 34 +++ .../jsonpath/internal/path/PathTokenTest.java | 4 + 41 files changed, 1074 insertions(+), 792 deletions(-) delete mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/path/ArrayPathToken.java create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/path/EvaluatorDispatcher.java delete mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/path/PathToken.java delete mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/path/ScanPathToken.java delete mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/path/WildcardPathToken.java create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/DefaultEvaluator.java create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/FunctionTokenEvaluator.java create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/ITokenEvaluator.java create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/IndexArrayTokenEvaluator.java create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/PredicateTokenEvaluator.java create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/PropertyTokenEvaluator.java create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/RootTokenEvaluator.java create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/ScanTokenEvaluator.java create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/SliceArrayTokenEvaluator.java create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/TokenEvaluator.java create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/WildcardTokenEvaluator.java rename json-path/src/main/java/com/jayway/jsonpath/internal/path/{ => operation}/ArrayIndexOperation.java (96%) rename json-path/src/main/java/com/jayway/jsonpath/internal/path/{ => operation}/ArraySliceOperation.java (66%) create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/path/operation/SliceBetweenOperation.java create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/path/operation/SliceFromOperation.java create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/path/operation/SliceToOperation.java create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/path/predicate/ArrayPathTokenPredicate.java create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/path/predicate/FilterPathTokenPredicate.java create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/path/predicate/PathTokenPredicate.java create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/path/predicate/PropertyPathTokenPredicate.java create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/path/predicate/WildcardPathTokenPredicate.java create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/path/token/ArrayPathToken.java rename json-path/src/main/java/com/jayway/jsonpath/internal/path/{ => token}/FunctionPathToken.java (68%) create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/path/token/IndexArrayPathToken.java create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/path/token/PathToken.java rename json-path/src/main/java/com/jayway/jsonpath/internal/path/{ => token}/PredicatePathToken.java (56%) rename json-path/src/main/java/com/jayway/jsonpath/internal/path/{ => token}/PropertyPathToken.java (53%) rename json-path/src/main/java/com/jayway/jsonpath/internal/path/{ => token}/RootPathToken.java (77%) create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/path/token/ScanPathToken.java create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/path/token/SliceArrayPathToken.java create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/path/token/WildcardPathToken.java diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/ArrayPathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/ArrayPathToken.java deleted file mode 100644 index 05d29d76..00000000 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/path/ArrayPathToken.java +++ /dev/null @@ -1,189 +0,0 @@ -/* - * 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.path; - -import com.jayway.jsonpath.InvalidPathException; -import com.jayway.jsonpath.PathNotFoundException; -import com.jayway.jsonpath.internal.PathRef; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import static java.lang.String.format; - -/** - * - */ -public class ArrayPathToken extends PathToken { - - private static final Logger logger = LoggerFactory.getLogger(ArrayPathToken.class); - - private final ArraySliceOperation arraySliceOperation; - private final ArrayIndexOperation arrayIndexOperation; - - ArrayPathToken(final ArraySliceOperation arraySliceOperation) { - this.arraySliceOperation = arraySliceOperation; - this.arrayIndexOperation = null; - } - - ArrayPathToken(final ArrayIndexOperation arrayIndexOperation) { - this.arrayIndexOperation = arrayIndexOperation; - this.arraySliceOperation = null; - } - - @Override - public void evaluate(String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx) { - if (! checkArrayModel(currentPath, model, ctx)) - return; - 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 (! checkArrayModel(currentPath, model, ctx)) - return; - - 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 (! checkArrayModel(currentPath, model, ctx)) - return; - - 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() { - if(arrayIndexOperation != null){ - return arrayIndexOperation.toString(); - } else { - return arraySliceOperation.toString(); - } - } - - @Override - public boolean isTokenDefinite() { - if(arrayIndexOperation != null){ - return arrayIndexOperation.isSingleIndexOperation(); - } else { - return false; - } - } - - /** - * Check if model is non-null and array. - * @param currentPath - * @param model - * @param ctx - * @return false if current evaluation call must be skipped, true otherwise - * @throws PathNotFoundException if model is null and evaluation must be interrupted - * @throws InvalidPathException if model is not an array and evaluation must be interrupted - */ - protected boolean checkArrayModel(String currentPath, Object model, EvaluationContextImpl ctx) { - if (model == null){ - if (! isUpstreamDefinite()) { - return false; - } else { - throw new PathNotFoundException("The path " + currentPath + " is null"); - } - } - if (!ctx.jsonProvider().isArray(model)) { - if (! isUpstreamDefinite()) { - return false; - } else { - throw new PathNotFoundException(format("Filter: %s can only be applied to arrays. Current context is: %s", toString(), model)); - } - } - return true; - } -} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/CompiledPath.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/CompiledPath.java index 018a8b5e..ae4aebfb 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/path/CompiledPath.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/path/CompiledPath.java @@ -19,6 +19,8 @@ import com.jayway.jsonpath.internal.EvaluationAbortException; import com.jayway.jsonpath.internal.EvaluationContext; import com.jayway.jsonpath.internal.Path; import com.jayway.jsonpath.internal.PathRef; +import com.jayway.jsonpath.internal.path.eval.DefaultEvaluator; +import com.jayway.jsonpath.internal.path.token.RootPathToken; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -30,6 +32,7 @@ public class CompiledPath implements Path { private final boolean isRootPath; + protected EvaluatorDispatcher evaluator; public CompiledPath(RootPathToken root, boolean isRootPath) { this.root = root; @@ -41,6 +44,15 @@ public class CompiledPath implements Path { return isRootPath; } + public EvaluatorDispatcher getEvaluator() { + if (evaluator == null) evaluator = new DefaultEvaluator(); + return evaluator; + } + + public void setEvaluator(final EvaluatorDispatcher evaluator) { + this.evaluator = evaluator; + } + @Override public EvaluationContext evaluate(Object document, Object rootDocument, Configuration configuration, boolean forUpdate) { if (logger.isDebugEnabled()) { @@ -50,7 +62,7 @@ public class CompiledPath implements Path { EvaluationContextImpl ctx = new EvaluationContextImpl(this, rootDocument, configuration, forUpdate); try { PathRef op = ctx.forUpdate() ? PathRef.createRoot(rootDocument) : PathRef.NO_OP; - root.evaluate("", op, document, ctx); + getEvaluator().evaluate(root, "", op, document, ctx); } catch (EvaluationAbortException abort){}; return ctx; @@ -75,4 +87,8 @@ public class CompiledPath implements Path { public String toString() { return root.toString(); } + + public RootPathToken getRoot() { + return root; + } } diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/EvaluatorDispatcher.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/EvaluatorDispatcher.java new file mode 100644 index 00000000..ee28483b --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/path/EvaluatorDispatcher.java @@ -0,0 +1,8 @@ +package com.jayway.jsonpath.internal.path; + +import com.jayway.jsonpath.internal.PathRef; +import com.jayway.jsonpath.internal.path.token.PathToken; + +public interface EvaluatorDispatcher { + void evaluate(PathToken token, String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx); +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/PathCompiler.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/PathCompiler.java index 4a8af920..dfddf2be 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/path/PathCompiler.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/path/PathCompiler.java @@ -5,6 +5,9 @@ import com.jayway.jsonpath.Predicate; import com.jayway.jsonpath.internal.CharacterIndex; import com.jayway.jsonpath.internal.Path; import com.jayway.jsonpath.internal.filter.FilterCompiler; +import com.jayway.jsonpath.internal.path.operation.ArrayIndexOperation; +import com.jayway.jsonpath.internal.path.operation.ArraySliceOperation; +import com.jayway.jsonpath.internal.path.token.RootPathToken; import java.util.ArrayList; import java.util.Collection; diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/PathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/PathToken.java deleted file mode 100644 index d6300c40..00000000 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/path/PathToken.java +++ /dev/null @@ -1,217 +0,0 @@ -/* - * 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.path; - -import com.jayway.jsonpath.Option; -import com.jayway.jsonpath.PathNotFoundException; -import com.jayway.jsonpath.internal.PathRef; -import com.jayway.jsonpath.internal.Utils; -import com.jayway.jsonpath.internal.function.PathFunction; -import com.jayway.jsonpath.spi.json.JsonProvider; - -import java.util.List; - -public abstract class PathToken { - - private PathToken prev; - private PathToken next; - private Boolean definite = null; - private Boolean upstreamDefinite = null; - - PathToken appendTailToken(PathToken next) { - this.next = next; - this.next.prev = this; - return next; - } - - void handleObjectProperty(String currentPath, Object model, EvaluationContextImpl ctx, List properties) { - - if(properties.size() == 1) { - String property = properties.get(0); - String evalPath = Utils.concat(currentPath, "['", property, "']"); - Object propertyVal = readObjectProperty(property, model, ctx); - if(propertyVal == JsonProvider.UNDEFINED){ - // Conditions below heavily depend on current token type (and its logic) and are not "universal", - // so this code is quite dangerous (I'd rather rewrite it & move to PropertyPathToken and implemented - // WildcardPathToken as a dynamic multi prop case of PropertyPathToken). - // Better safe than sorry. - assert this instanceof PropertyPathToken : "only PropertyPathToken is supported"; - - if(isLeaf()) { - if(ctx.options().contains(Option.DEFAULT_PATH_LEAF_TO_NULL)){ - propertyVal = null; - } else { - if(ctx.options().contains(Option.SUPPRESS_EXCEPTIONS) || - !ctx.options().contains(Option.REQUIRE_PROPERTIES)){ - return; - } else { - throw new PathNotFoundException("No results for path: " + evalPath); - } - } - } else { - if (! (isUpstreamDefinite() && isTokenDefinite()) && - !ctx.options().contains(Option.REQUIRE_PROPERTIES) || - ctx.options().contains(Option.SUPPRESS_EXCEPTIONS)){ - // If there is some indefiniteness in the path and properties are not required - we'll ignore - // absent property. And also in case of exception suppression - so that other path evaluation - // branches could be examined. - return; - } else { - throw new PathNotFoundException("Missing property in path " + evalPath); - } - } - } - PathRef pathRef = ctx.forUpdate() ? PathRef.create(model, property) : PathRef.NO_OP; - if (isLeaf()) { - ctx.addResult(evalPath, pathRef, propertyVal); - } - else { - next().evaluate(evalPath, pathRef, propertyVal, ctx); - } - } else { - String evalPath = currentPath + "[" + Utils.join(", ", "'", properties) + "]"; - - assert isLeaf() : "non-leaf multi props handled elsewhere"; - - Object merged = ctx.jsonProvider().createMap(); - for (String property : properties) { - Object propertyVal; - if(hasProperty(property, model, ctx)) { - propertyVal = readObjectProperty(property, model, ctx); - if(propertyVal == JsonProvider.UNDEFINED){ - if(ctx.options().contains(Option.DEFAULT_PATH_LEAF_TO_NULL)) { - propertyVal = null; - } else { - continue; - } - } - } else { - if(ctx.options().contains(Option.DEFAULT_PATH_LEAF_TO_NULL)){ - propertyVal = null; - } else if (ctx.options().contains(Option.REQUIRE_PROPERTIES)) { - throw new PathNotFoundException("Missing property in path " + evalPath); - } else { - continue; - } - } - ctx.jsonProvider().setProperty(merged, property, propertyVal); - } - PathRef pathRef = ctx.forUpdate() ? PathRef.create(model, properties) : PathRef.NO_OP; - ctx.addResult(evalPath, pathRef, merged); - } - } - - private static boolean hasProperty(String property, Object model, EvaluationContextImpl ctx) { - return ctx.jsonProvider().getPropertyKeys(model).contains(property); - } - - private static Object readObjectProperty(String property, Object model, EvaluationContextImpl ctx) { - return ctx.jsonProvider().getMapValue(model, property); - } - - - 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 { - Object evalHit = ctx.jsonProvider().getArrayIndex(model, index); - if (isLeaf()) { - ctx.addResult(evalPath, pathRef, evalHit); - } else { - next().evaluate(evalPath, pathRef, evalHit, ctx); - } - } catch (IndexOutOfBoundsException e) { - } - } - - PathToken prev(){ - return prev; - } - - PathToken next() { - if (isLeaf()) { - throw new IllegalStateException("Current path token is a leaf"); - } - return next; - } - - boolean isLeaf() { - return next == null; - } - - boolean isRoot() { - return prev == null; - } - - boolean isUpstreamDefinite() { - if (upstreamDefinite == null) { - upstreamDefinite = isRoot() || prev.isTokenDefinite() && prev.isUpstreamDefinite(); - } - return upstreamDefinite; - } - - public int getTokenCount() { - int cnt = 1; - PathToken token = this; - - while (!token.isLeaf()){ - token = token.next(); - cnt++; - } - return cnt; - } - - public boolean isPathDefinite() { - if(definite != null){ - return definite.booleanValue(); - } - boolean isDefinite = isTokenDefinite(); - if (isDefinite && !isLeaf()) { - isDefinite = next.isPathDefinite(); - } - definite = isDefinite; - return isDefinite; - } - - @Override - public String toString() { - if (isLeaf()) { - return getPathFragment(); - } else { - return getPathFragment() + next().toString(); - } - } - - @Override - public int hashCode() { - return toString().hashCode(); - } - - @Override - public boolean equals(Object obj) { - return super.equals(obj); - } - - public void invoke(PathFunction pathFunction, String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx) { - ctx.addResult(currentPath, parent, pathFunction.invoke(currentPath, parent, model, ctx)); - } - - public abstract void evaluate(String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx); - - public abstract boolean isTokenDefinite(); - - protected abstract String getPathFragment(); - -} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/PathTokenAppender.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/PathTokenAppender.java index c5b7a68e..86e5ab77 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/path/PathTokenAppender.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/path/PathTokenAppender.java @@ -1,5 +1,7 @@ package com.jayway.jsonpath.internal.path; +import com.jayway.jsonpath.internal.path.token.PathToken; + public interface PathTokenAppender { PathTokenAppender appendPathToken(PathToken next); } diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/PathTokenFactory.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/PathTokenFactory.java index e242253d..90c41902 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/path/PathTokenFactory.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/path/PathTokenFactory.java @@ -1,6 +1,9 @@ package com.jayway.jsonpath.internal.path; import com.jayway.jsonpath.Predicate; +import com.jayway.jsonpath.internal.path.operation.ArrayIndexOperation; +import com.jayway.jsonpath.internal.path.operation.ArraySliceOperation; +import com.jayway.jsonpath.internal.path.token.*; import java.util.Collection; import java.util.List; @@ -22,11 +25,11 @@ public class PathTokenFactory { } public static PathToken createSliceArrayPathToken(final ArraySliceOperation arraySliceOperation) { - return new ArrayPathToken(arraySliceOperation); + return new SliceArrayPathToken(arraySliceOperation); } public static PathToken createIndexArrayPathToken(final ArrayIndexOperation arrayIndexOperation) { - return new ArrayPathToken(arrayIndexOperation); + return new IndexArrayPathToken(arrayIndexOperation); } public static PathToken createWildCardPathToken() { diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/ScanPathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/ScanPathToken.java deleted file mode 100644 index f98c3729..00000000 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/path/ScanPathToken.java +++ /dev/null @@ -1,206 +0,0 @@ -/* - * 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.path; - -import com.jayway.jsonpath.Option; -import com.jayway.jsonpath.internal.PathRef; -import com.jayway.jsonpath.spi.json.JsonProvider; - -import java.util.Collection; - -/** - * - */ -public class ScanPathToken extends PathToken { - - ScanPathToken() { - } - - @Override - public void evaluate(String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx) { - - PathToken pt = next(); - - walk(pt, currentPath, parent, model, ctx, createScanPredicate(pt, ctx)); - } - - public static void walk(PathToken pt, String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx, Predicate predicate) { - if (ctx.jsonProvider().isMap(model)) { - walkObject(pt, currentPath, parent, model, ctx, predicate); - } else if (ctx.jsonProvider().isArray(model)) { - walkArray(pt, currentPath, parent, model, ctx, predicate); - } - } - - public static void walkArray(PathToken pt, String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx, Predicate predicate) { - - if (predicate.matches(model)) { - if (pt.isLeaf()) { - pt.evaluate(currentPath, parent, model, ctx); - } else { - PathToken next = pt.next(); - Iterable models = ctx.jsonProvider().toIterable(model); - int idx = 0; - for (Object evalModel : models) { - String evalPath = currentPath + "[" + idx + "]"; - next.evaluate(evalPath, parent, evalModel, ctx); - idx++; - } - } - } - - Iterable models = ctx.jsonProvider().toIterable(model); - int idx = 0; - for (Object evalModel : models) { - String evalPath = currentPath + "[" + idx + "]"; - walk(pt, evalPath, PathRef.create(model, idx), evalModel, ctx, predicate); - idx++; - } - } - - public static void walkObject(PathToken pt, String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx, Predicate predicate) { - - if (predicate.matches(model)) { - pt.evaluate(currentPath, parent, model, ctx); - } - Collection properties = ctx.jsonProvider().getPropertyKeys(model); - - for (String property : properties) { - String evalPath = currentPath + "['" + property + "']"; - Object propertyModel = ctx.jsonProvider().getMapValue(model, property); - if (propertyModel != JsonProvider.UNDEFINED) { - walk(pt, evalPath, PathRef.create(model, property), propertyModel, ctx, predicate); - } - } - } - - private static Predicate createScanPredicate(final PathToken target, final EvaluationContextImpl ctx) { - if (target instanceof PropertyPathToken) { - return new PropertyPathTokenPredicate(target, ctx); - } else if (target instanceof ArrayPathToken) { - return new ArrayPathTokenPredicate(ctx); - } else if (target instanceof WildcardPathToken) { - return new WildcardPathTokenPredicate(); - } else if (target instanceof PredicatePathToken) { - return new FilterPathTokenPredicate(target, ctx); - } else { - return FALSE_PREDICATE; - } - } - - - @Override - public boolean isTokenDefinite() { - return false; - } - - @Override - public String getPathFragment() { - return ".."; - } - - private interface Predicate { - boolean matches(Object model); - } - - private static final Predicate FALSE_PREDICATE = new Predicate() { - - @Override - public boolean matches(Object model) { - return false; - } - }; - - private static final class FilterPathTokenPredicate implements Predicate { - private final EvaluationContextImpl ctx; - private PredicatePathToken predicatePathToken; - - private FilterPathTokenPredicate(PathToken target, EvaluationContextImpl ctx) { - this.ctx = ctx; - predicatePathToken = (PredicatePathToken) target; - } - - @Override - public boolean matches(Object model) { - return predicatePathToken.accept(model, ctx.rootDocument(), ctx.configuration(), ctx); - } - } - - private static final class WildcardPathTokenPredicate implements Predicate { - - @Override - public boolean matches(Object model) { - return true; - } - } - - private static final class ArrayPathTokenPredicate implements Predicate { - private final EvaluationContextImpl ctx; - - private ArrayPathTokenPredicate(EvaluationContextImpl ctx) { - this.ctx = ctx; - } - - @Override - public boolean matches(Object model) { - return ctx.jsonProvider().isArray(model); - } - } - - private static final class PropertyPathTokenPredicate implements Predicate { - private final EvaluationContextImpl ctx; - private PropertyPathToken propertyPathToken; - - private PropertyPathTokenPredicate(PathToken target, EvaluationContextImpl ctx) { - this.ctx = ctx; - propertyPathToken = (PropertyPathToken) target; - } - - @Override - public boolean matches(Object model) { - - if (! ctx.jsonProvider().isMap(model)) { - return false; - } - -// -// The commented code below makes it really hard understand, use and predict the result -// of deep scanning operations. It might be correct but was decided to be -// left out until the behavior of REQUIRE_PROPERTIES is more strictly defined -// in a deep scanning scenario. For details read conversation in commit -// https://github.com/jayway/JsonPath/commit/1a72fc078deb16995e323442bfb681bd715ce45a#commitcomment-14616092 -// -// if (ctx.options().contains(Option.REQUIRE_PROPERTIES)) { -// // Have to require properties defined in path when an indefinite path is evaluated, -// // so have to go there and search for it. -// return true; -// } - - if (! propertyPathToken.isTokenDefinite()) { - // It's responsibility of PropertyPathToken code to handle indefinite scenario of properties, - // so we'll allow it to do its job. - return true; - } - - if (propertyPathToken.isLeaf() && ctx.options().contains(Option.DEFAULT_PATH_LEAF_TO_NULL)) { - // In case of DEFAULT_PATH_LEAF_TO_NULL missing properties is not a problem. - return true; - } - - Collection keys = ctx.jsonProvider().getPropertyKeys(model); - return keys.containsAll(propertyPathToken.getProperties()); - } - } -} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/WildcardPathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/WildcardPathToken.java deleted file mode 100644 index 37d02ec9..00000000 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/path/WildcardPathToken.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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.path; - -import com.jayway.jsonpath.Option; -import com.jayway.jsonpath.PathNotFoundException; -import com.jayway.jsonpath.internal.PathRef; - -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)) { - for (String property : ctx.jsonProvider().getPropertyKeys(model)) { - handleObjectProperty(currentPath, model, ctx, asList(property)); - } - } else if (ctx.jsonProvider().isArray(model)) { - for (int idx = 0; idx < ctx.jsonProvider().length(model); idx++) { - try { - handleArrayIndex(idx, currentPath, model, ctx); - } catch (PathNotFoundException p){ - if(ctx.options().contains(Option.REQUIRE_PROPERTIES)){ - throw p; - } - } - } - } - } - - - @Override - public boolean isTokenDefinite() { - return false; - } - - @Override - public String getPathFragment() { - return "[*]"; - } -} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/DefaultEvaluator.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/DefaultEvaluator.java new file mode 100644 index 00000000..0ea0fe45 --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/DefaultEvaluator.java @@ -0,0 +1,52 @@ +package com.jayway.jsonpath.internal.path.eval; + +import com.jayway.jsonpath.internal.PathRef; +import com.jayway.jsonpath.internal.path.EvaluationContextImpl; +import com.jayway.jsonpath.internal.path.EvaluatorDispatcher; +import com.jayway.jsonpath.internal.path.token.*; + +import java.util.LinkedHashMap; +import java.util.Map; + +public class DefaultEvaluator implements EvaluatorDispatcher { + /** + * Type safe cache abstraction + */ + protected class EvaluatorCache { + protected final LinkedHashMap, ITokenEvaluator> evaluators = new LinkedHashMap, ITokenEvaluator>(); + + public

> void put(final Class

tokenClass, final E evaluator) { + evaluators.put(tokenClass, evaluator); + } + + public

> E get(final P token) { + final Class wantClass = token.getClass(); + for (final Map.Entry, ITokenEvaluator> pair : evaluators.entrySet()) { + if (pair.getKey().isAssignableFrom(wantClass)) { + //noinspection unchecked + return (E) pair.getValue(); + } + } + throw new IllegalArgumentException("Unknown PathToken type: " + wantClass.getCanonicalName()); + } + } + + protected final EvaluatorCache evaluators; + + public DefaultEvaluator() { + evaluators = new EvaluatorCache(); + evaluators.put(IndexArrayPathToken.class, new IndexArrayTokenEvaluator(this)); + evaluators.put(SliceArrayPathToken.class, new SliceArrayTokenEvaluator(this)); + evaluators.put(FunctionPathToken.class, new FunctionTokenEvaluator(this)); + evaluators.put(PredicatePathToken.class, new PredicateTokenEvaluator(this)); + evaluators.put(PropertyPathToken.class, new PropertyTokenEvaluator(this)); + evaluators.put(RootPathToken.class, new RootTokenEvaluator(this)); + evaluators.put(ScanPathToken.class, new ScanTokenEvaluator(this)); + evaluators.put(WildcardPathToken.class, new WildcardTokenEvaluator(this)); + } + + @Override + public void evaluate(final PathToken token, final String currentPath, final PathRef parent, final Object model, final EvaluationContextImpl ctx) { + evaluators.get(token).evaluate(token, currentPath, parent, model, ctx); + } +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/FunctionTokenEvaluator.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/FunctionTokenEvaluator.java new file mode 100644 index 00000000..9bd0ae62 --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/FunctionTokenEvaluator.java @@ -0,0 +1,21 @@ +package com.jayway.jsonpath.internal.path.eval; + +import com.jayway.jsonpath.internal.PathRef; +import com.jayway.jsonpath.internal.function.PathFunction; +import com.jayway.jsonpath.internal.function.PathFunctionFactory; +import com.jayway.jsonpath.internal.path.EvaluationContextImpl; +import com.jayway.jsonpath.internal.path.EvaluatorDispatcher; +import com.jayway.jsonpath.internal.path.token.FunctionPathToken; + +public class FunctionTokenEvaluator extends TokenEvaluator { + public FunctionTokenEvaluator(final EvaluatorDispatcher dispatcher) { + super(dispatcher); + } + + @Override + public void evaluate(final FunctionPathToken token, final String currentPath, final PathRef parent, final Object model, final EvaluationContextImpl ctx) { + PathFunction pathFunction = PathFunctionFactory.newFunction(token.getFunctionName()); + Object result = pathFunction.invoke(currentPath, parent, model, ctx); + ctx.addResult(currentPath, parent, result); + } +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/ITokenEvaluator.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/ITokenEvaluator.java new file mode 100644 index 00000000..450a4f5a --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/ITokenEvaluator.java @@ -0,0 +1,9 @@ +package com.jayway.jsonpath.internal.path.eval; + +import com.jayway.jsonpath.internal.PathRef; +import com.jayway.jsonpath.internal.path.EvaluationContextImpl; +import com.jayway.jsonpath.internal.path.token.PathToken; + +public interface ITokenEvaluator

{ + void evaluate(P token, String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx); +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/IndexArrayTokenEvaluator.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/IndexArrayTokenEvaluator.java new file mode 100644 index 00000000..23c37705 --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/IndexArrayTokenEvaluator.java @@ -0,0 +1,36 @@ +package com.jayway.jsonpath.internal.path.eval; + +import com.jayway.jsonpath.internal.PathRef; +import com.jayway.jsonpath.internal.path.EvaluationContextImpl; +import com.jayway.jsonpath.internal.path.EvaluatorDispatcher; +import com.jayway.jsonpath.internal.path.operation.ArrayIndexOperation; +import com.jayway.jsonpath.internal.path.token.IndexArrayPathToken; + +public class IndexArrayTokenEvaluator extends TokenEvaluator { + public IndexArrayTokenEvaluator(final EvaluatorDispatcher dispatcher) { + super(dispatcher); + } + + @Override + public void evaluate(final IndexArrayPathToken token, final String currentPath, final PathRef parent, final Object model, final EvaluationContextImpl ctx) { + if (! checkArrayModel(token, currentPath, model, ctx)) + return; + evaluateIndexOperation(token, currentPath, parent, model, ctx); + } + + public void evaluateIndexOperation(final IndexArrayPathToken token, String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx) { + + if (! checkArrayModel(token, currentPath, model, ctx)) + return; + + final ArrayIndexOperation operation = token.operation(); + if(operation.isSingleIndexOperation()){ + handleArrayIndex(token, operation.indexes().get(0), currentPath, model, ctx); + } else { + for (Integer index : operation.indexes()) { + handleArrayIndex(token, index, currentPath, model, ctx); + } + } + } + +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/PredicateTokenEvaluator.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/PredicateTokenEvaluator.java new file mode 100644 index 00000000..abf6aeec --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/PredicateTokenEvaluator.java @@ -0,0 +1,43 @@ +package com.jayway.jsonpath.internal.path.eval; + +import com.jayway.jsonpath.InvalidPathException; +import com.jayway.jsonpath.internal.PathRef; +import com.jayway.jsonpath.internal.path.EvaluationContextImpl; +import com.jayway.jsonpath.internal.path.EvaluatorDispatcher; +import com.jayway.jsonpath.internal.path.token.PredicatePathToken; + +import static java.lang.String.format; + +public class PredicateTokenEvaluator extends TokenEvaluator { + public PredicateTokenEvaluator(final EvaluatorDispatcher dispatcher) { + super(dispatcher); + } + + @Override + public void evaluate(final PredicatePathToken token, final String currentPath, final PathRef parent, final Object model, final EvaluationContextImpl ctx) { + if (ctx.jsonProvider().isMap(model)) { + if (token.accept(model, ctx.rootDocument(), ctx.configuration(), ctx)) { + PathRef op = ctx.forUpdate() ? parent : PathRef.NO_OP; + if (token.isLeaf()) { + ctx.addResult(currentPath, op, model); + } else { + dispatcher.evaluate(token.next(), currentPath, op, model, ctx); + } + } + } else if (ctx.jsonProvider().isArray(model)){ + int idx = 0; + Iterable objects = ctx.jsonProvider().toIterable(model); + + for (Object idxModel : objects) { + if (token.accept(idxModel, ctx.rootDocument(), ctx.configuration(), ctx)) { + handleArrayIndex(token, idx, currentPath, model, ctx); + } + idx++; + } + } else { + if (token.isUpstreamDefinite()) { + throw new InvalidPathException(format("Filter: %s can not be applied to primitives. Current context is: %s", toString(), model)); + } + } + } +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/PropertyTokenEvaluator.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/PropertyTokenEvaluator.java new file mode 100644 index 00000000..b96d87f2 --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/PropertyTokenEvaluator.java @@ -0,0 +1,50 @@ +package com.jayway.jsonpath.internal.path.eval; + +import com.jayway.jsonpath.PathNotFoundException; +import com.jayway.jsonpath.internal.PathRef; +import com.jayway.jsonpath.internal.path.EvaluationContextImpl; +import com.jayway.jsonpath.internal.path.EvaluatorDispatcher; +import com.jayway.jsonpath.internal.path.token.PropertyPathToken; + +import java.util.ArrayList; +import java.util.List; + +import static com.jayway.jsonpath.internal.Utils.onlyOneIsTrueNonThrow; + +public class PropertyTokenEvaluator extends TokenEvaluator { + public PropertyTokenEvaluator(final EvaluatorDispatcher dispatcher) { + super(dispatcher); + } + + @Override + public void evaluate(final PropertyPathToken token, final String currentPath, final PathRef parent, final Object model, final EvaluationContextImpl ctx) { + // Can't assert it in ctor because isLeaf() could be changed later on. + assert onlyOneIsTrueNonThrow(token.singlePropertyCase(), token.multiPropertyMergeCase(), token.multiPropertyIterationCase()); + + if (!ctx.jsonProvider().isMap(model)) { + if (! token.isUpstreamDefinite()) { + return; + } else { + String m = model == null ? "null" : model.getClass().getName(); + + throw new PathNotFoundException(String.format( + "Expected to find an object with property %s in path %s but found '%s'. " + + "This is not a json object according to the JsonProvider: '%s'.", + token.getPathFragment(), currentPath, m, ctx.configuration().jsonProvider().getClass().getName())); + } + } + + if (token.singlePropertyCase() || token.multiPropertyMergeCase()) { + handleObjectProperty(token, currentPath, model, ctx, token.getProperties()); + return; + } + + assert token.multiPropertyIterationCase(); + final List currentlyHandledProperty = new ArrayList(1); + currentlyHandledProperty.add(null); + for (final String property : token.getProperties()) { + currentlyHandledProperty.set(0, property); + handleObjectProperty(token, currentPath, model, ctx, currentlyHandledProperty); + } + } +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/RootTokenEvaluator.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/RootTokenEvaluator.java new file mode 100644 index 00000000..e8647702 --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/RootTokenEvaluator.java @@ -0,0 +1,22 @@ +package com.jayway.jsonpath.internal.path.eval; + +import com.jayway.jsonpath.internal.PathRef; +import com.jayway.jsonpath.internal.path.EvaluationContextImpl; +import com.jayway.jsonpath.internal.path.EvaluatorDispatcher; +import com.jayway.jsonpath.internal.path.token.RootPathToken; + +public class RootTokenEvaluator extends TokenEvaluator { + public RootTokenEvaluator(final EvaluatorDispatcher dispatcher) { + super(dispatcher); + } + + @Override + public void evaluate(final RootPathToken token, final String currentPath, final PathRef parent, final Object model, final EvaluationContextImpl ctx) { + if (token.isLeaf()) { + PathRef op = ctx.forUpdate() ? parent : PathRef.NO_OP; + ctx.addResult(token.getPathFragment(), op, model); + } else { + dispatcher.evaluate(token.next(), token.getPathFragment(), parent, model, ctx); + } + } +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/ScanTokenEvaluator.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/ScanTokenEvaluator.java new file mode 100644 index 00000000..ffeb451b --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/ScanTokenEvaluator.java @@ -0,0 +1,95 @@ +package com.jayway.jsonpath.internal.path.eval; + +import com.jayway.jsonpath.internal.PathRef; +import com.jayway.jsonpath.internal.path.*; +import com.jayway.jsonpath.internal.path.predicate.*; +import com.jayway.jsonpath.internal.path.token.*; +import com.jayway.jsonpath.spi.json.JsonProvider; + +import java.util.Collection; + +public class ScanTokenEvaluator extends TokenEvaluator { + public ScanTokenEvaluator(final EvaluatorDispatcher dispatcher) { + super(dispatcher); + } + + @Override + public void evaluate(final ScanPathToken token, final String currentPath, final PathRef parent, final Object model, final EvaluationContextImpl ctx) { + PathToken pt = token.next(); + + walk(pt, currentPath, parent, model, ctx, createScanPredicate(pt, ctx)); + + } + + public void walk(PathToken pt, String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx, PathTokenPredicate predicate) { + if (ctx.jsonProvider().isMap(model)) { + walkObject(pt, currentPath, parent, model, ctx, predicate); + } else if (ctx.jsonProvider().isArray(model)) { + walkArray(pt, currentPath, parent, model, ctx, predicate); + } + } + + public void walkArray(PathToken pt, String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx, PathTokenPredicate predicate) { + + if (predicate.matches(model)) { + if (pt.isLeaf()) { + dispatcher.evaluate(pt, currentPath, parent, model, ctx); + } else { + PathToken next = pt.next(); + Iterable models = ctx.jsonProvider().toIterable(model); + int idx = 0; + for (Object evalModel : models) { + String evalPath = currentPath + "[" + idx + "]"; + dispatcher.evaluate(next, evalPath, parent, evalModel, ctx); + idx++; + } + } + } + + Iterable models = ctx.jsonProvider().toIterable(model); + int idx = 0; + for (Object evalModel : models) { + String evalPath = currentPath + "[" + idx + "]"; + walk(pt, evalPath, PathRef.create(model, idx), evalModel, ctx, predicate); + idx++; + } + } + + public void walkObject(PathToken pt, String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx, PathTokenPredicate predicate) { + + if (predicate.matches(model)) { + dispatcher.evaluate(pt, currentPath, parent, model, ctx); + } + Collection properties = ctx.jsonProvider().getPropertyKeys(model); + + for (String property : properties) { + String evalPath = currentPath + "['" + property + "']"; + Object propertyModel = ctx.jsonProvider().getMapValue(model, property); + if (propertyModel != JsonProvider.UNDEFINED) { + walk(pt, evalPath, PathRef.create(model, property), propertyModel, ctx, predicate); + } + } + } + + public static PathTokenPredicate createScanPredicate(final PathToken target, final EvaluationContextImpl ctx) { + if (target instanceof PropertyPathToken) { + return new PropertyPathTokenPredicate(target, ctx); + } else if (target instanceof ArrayPathToken) { + return new ArrayPathTokenPredicate(ctx); + } else if (target instanceof WildcardPathToken) { + return new WildcardPathTokenPredicate(); + } else if (target instanceof PredicatePathToken) { + return new FilterPathTokenPredicate(target, ctx); + } else { + return FALSE_PREDICATE; + } + } + + private static final PathTokenPredicate FALSE_PREDICATE = new PathTokenPredicate() { + + @Override + public boolean matches(Object model) { + return false; + } + }; +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/SliceArrayTokenEvaluator.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/SliceArrayTokenEvaluator.java new file mode 100644 index 00000000..1d3c4d64 --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/SliceArrayTokenEvaluator.java @@ -0,0 +1,99 @@ +package com.jayway.jsonpath.internal.path.eval; + +import com.jayway.jsonpath.internal.PathRef; +import com.jayway.jsonpath.internal.path.*; +import com.jayway.jsonpath.internal.path.operation.ArraySliceOperation; +import com.jayway.jsonpath.internal.path.token.SliceArrayPathToken; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SliceArrayTokenEvaluator extends TokenEvaluator { + private static final Logger logger = LoggerFactory.getLogger(SliceArrayTokenEvaluator.class); + + public SliceArrayTokenEvaluator(final EvaluatorDispatcher dispatcher) { + super(dispatcher); + } + + @Override + public void evaluate(final SliceArrayPathToken token, final String currentPath, final PathRef parent, final Object model, final EvaluationContextImpl ctx) { + if (! checkArrayModel(token, currentPath, model, ctx)) + return; + evaluateSliceOperation(token, currentPath, parent, model, ctx); + } + + public void evaluateSliceOperation(final SliceArrayPathToken token, String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx) { + + if (! checkArrayModel(token, currentPath, model, ctx)) + return; + + final ArraySliceOperation operation = token.operation(); + switch (operation.operation()) { + case SLICE_FROM: + sliceFrom(token, currentPath, parent, model, ctx); + break; + case SLICE_BETWEEN: + sliceBetween(token, currentPath, parent, model, ctx); + break; + case SLICE_TO: + sliceTo(token, currentPath, parent, model, ctx); + break; + } + } + + public void sliceFrom(final SliceArrayPathToken token, String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx) { + int length = ctx.jsonProvider().length(model); + int from = token.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(token, i, currentPath, model, ctx); + } + } + + public void sliceBetween(final SliceArrayPathToken token, String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx) { + int length = ctx.jsonProvider().length(model); + int from = token.operation().from(); + int to = token.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(token, i, currentPath, model, ctx); + } + } + + public void sliceTo(final SliceArrayPathToken token, String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx) { + int length = ctx.jsonProvider().length(model); + if (length == 0) { + return; + } + int to = token.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(token, i, currentPath, model, ctx); + } + } + +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/TokenEvaluator.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/TokenEvaluator.java new file mode 100644 index 00000000..8e13fc63 --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/TokenEvaluator.java @@ -0,0 +1,156 @@ +package com.jayway.jsonpath.internal.path.eval; + +import com.jayway.jsonpath.InvalidPathException; +import com.jayway.jsonpath.Option; +import com.jayway.jsonpath.PathNotFoundException; +import com.jayway.jsonpath.internal.PathRef; +import com.jayway.jsonpath.internal.Utils; +import com.jayway.jsonpath.internal.path.EvaluationContextImpl; +import com.jayway.jsonpath.internal.path.EvaluatorDispatcher; +import com.jayway.jsonpath.internal.path.token.ArrayPathToken; +import com.jayway.jsonpath.internal.path.token.PathToken; +import com.jayway.jsonpath.internal.path.token.PropertyPathToken; +import com.jayway.jsonpath.spi.json.JsonProvider; + +import java.util.List; + +import static java.lang.String.format; + +public abstract class TokenEvaluator

implements ITokenEvaluator

{ + + protected final EvaluatorDispatcher dispatcher; + + public TokenEvaluator(final EvaluatorDispatcher dispatcher) { + this.dispatcher = dispatcher; + } + + public abstract void evaluate(final P token, String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx); + + /** + * Check if model is non-null and array. + * @param currentPath + * @param model + * @param ctx + * @return false if current evaluation call must be skipped, true otherwise + * @throws PathNotFoundException if model is null and evaluation must be interrupted + * @throws InvalidPathException if model is not an array and evaluation must be interrupted + */ + public boolean checkArrayModel(final ArrayPathToken token, String currentPath, Object model, EvaluationContextImpl ctx) { + if (model == null){ + if (! token.isUpstreamDefinite()) { + return false; + } else { + throw new PathNotFoundException("The path " + currentPath + " is null"); + } + } + if (!ctx.jsonProvider().isArray(model)) { + if (! token.isUpstreamDefinite()) { + return false; + } else { + throw new PathNotFoundException(format("Filter: %s can only be applied to arrays. Current context is: %s", toString(), model)); + } + } + return true; + } + + + public void handleObjectProperty(final P token, String currentPath, Object model, EvaluationContextImpl ctx, List properties) { + + if(properties.size() == 1) { + String property = properties.get(0); + String evalPath = Utils.concat(currentPath, "['", property, "']"); + Object propertyVal = readObjectProperty(property, model, ctx); + if(propertyVal == JsonProvider.UNDEFINED){ + // Conditions below heavily depend on current token type (and its logic) and are not "universal", + // so this code is quite dangerous (I'd rather rewrite it & move to PropertyPathToken and implemented + // WildcardPathToken as a dynamic multi prop case of PropertyPathToken). + // Better safe than sorry. + assert token instanceof PropertyPathToken : "only PropertyPathToken is supported"; + + if(token.isLeaf()) { + if(ctx.options().contains(Option.DEFAULT_PATH_LEAF_TO_NULL)){ + propertyVal = null; + } else { + if(ctx.options().contains(Option.SUPPRESS_EXCEPTIONS) || + !ctx.options().contains(Option.REQUIRE_PROPERTIES)){ + return; + } else { + throw new PathNotFoundException("No results for path: " + evalPath); + } + } + } else { + if (! (token.isUpstreamDefinite() && token.isTokenDefinite()) && + !ctx.options().contains(Option.REQUIRE_PROPERTIES) || + ctx.options().contains(Option.SUPPRESS_EXCEPTIONS)){ + // If there is some indefiniteness in the path and properties are not required - we'll ignore + // absent property. And also in case of exception suppression - so that other path evaluation + // branches could be examined. + return; + } else { + throw new PathNotFoundException("Missing property in path " + evalPath); + } + } + } + PathRef pathRef = ctx.forUpdate() ? PathRef.create(model, property) : PathRef.NO_OP; + if (token.isLeaf()) { + ctx.addResult(evalPath, pathRef, propertyVal); + } + else { + dispatcher.evaluate(token.next(), evalPath, pathRef, propertyVal, ctx); + } + } else { + String evalPath = currentPath + "[" + Utils.join(", ", "'", properties) + "]"; + + assert token.isLeaf() : "non-leaf multi props handled elsewhere"; + + Object merged = ctx.jsonProvider().createMap(); + for (String property : properties) { + Object propertyVal; + if(hasProperty(property, model, ctx)) { + propertyVal = readObjectProperty(property, model, ctx); + if(propertyVal == JsonProvider.UNDEFINED){ + if(ctx.options().contains(Option.DEFAULT_PATH_LEAF_TO_NULL)) { + propertyVal = null; + } else { + continue; + } + } + } else { + if(ctx.options().contains(Option.DEFAULT_PATH_LEAF_TO_NULL)){ + propertyVal = null; + } else if (ctx.options().contains(Option.REQUIRE_PROPERTIES)) { + throw new PathNotFoundException("Missing property in path " + evalPath); + } else { + continue; + } + } + ctx.jsonProvider().setProperty(merged, property, propertyVal); + } + PathRef pathRef = ctx.forUpdate() ? PathRef.create(model, properties) : PathRef.NO_OP; + ctx.addResult(evalPath, pathRef, merged); + } + } + + private static boolean hasProperty(String property, Object model, EvaluationContextImpl ctx) { + return ctx.jsonProvider().getPropertyKeys(model).contains(property); + } + + private static Object readObjectProperty(String property, Object model, EvaluationContextImpl ctx) { + return ctx.jsonProvider().getMapValue(model, property); + } + + + public void handleArrayIndex(final P token, 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 { + Object evalHit = ctx.jsonProvider().getArrayIndex(model, index); + if (token.isLeaf()) { + ctx.addResult(evalPath, pathRef, evalHit); + } else { + dispatcher.evaluate(token.next(), evalPath, pathRef, evalHit, ctx); + } + } catch (IndexOutOfBoundsException e) { + } + } +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/WildcardTokenEvaluator.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/WildcardTokenEvaluator.java new file mode 100644 index 00000000..2e269732 --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/path/eval/WildcardTokenEvaluator.java @@ -0,0 +1,35 @@ +package com.jayway.jsonpath.internal.path.eval; + +import com.jayway.jsonpath.Option; +import com.jayway.jsonpath.PathNotFoundException; +import com.jayway.jsonpath.internal.PathRef; +import com.jayway.jsonpath.internal.path.EvaluationContextImpl; +import com.jayway.jsonpath.internal.path.EvaluatorDispatcher; +import com.jayway.jsonpath.internal.path.token.WildcardPathToken; + +import java.util.Collections; + +public class WildcardTokenEvaluator extends TokenEvaluator { + public WildcardTokenEvaluator(final EvaluatorDispatcher dispatcher) { + super(dispatcher); + } + + @Override + public void evaluate(final WildcardPathToken token, final String currentPath, final PathRef parent, final Object model, final EvaluationContextImpl ctx) { + if (ctx.jsonProvider().isMap(model)) { + for (String property : ctx.jsonProvider().getPropertyKeys(model)) { + handleObjectProperty(token, currentPath, model, ctx, Collections.singletonList(property)); + } + } else if (ctx.jsonProvider().isArray(model)) { + for (int idx = 0; idx < ctx.jsonProvider().length(model); idx++) { + try { + handleArrayIndex(token, idx, currentPath, model, ctx); + } catch (PathNotFoundException p){ + if(ctx.options().contains(Option.REQUIRE_PROPERTIES)){ + throw p; + } + } + } + } + } +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/ArrayIndexOperation.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/operation/ArrayIndexOperation.java similarity index 96% rename from json-path/src/main/java/com/jayway/jsonpath/internal/path/ArrayIndexOperation.java rename to json-path/src/main/java/com/jayway/jsonpath/internal/path/operation/ArrayIndexOperation.java index 0fd833a0..1662bd36 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/path/ArrayIndexOperation.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/path/operation/ArrayIndexOperation.java @@ -1,4 +1,4 @@ -package com.jayway.jsonpath.internal.path; +package com.jayway.jsonpath.internal.path.operation; import com.jayway.jsonpath.InvalidPathException; import com.jayway.jsonpath.internal.Utils; diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/ArraySliceOperation.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/operation/ArraySliceOperation.java similarity index 66% rename from json-path/src/main/java/com/jayway/jsonpath/internal/path/ArraySliceOperation.java rename to json-path/src/main/java/com/jayway/jsonpath/internal/path/operation/ArraySliceOperation.java index e6dcb8c1..99c78b2b 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/path/ArraySliceOperation.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/path/operation/ArraySliceOperation.java @@ -1,10 +1,10 @@ -package com.jayway.jsonpath.internal.path; +package com.jayway.jsonpath.internal.path.operation; import com.jayway.jsonpath.InvalidPathException; import static java.lang.Character.isDigit; -public class ArraySliceOperation { +public abstract class ArraySliceOperation { public enum Operation { SLICE_FROM, @@ -14,12 +14,10 @@ public class ArraySliceOperation { private final Integer from; private final Integer to; - private final Operation operation; - private ArraySliceOperation(Integer from, Integer to, Operation operation) { + protected ArraySliceOperation(Integer from, Integer to) { this.from = from; this.to = to; - this.operation = operation; } public Integer from() { @@ -30,9 +28,7 @@ public class ArraySliceOperation { return to; } - public Operation operation() { - return operation; - } + public abstract Operation operation(); @Override public String toString() { @@ -58,19 +54,15 @@ public class ArraySliceOperation { 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 SliceFromOperation(tempFrom); + } else if(tempFrom != null){ + return new SliceBetweenOperation(tempFrom, tempTo); + } else if(tempTo != null){ + return new SliceToOperation(tempTo); } - - return new ArraySliceOperation(tempFrom, tempTo, tempOperpation); + throw new InvalidPathException("Failed to parse SliceOperation: " + operation); } private static Integer tryRead(String[] tokens, int idx){ diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/operation/SliceBetweenOperation.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/operation/SliceBetweenOperation.java new file mode 100644 index 00000000..373b4c3a --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/path/operation/SliceBetweenOperation.java @@ -0,0 +1,12 @@ +package com.jayway.jsonpath.internal.path.operation; + +public class SliceBetweenOperation extends ArraySliceOperation { + protected SliceBetweenOperation(final Integer from, final Integer to) { + super(from, to); + } + + @Override + public Operation operation() { + return Operation.SLICE_BETWEEN; + } +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/operation/SliceFromOperation.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/operation/SliceFromOperation.java new file mode 100644 index 00000000..6e642b57 --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/path/operation/SliceFromOperation.java @@ -0,0 +1,12 @@ +package com.jayway.jsonpath.internal.path.operation; + +public class SliceFromOperation extends ArraySliceOperation { + protected SliceFromOperation(Integer from) { + super(from, null); + } + + @Override + public Operation operation() { + return Operation.SLICE_FROM; + } +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/operation/SliceToOperation.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/operation/SliceToOperation.java new file mode 100644 index 00000000..ddc9820b --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/path/operation/SliceToOperation.java @@ -0,0 +1,12 @@ +package com.jayway.jsonpath.internal.path.operation; + +public class SliceToOperation extends ArraySliceOperation { + protected SliceToOperation(final Integer to) { + super(null, to); + } + + @Override + public Operation operation() { + return Operation.SLICE_TO; + } +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/predicate/ArrayPathTokenPredicate.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/predicate/ArrayPathTokenPredicate.java new file mode 100644 index 00000000..af1ad028 --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/path/predicate/ArrayPathTokenPredicate.java @@ -0,0 +1,16 @@ +package com.jayway.jsonpath.internal.path.predicate; + +import com.jayway.jsonpath.internal.path.EvaluationContextImpl; + +public final class ArrayPathTokenPredicate implements PathTokenPredicate { + private final EvaluationContextImpl ctx; + + public ArrayPathTokenPredicate(EvaluationContextImpl ctx) { + this.ctx = ctx; + } + + @Override + public boolean matches(Object model) { + return ctx.jsonProvider().isArray(model); + } +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/predicate/FilterPathTokenPredicate.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/predicate/FilterPathTokenPredicate.java new file mode 100644 index 00000000..81e00176 --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/path/predicate/FilterPathTokenPredicate.java @@ -0,0 +1,20 @@ +package com.jayway.jsonpath.internal.path.predicate; + +import com.jayway.jsonpath.internal.path.EvaluationContextImpl; +import com.jayway.jsonpath.internal.path.token.PathToken; +import com.jayway.jsonpath.internal.path.token.PredicatePathToken; + +public final class FilterPathTokenPredicate implements PathTokenPredicate { + private final EvaluationContextImpl ctx; + private PredicatePathToken predicatePathToken; + + public FilterPathTokenPredicate(PathToken target, EvaluationContextImpl ctx) { + this.ctx = ctx; + predicatePathToken = (PredicatePathToken) target; + } + + @Override + public boolean matches(Object model) { + return predicatePathToken.accept(model, ctx.rootDocument(), ctx.configuration(), ctx); + } +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/predicate/PathTokenPredicate.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/predicate/PathTokenPredicate.java new file mode 100644 index 00000000..04182ba2 --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/path/predicate/PathTokenPredicate.java @@ -0,0 +1,5 @@ +package com.jayway.jsonpath.internal.path.predicate; + +public interface PathTokenPredicate { + boolean matches(Object model); +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/predicate/PropertyPathTokenPredicate.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/predicate/PropertyPathTokenPredicate.java new file mode 100644 index 00000000..718a2f01 --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/path/predicate/PropertyPathTokenPredicate.java @@ -0,0 +1,52 @@ +package com.jayway.jsonpath.internal.path.predicate; + +import com.jayway.jsonpath.Option; +import com.jayway.jsonpath.internal.path.EvaluationContextImpl; +import com.jayway.jsonpath.internal.path.token.PathToken; +import com.jayway.jsonpath.internal.path.token.PropertyPathToken; + +import java.util.Collection; + +public final class PropertyPathTokenPredicate implements PathTokenPredicate { + private final EvaluationContextImpl ctx; + private PropertyPathToken propertyPathToken; + + public PropertyPathTokenPredicate(PathToken target, EvaluationContextImpl ctx) { + this.ctx = ctx; + propertyPathToken = (PropertyPathToken) target; + } + + @Override + public boolean matches(Object model) { + if (! ctx.jsonProvider().isMap(model)) { + return false; + } + +// +// The commented code below makes it really hard understand, use and predict the result +// of deep scanning operations. It might be correct but was decided to be +// left out until the behavior of REQUIRE_PROPERTIES is more strictly defined +// in a deep scanning scenario. For details read conversation in commit +// https://github.com/jayway/JsonPath/commit/1a72fc078deb16995e323442bfb681bd715ce45a#commitcomment-14616092 +// +// if (ctx.options().contains(Option.REQUIRE_PROPERTIES)) { +// // Have to require properties defined in path when an indefinite path is evaluated, +// // so have to go there and search for it. +// return true; +// } + + if (! propertyPathToken.isTokenDefinite()) { + // It's responsibility of PropertyPathToken code to handle indefinite scenario of properties, + // so we'll allow it to do its job. + return true; + } + + if (propertyPathToken.isLeaf() && ctx.options().contains(Option.DEFAULT_PATH_LEAF_TO_NULL)) { + // In case of DEFAULT_PATH_LEAF_TO_NULL missing properties is not a problem. + return true; + } + + Collection keys = ctx.jsonProvider().getPropertyKeys(model); + return keys.containsAll(propertyPathToken.getProperties()); + } +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/predicate/WildcardPathTokenPredicate.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/predicate/WildcardPathTokenPredicate.java new file mode 100644 index 00000000..8211c33f --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/path/predicate/WildcardPathTokenPredicate.java @@ -0,0 +1,8 @@ +package com.jayway.jsonpath.internal.path.predicate; + +public final class WildcardPathTokenPredicate implements PathTokenPredicate { + @Override + public boolean matches(Object model) { + return true; + } +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/token/ArrayPathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/token/ArrayPathToken.java new file mode 100644 index 00000000..56f9af98 --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/path/token/ArrayPathToken.java @@ -0,0 +1,22 @@ +/* + * 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.path.token; + +/** + * Placeholder common ancestor for {@link IndexArrayPathToken} + * and {@link SliceArrayPathToken} + */ +public abstract class ArrayPathToken extends PathToken { +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/FunctionPathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/token/FunctionPathToken.java similarity index 68% rename from json-path/src/main/java/com/jayway/jsonpath/internal/path/FunctionPathToken.java rename to json-path/src/main/java/com/jayway/jsonpath/internal/path/token/FunctionPathToken.java index beef14db..6c970064 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/path/FunctionPathToken.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/path/token/FunctionPathToken.java @@ -1,7 +1,5 @@ -package com.jayway.jsonpath.internal.path; +package com.jayway.jsonpath.internal.path.token; -import com.jayway.jsonpath.internal.PathRef; -import com.jayway.jsonpath.internal.function.PathFunction; import com.jayway.jsonpath.internal.function.PathFunctionFactory; /** @@ -25,13 +23,6 @@ public class FunctionPathToken extends PathToken { } } - @Override - public void evaluate(String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx) { - PathFunction pathFunction = PathFunctionFactory.newFunction(functionName); - Object result = pathFunction.invoke(currentPath, parent, model, ctx); - ctx.addResult(currentPath, parent, result); - } - /** * Return the actual value by indicating true. If this return was false then we'd return the value in an array which * isn't what is desired - true indicates the raw value is returned. @@ -48,5 +39,7 @@ public class FunctionPathToken extends PathToken { return "." + pathFragment; } - + public String getFunctionName() { + return functionName; + } } diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/token/IndexArrayPathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/token/IndexArrayPathToken.java new file mode 100644 index 00000000..97c67b7e --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/path/token/IndexArrayPathToken.java @@ -0,0 +1,27 @@ +package com.jayway.jsonpath.internal.path.token; + +import com.jayway.jsonpath.internal.path.operation.ArrayIndexOperation; + +public class IndexArrayPathToken extends ArrayPathToken { + protected final ArrayIndexOperation arrayIndexOperation; + + public IndexArrayPathToken(final ArrayIndexOperation arrayIndexOperation) { + super(); + if (arrayIndexOperation == null) throw new IllegalArgumentException(); + this.arrayIndexOperation = arrayIndexOperation; + } + + @Override + public String getPathFragment() { + return arrayIndexOperation.toString(); + } + + @Override + public boolean isTokenDefinite() { + return arrayIndexOperation.isSingleIndexOperation(); + } + + public ArrayIndexOperation operation() { + return arrayIndexOperation; + } +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/token/PathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/token/PathToken.java new file mode 100644 index 00000000..3333d16d --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/path/token/PathToken.java @@ -0,0 +1,110 @@ +/* + * 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.path.token; + +import com.jayway.jsonpath.internal.PathRef; +import com.jayway.jsonpath.internal.function.PathFunction; +import com.jayway.jsonpath.internal.path.EvaluationContextImpl; + +public abstract class PathToken { + + private PathToken prev; + private PathToken next; + private Boolean definite = null; + private Boolean upstreamDefinite = null; + + public PathToken appendTailToken(PathToken next) { + this.next = next; + this.next.prev = this; + return next; + } + + public PathToken prev(){ + return prev; + } + + public PathToken next() { + if (isLeaf()) { + throw new IllegalStateException("Current path token is a leaf"); + } + return next; + } + + public boolean isLeaf() { + return next == null; + } + + public boolean isRoot() { + return prev == null; + } + + public boolean isUpstreamDefinite() { + if (upstreamDefinite == null) { + upstreamDefinite = isRoot() || prev.isTokenDefinite() && prev.isUpstreamDefinite(); + } + return upstreamDefinite; + } + + public int getTokenCount() { + int cnt = 1; + PathToken token = this; + + while (!token.isLeaf()){ + token = token.next(); + cnt++; + } + return cnt; + } + + public boolean isPathDefinite() { + if(definite != null){ + return definite.booleanValue(); + } + boolean isDefinite = isTokenDefinite(); + if (isDefinite && !isLeaf()) { + isDefinite = next.isPathDefinite(); + } + definite = isDefinite; + return isDefinite; + } + + @Override + public String toString() { + if (isLeaf()) { + return getPathFragment(); + } else { + return getPathFragment() + next().toString(); + } + } + + @Override + public int hashCode() { + return toString().hashCode(); + } + + @Override + public boolean equals(Object obj) { + return super.equals(obj); + } + + public void invoke(PathFunction pathFunction, String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx) { + ctx.addResult(currentPath, parent, pathFunction.invoke(currentPath, parent, model, ctx)); + } + + public abstract boolean isTokenDefinite(); + + protected abstract String getPathFragment(); + +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/PredicatePathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/token/PredicatePathToken.java similarity index 56% rename from json-path/src/main/java/com/jayway/jsonpath/internal/path/PredicatePathToken.java rename to json-path/src/main/java/com/jayway/jsonpath/internal/path/token/PredicatePathToken.java index b50b848c..8285980d 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/path/PredicatePathToken.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/path/token/PredicatePathToken.java @@ -12,16 +12,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.jayway.jsonpath.internal.path; +package com.jayway.jsonpath.internal.path.token; import com.jayway.jsonpath.Configuration; -import com.jayway.jsonpath.InvalidPathException; import com.jayway.jsonpath.Predicate; -import com.jayway.jsonpath.internal.PathRef; +import com.jayway.jsonpath.internal.path.EvaluationContextImpl; +import com.jayway.jsonpath.internal.path.PredicateContextImpl; import java.util.Collection; -import static java.lang.String.format; import static java.util.Arrays.asList; /** @@ -29,45 +28,16 @@ import static java.util.Arrays.asList; */ public class PredicatePathToken extends PathToken { - private final Collection predicates; - PredicatePathToken(Predicate filter) { + public PredicatePathToken(Predicate filter) { this.predicates = asList(filter); } - PredicatePathToken(Collection predicates) { + public PredicatePathToken(Collection predicates) { this.predicates = predicates; } - @Override - public void evaluate(String currentPath, PathRef ref, Object model, EvaluationContextImpl ctx) { - if (ctx.jsonProvider().isMap(model)) { - if (accept(model, ctx.rootDocument(), ctx.configuration(), ctx)) { - PathRef op = ctx.forUpdate() ? ref : PathRef.NO_OP; - if (isLeaf()) { - ctx.addResult(currentPath, op, model); - } else { - next().evaluate(currentPath, op, model, ctx); - } - } - } else if (ctx.jsonProvider().isArray(model)){ - int idx = 0; - Iterable objects = ctx.jsonProvider().toIterable(model); - - for (Object idxModel : objects) { - if (accept(idxModel, ctx.rootDocument(), ctx.configuration(), ctx)) { - handleArrayIndex(idx, currentPath, model, ctx); - } - idx++; - } - } else { - if (isUpstreamDefinite()) { - throw new InvalidPathException(format("Filter: %s can not be applied to primitives. Current context is: %s", toString(), model)); - } - } - } - public boolean accept(final Object obj, final Object root, final Configuration configuration, EvaluationContextImpl evaluationContext) { Predicate.PredicateContext ctx = new PredicateContextImpl(obj, root, configuration, evaluationContext.documentEvalCache()); diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/PropertyPathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/token/PropertyPathToken.java similarity index 53% rename from json-path/src/main/java/com/jayway/jsonpath/internal/path/PropertyPathToken.java rename to json-path/src/main/java/com/jayway/jsonpath/internal/path/token/PropertyPathToken.java index 74d280e4..41f7472d 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/path/PropertyPathToken.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/path/token/PropertyPathToken.java @@ -12,22 +12,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.jayway.jsonpath.internal.path; +package com.jayway.jsonpath.internal.path.token; import com.jayway.jsonpath.InvalidPathException; -import com.jayway.jsonpath.PathNotFoundException; -import com.jayway.jsonpath.internal.PathRef; import com.jayway.jsonpath.internal.Utils; -import java.util.ArrayList; import java.util.List; -import static com.jayway.jsonpath.internal.Utils.onlyOneIsTrueNonThrow; - /** * */ -class PropertyPathToken extends PathToken { +public class PropertyPathToken extends PathToken { private final List properties; private final String stringDelimiter; @@ -57,38 +52,6 @@ class PropertyPathToken extends PathToken { return ! isLeaf() && properties.size() > 1; } - @Override - public void evaluate(String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx) { - // Can't assert it in ctor because isLeaf() could be changed later on. - assert onlyOneIsTrueNonThrow(singlePropertyCase(), multiPropertyMergeCase(), multiPropertyIterationCase()); - - if (!ctx.jsonProvider().isMap(model)) { - if (! isUpstreamDefinite()) { - return; - } else { - String m = model == null ? "null" : model.getClass().getName(); - - throw new PathNotFoundException(String.format( - "Expected to find an object with property %s in path %s but found '%s'. " + - "This is not a json object according to the JsonProvider: '%s'.", - getPathFragment(), currentPath, m, ctx.configuration().jsonProvider().getClass().getName())); - } - } - - if (singlePropertyCase() || multiPropertyMergeCase()) { - handleObjectProperty(currentPath, model, ctx, properties); - return; - } - - assert multiPropertyIterationCase(); - final List currentlyHandledProperty = new ArrayList(1); - currentlyHandledProperty.add(null); - for (final String property : properties) { - currentlyHandledProperty.set(0, property); - handleObjectProperty(currentPath, model, ctx, currentlyHandledProperty); - } - } - @Override public boolean isTokenDefinite() { // in case of leaf multiprops will be merged, so it's kinda definite diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/RootPathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/token/RootPathToken.java similarity index 77% rename from json-path/src/main/java/com/jayway/jsonpath/internal/path/RootPathToken.java rename to json-path/src/main/java/com/jayway/jsonpath/internal/path/token/RootPathToken.java index 2b2db7b1..3a84085c 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/path/RootPathToken.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/path/token/RootPathToken.java @@ -12,9 +12,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.jayway.jsonpath.internal.path; +package com.jayway.jsonpath.internal.path.token; -import com.jayway.jsonpath.internal.PathRef; +import com.jayway.jsonpath.internal.path.PathTokenAppender; /** * @@ -26,7 +26,7 @@ public class RootPathToken extends PathToken { private final String rootToken; - RootPathToken(char rootToken) { + public RootPathToken(char rootToken) { this.rootToken = Character.toString(rootToken);; this.tail = this; this.tokenCount = 1; @@ -53,16 +53,6 @@ public class RootPathToken extends PathToken { }; } - @Override - public void evaluate(String currentPath, PathRef pathRef, Object model, EvaluationContextImpl ctx) { - if (isLeaf()) { - PathRef op = ctx.forUpdate() ? pathRef : PathRef.NO_OP; - ctx.addResult(rootToken, op, model); - } else { - next().evaluate(rootToken, pathRef, model, ctx); - } - } - @Override public String getPathFragment() { return rootToken; diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/token/ScanPathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/token/ScanPathToken.java new file mode 100644 index 00000000..1f815d3d --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/path/token/ScanPathToken.java @@ -0,0 +1,35 @@ +/* + * 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.path.token; + +/** + * + */ +public class ScanPathToken extends PathToken { + + public ScanPathToken() { + } + + @Override + public boolean isTokenDefinite() { + return false; + } + + @Override + public String getPathFragment() { + return ".."; + } + +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/token/SliceArrayPathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/token/SliceArrayPathToken.java new file mode 100644 index 00000000..540cebef --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/path/token/SliceArrayPathToken.java @@ -0,0 +1,27 @@ +package com.jayway.jsonpath.internal.path.token; + +import com.jayway.jsonpath.internal.path.operation.ArraySliceOperation; + +public class SliceArrayPathToken extends ArrayPathToken { + protected final ArraySliceOperation arraySliceOperation; + + public SliceArrayPathToken(final ArraySliceOperation arraySliceOperation) { + super(); + if (arraySliceOperation == null) throw new IllegalArgumentException(); + this.arraySliceOperation = arraySliceOperation; + } + + @Override + public String getPathFragment() { + return arraySliceOperation.toString(); + } + + @Override + public boolean isTokenDefinite() { + return false; + } + + public ArraySliceOperation operation() { + return arraySliceOperation; + } +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/token/WildcardPathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/token/WildcardPathToken.java new file mode 100644 index 00000000..7e99d197 --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/path/token/WildcardPathToken.java @@ -0,0 +1,34 @@ +/* + * 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.path.token; + +/** + * + */ +public class WildcardPathToken extends PathToken { + + public WildcardPathToken() { + } + + @Override + public boolean isTokenDefinite() { + return false; + } + + @Override + public String getPathFragment() { + return "[*]"; + } +} diff --git a/json-path/src/test/java/com/jayway/jsonpath/internal/path/PathTokenTest.java b/json-path/src/test/java/com/jayway/jsonpath/internal/path/PathTokenTest.java index 9d87a7a3..8a193aa7 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/internal/path/PathTokenTest.java +++ b/json-path/src/test/java/com/jayway/jsonpath/internal/path/PathTokenTest.java @@ -1,6 +1,10 @@ package com.jayway.jsonpath.internal.path; import com.jayway.jsonpath.BaseTest; +import com.jayway.jsonpath.internal.path.token.PathToken; +import com.jayway.jsonpath.internal.path.token.PropertyPathToken; +import com.jayway.jsonpath.internal.path.token.ScanPathToken; +import com.jayway.jsonpath.internal.path.token.WildcardPathToken; import org.junit.Test; import java.util.Arrays;