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 6d341ed9..5156379c 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/JsonPath.java +++ b/json-path/src/main/java/com/jayway/jsonpath/JsonPath.java @@ -17,7 +17,8 @@ package com.jayway.jsonpath; import com.jayway.jsonpath.internal.JsonReader; import com.jayway.jsonpath.internal.Utils; -import com.jayway.jsonpath.internal.spi.compiler.PathCompiler; +import com.jayway.jsonpath.internal.spi.compiler.*; +import com.jayway.jsonpath.spi.compiler.EvaluationContext; import com.jayway.jsonpath.spi.compiler.Path; import com.jayway.jsonpath.spi.http.HttpProviderFactory; import com.jayway.jsonpath.spi.json.JsonProvider; @@ -28,6 +29,8 @@ import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.net.URL; +import java.util.List; +import java.util.Map; import static com.jayway.jsonpath.internal.Utils.*; @@ -203,6 +206,63 @@ public class JsonPath { } + /** + * Writes the provided value at this path to the provided json document. + * Note that the document must be identified as either a List or Map by + * the {@link JsonProvider} + * + * @param jsonObject a container Object + * @param value value to set + * @param configuration configuration to use + * @return a modified container object + */ + public Map write(Object jsonObject, Object value, Configuration configuration) { + + // new configuration that will always return list + Configuration.ConfigurationBuilder builder = new Configuration.ConfigurationBuilder(); + Configuration readConfig = builder + .jsonProvider(configuration.getProvider()) + .options(Option.ALWAYS_RETURN_LIST) + .build(); + + // clone the path for manipulation to prevent modifications by reference + Path clonedPath = path.clone(); + RootPathToken clonedRoot = clonedPath.getRoot(); + PathToken tail = clonedRoot.getTail(); + PathToken parent = tail.getPrevious(); + parent.setNext(null); + + // get results for parent paths + EvaluationContext res = clonedPath.evaluate(jsonObject, readConfig); + + for (Object valueResult : (List) res.getValueResult()) { + Class clazz = valueResult.getClass(); + // set index value in array + if (List.class.isAssignableFrom(clazz)) { + + ArrayPathToken arrayPathToken = (ArrayPathToken) tail; + List criteria = arrayPathToken.getCriteria(); + Integer index = criteria.get(0); + + List list = (List) valueResult; + list.set(index, value); + } + // set property value on object + else if (Map.class.isAssignableFrom(clazz)) { + + PropertyPathToken propertyPathToken = (PropertyPathToken) tail; + List properties = propertyPathToken.getProperties(); + String key = properties.get(0); + + Map map = (Map) valueResult; + map.put(key, value); + } + } + + return (Map) jsonObject; + } + + /** * Applies this JsonPath to the provided json string * @@ -215,6 +275,18 @@ public class JsonPath { return read(json, Configuration.defaultConfiguration()); } + /** + * Writes the provided value at this path to the provided json string. + * + * @param json a json string + * @param value value to set at path + * @return a modified container object + */ + public Map write(String json, Object value) { + Configuration configuration = Configuration.defaultConfiguration(); + return write(json, value, configuration); + } + /** * Applies this JsonPath to the provided json string * @@ -231,6 +303,18 @@ public class JsonPath { return read(configuration.getProvider().parse(json), configuration); } + /** + * + * @param json a json string + * @param value value to set at path + * @param configuration + * @return a modified container object + */ + public Map write(String json, Object value, Configuration configuration) { + Object jsonObject = configuration.getProvider().parse(json); + return write(jsonObject, value, configuration); + } + /** * Applies this JsonPath to the provided json URL * diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/ArrayPathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/ArrayPathToken.java index d10ebb56..3238393d 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/ArrayPathToken.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/ArrayPathToken.java @@ -13,7 +13,7 @@ import static java.lang.String.format; /** * */ -class ArrayPathToken extends PathToken { +public class ArrayPathToken extends PathToken { private static final Logger logger = LoggerFactory.getLogger(ArrayPathToken.class); @@ -36,6 +36,10 @@ class ArrayPathToken extends PathToken { this.isDefinite = (Operation.SINGLE_INDEX == operation || Operation.CONTEXT_SIZE == operation); } + public List getCriteria() { + return criteria; + } + @Override void evaluate(String currentPath, Object model, EvaluationContextImpl ctx) { if(model == null){ @@ -167,4 +171,12 @@ class ArrayPathToken extends PathToken { boolean isTokenDefinite() { return isDefinite; } + + @Override + public ArrayPathToken clone() { + ArrayPathToken pathToken = new ArrayPathToken(criteria, operation); + cloneTo(pathToken); + return pathToken; + } + } diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/CompiledPath.java b/json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/CompiledPath.java index 15ae72b6..3f9f8c29 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/CompiledPath.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/CompiledPath.java @@ -6,14 +6,17 @@ import com.jayway.jsonpath.spi.compiler.Path; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -class CompiledPath implements Path { +public class CompiledPath implements Path { private static final Logger logger = LoggerFactory.getLogger(CompiledPath.class); - private final PathToken root; + private final RootPathToken root; + public RootPathToken getRoot() { + return root; + } - public CompiledPath(PathToken root) { + public CompiledPath(RootPathToken root) { this.root = root; } @@ -34,6 +37,16 @@ class CompiledPath implements Path { return root.isPathDefinite(); } + @Override + public Path clone() { + RootPathToken newRoot = null; + if (root != null) { + newRoot = root.clone(); + } + Path path = new CompiledPath(newRoot); + return path; + } + @Override public String toString() { return root.toString(); diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/EvaluationContextImpl.java b/json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/EvaluationContextImpl.java index 2faee4ff..e1e23044 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/EvaluationContextImpl.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/EvaluationContextImpl.java @@ -49,6 +49,15 @@ class EvaluationContextImpl implements EvaluationContext { return configuration; } + @Override + public Object getValueResult() { + return valueResult; + } + + @Override + public Object getPathResult() { + return pathResult; + } @Override public T getValue() { diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/FilterPathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/FilterPathToken.java index b3add3c0..69b2957a 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/FilterPathToken.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/FilterPathToken.java @@ -70,4 +70,12 @@ class FilterPathToken extends PathToken { boolean isTokenDefinite() { return false; } + + @Override + public FilterPathToken clone() { + FilterPathToken pathToken = new FilterPathToken(filters); + cloneTo(pathToken); + return pathToken; + } + } diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/PathCompiler.java b/json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/PathCompiler.java index 307112c5..9281f708 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/PathCompiler.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/PathCompiler.java @@ -50,7 +50,11 @@ public class PathCompiler { } RootPathToken root = null; - + PathToken pathToken = null; + /** + * Track previous path token we looped through, for setting previous links. + */ + PathToken previousPathToken = null; char[] chars = path.toCharArray(); int i = 0; @@ -104,12 +108,20 @@ public class PathCompiler { i += positions; break; } + + pathToken = PathComponentAnalyzer.analyze(fragment, filterList); + + // set the previous link + pathToken.setPrevious(previousPathToken); + if (root == null) { - root = (RootPathToken) PathComponentAnalyzer.analyze(fragment, filterList); + root = (RootPathToken) pathToken; } else { - root.append(PathComponentAnalyzer.analyze(fragment, filterList)); + root.append(pathToken); } + previousPathToken = pathToken; + } while (i < chars.length); Path pa = new CompiledPath(root); diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/PathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/PathToken.java index 3690301a..8341c500 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/PathToken.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/PathToken.java @@ -8,11 +8,28 @@ import com.jayway.jsonpath.spi.json.JsonProvider; import java.util.List; -abstract class PathToken { +public abstract class PathToken { + private PathToken previous; private PathToken next; private Boolean definite = null; + public PathToken getPrevious() { + return previous; + } + + public void setPrevious(PathToken previous) { + this.previous = previous; + } + + public void setNext(PathToken next) { + this.next = next; + } + + public void setDefinte(Boolean definite) { + this.definite = definite; + } + PathToken appendTailToken(PathToken next) { this.next = next; return next; @@ -94,14 +111,14 @@ abstract class PathToken { } - PathToken next() { + public PathToken next() { if (isLeaf()) { throw new IllegalStateException("Current path token is a leaf"); } return next; } - boolean isLeaf() { + public boolean isLeaf() { return next == null; } @@ -154,4 +171,27 @@ abstract class PathToken { abstract String getPathFragment(); + @Override + abstract public PathToken clone(); + + /** + * Deep clone this path's fields to the provided path. + * While next links are cloned, previous links are only updated, to prevent infinite recursion. + * Cannot use standard clone and super calls for sub-classes due to abstract modifier. + * + * @param pathToken + */ + public void cloneTo(PathToken pathToken) { + PathToken newNext; + if (next != null) { + newNext = next.clone(); + newNext.setPrevious(pathToken); + } + else { + newNext = null; + } + pathToken.setNext(newNext); + pathToken.setDefinte(definite); + } + } diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/PropertyPathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/PropertyPathToken.java index 589c14cf..3b9c6c0a 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/PropertyPathToken.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/PropertyPathToken.java @@ -8,7 +8,7 @@ import java.util.List; /** * */ -class PropertyPathToken extends PathToken { +public class PropertyPathToken extends PathToken { private final List properties; @@ -41,4 +41,12 @@ class PropertyPathToken extends PathToken { .append(Utils.join(", ", "'", properties)) .append("]").toString(); } + + @Override + public PropertyPathToken clone() { + PropertyPathToken pathToken = new PropertyPathToken(properties); + cloneTo(pathToken); + return pathToken; + } + } diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/RootPathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/RootPathToken.java index 4270e23d..7686457d 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/RootPathToken.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/RootPathToken.java @@ -6,7 +6,7 @@ import org.slf4j.LoggerFactory; /** * */ -class RootPathToken extends PathToken /*implements Path*/ { +public class RootPathToken extends PathToken /*implements Path*/ { private static final Logger logger = LoggerFactory.getLogger(RootPathToken.class); @@ -18,11 +18,23 @@ class RootPathToken extends PathToken /*implements Path*/ { this.tokenCount = 1; } + public PathToken getTail() { + return tail; + } + + public void setTail(PathToken tail) { + this.tail = tail; + } + @Override public int getTokenCount() { return tokenCount; } + public void setTokenCount(int tokenCount) { + this.tokenCount = tokenCount; + } + public RootPathToken append(PathToken next) { this.tail = tail.appendTailToken(next); this.tokenCount++; @@ -63,4 +75,25 @@ class RootPathToken extends PathToken /*implements Path*/ { boolean isTokenDefinite() { return true; } + + public PathToken walkToTail() { + PathToken next = this; + while (next != null && ! next.isLeaf()) { + next = next.next(); + } + return next; + } + + @Override + public RootPathToken clone() { + RootPathToken pathToken = new RootPathToken(); + // deep clone 'next' path + cloneTo(pathToken); + // get new tail clone + PathToken tail = pathToken.walkToTail(); + pathToken.setTail(tail); + pathToken.setTokenCount(tokenCount); + return pathToken; + } + } diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/ScanPathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/ScanPathToken.java index 3aef735e..66743ed1 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/ScanPathToken.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/ScanPathToken.java @@ -171,4 +171,12 @@ class ScanPathToken extends PathToken { boolean matches(Object model); } + + @Override + public ScanPathToken clone() { + ScanPathToken pathToken = new ScanPathToken(); + cloneTo(pathToken); + return pathToken; + } + } diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/WildcardPathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/WildcardPathToken.java index bead8105..2b0ef7fb 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/WildcardPathToken.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/spi/compiler/WildcardPathToken.java @@ -30,4 +30,12 @@ class WildcardPathToken extends PathToken { public String getPathFragment() { return "[*]"; } + + @Override + public ScanPathToken clone() { + ScanPathToken pathToken = new ScanPathToken(); + cloneTo(pathToken); + return pathToken; + } + } diff --git a/json-path/src/main/java/com/jayway/jsonpath/spi/compiler/EvaluationContext.java b/json-path/src/main/java/com/jayway/jsonpath/spi/compiler/EvaluationContext.java index 99fa1adb..3d33c5c2 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/spi/compiler/EvaluationContext.java +++ b/json-path/src/main/java/com/jayway/jsonpath/spi/compiler/EvaluationContext.java @@ -12,6 +12,10 @@ public interface EvaluationContext { */ Configuration configuration(); + Object getValueResult(); + + Object getPathResult(); + /** * This method does not adhere to configuration settings. It will return a single object (not wrapped in a List) even if the * configuration contains the {@link com.jayway.jsonpath.Option#ALWAYS_RETURN_LIST} diff --git a/json-path/src/main/java/com/jayway/jsonpath/spi/compiler/Path.java b/json-path/src/main/java/com/jayway/jsonpath/spi/compiler/Path.java index cbda9f64..4bdd07f0 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/spi/compiler/Path.java +++ b/json-path/src/main/java/com/jayway/jsonpath/spi/compiler/Path.java @@ -1,6 +1,7 @@ package com.jayway.jsonpath.spi.compiler; import com.jayway.jsonpath.Configuration; +import com.jayway.jsonpath.internal.spi.compiler.RootPathToken; /** * @@ -9,6 +10,10 @@ public interface Path { EvaluationContext evaluate(Object model, Configuration configuration); + public RootPathToken getRoot(); + boolean isDefinite(); + public Path clone(); + } diff --git a/json-path/src/test/java/com/jayway/jsonpath/JsonPathTest.java b/json-path/src/test/java/com/jayway/jsonpath/JsonPathTest.java index 35611530..77a1c88e 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/JsonPathTest.java +++ b/json-path/src/test/java/com/jayway/jsonpath/JsonPathTest.java @@ -1,11 +1,13 @@ package com.jayway.jsonpath; import com.jayway.jsonpath.internal.spi.compiler.PathCompiler; +import com.jayway.jsonpath.spi.json.JsonProvider; import com.jayway.jsonpath.spi.json.JsonProviderFactory; import com.jayway.jsonpath.util.ScriptEngineJsonPath; import org.assertj.core.api.Assertions; import org.junit.Test; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -192,6 +194,70 @@ public class JsonPathTest { assertEquals(2, result.values().size()); } + @Test + public void write_store_book_author() throws Exception { + + JsonPath path = JsonPath.compile("$.store.book[0].author"); + + String writeAuthor = "Douglas Adams"; + Map jsonMap = path.write(DOCUMENT, writeAuthor); + + JsonProvider provider = JsonProviderFactory.createProvider(); + String json = provider.toJson(jsonMap); + String readAuthor = path.read(json); + assertEquals(writeAuthor, readAuthor); + } + + @Test + public void write_store_book_authors() throws Exception { + + JsonPath path = JsonPath.compile("$.store.book[0,1].author"); + + String writeAuthor = "Douglas Adams"; + Map jsonMap = path.write(DOCUMENT, writeAuthor); + + JsonProvider provider = JsonProviderFactory.createProvider(); + String json = provider.toJson(jsonMap); + List readAuthors = path.read(json); + + for (String readAuthor : readAuthors) { + assertEquals(writeAuthor, readAuthor); + } + } + + @Test + public void write_store_book_0_shallow() throws Exception { + + JsonPath path = JsonPath.compile("$.store.book[0]"); + + String writeBook = "Book"; + Map jsonMap = path.write(DOCUMENT, writeBook); + + JsonProvider provider = JsonProviderFactory.createProvider(); + String json = provider.toJson(jsonMap); + String readBook = path.read(json); + + assertEquals(writeBook, readBook); + } + + @Test + public void write_store_book_0_deep() throws Exception { + + JsonPath path = JsonPath.compile("$.store.book[0]"); + + HashMap writeBook = new HashMap(); + String writeBookAuthor = "New Author"; + writeBook.put("author", writeBookAuthor); + Map jsonMap = path.write(DOCUMENT, writeBook); + + JsonProvider provider = JsonProviderFactory.createProvider(); + String json = provider.toJson(jsonMap); + JsonPath readPath = JsonPath.compile("$.store.book[0].author"); + String readBookAuthor = readPath.read(json); + + assertEquals(writeBookAuthor, readBookAuthor); + } + @Test public void read_store_book_1() throws Exception {