diff --git a/build.gradle b/build.gradle index ec8aa0c7..ebed1509 100644 --- a/build.gradle +++ b/build.gradle @@ -41,6 +41,7 @@ subprojects { apply plugin: 'java' apply plugin: 'signing' apply plugin: 'osgi' + apply plugin: 'maven' sourceCompatibility = 1.6 targetCompatibility = 1.6 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 2c1a7905..fb0ecdc5 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/JsonPath.java +++ b/json-path/src/main/java/com/jayway/jsonpath/JsonPath.java @@ -15,12 +15,7 @@ package com.jayway.jsonpath; -import com.jayway.jsonpath.internal.EvaluationContext; -import com.jayway.jsonpath.internal.JsonReader; -import com.jayway.jsonpath.internal.Path; -import com.jayway.jsonpath.internal.PathCompiler; -import com.jayway.jsonpath.internal.PathRef; -import com.jayway.jsonpath.internal.Utils; +import com.jayway.jsonpath.internal.*; import com.jayway.jsonpath.spi.json.JsonProvider; import java.io.File; @@ -226,6 +221,17 @@ public class JsonPath { return resultByConfiguration(jsonObject, configuration, evaluationContext); } + public T convert(Object jsonObject, ValueConverter valueConverter, Configuration configuration) { + notNull(jsonObject, "json can not be null"); + notNull(configuration, "configuration can not be null"); + EvaluationContext evaluationContext = path.evaluate(jsonObject, jsonObject, configuration, true); + for (PathRef updateOperation : evaluationContext.updateOperations()) { + updateOperation.convert(valueConverter, configuration); + } + return resultByConfiguration(jsonObject, configuration, evaluationContext); + } + + /** * Deletes the object this path points to in the provided jsonObject * @@ -284,6 +290,17 @@ public class JsonPath { return resultByConfiguration(jsonObject, configuration, evaluationContext); } + public T renameKey(Object jsonObject, String oldKeyName, String newKeyName, Configuration configuration){ + notNull(jsonObject, "json can not be null"); + notEmpty(newKeyName, "newKeyName can not be null or empty"); + notNull(configuration, "configuration can not be null"); + EvaluationContext evaluationContext = path.evaluate(jsonObject, jsonObject, configuration, true); + for (PathRef updateOperation : evaluationContext.updateOperations()) { + updateOperation.renameKey(oldKeyName, newKeyName, configuration); + } + return resultByConfiguration(jsonObject, configuration, evaluationContext); + } + /** * Applies this JsonPath to the provided json string * diff --git a/json-path/src/main/java/com/jayway/jsonpath/WriteContext.java b/json-path/src/main/java/com/jayway/jsonpath/WriteContext.java index 0f827895..4c617a71 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/WriteContext.java +++ b/json-path/src/main/java/com/jayway/jsonpath/WriteContext.java @@ -14,6 +14,8 @@ */ package com.jayway.jsonpath; +import com.jayway.jsonpath.internal.ValueConverter; + public interface WriteContext { /** @@ -56,6 +58,25 @@ public interface WriteContext { */ DocumentContext set(JsonPath path, Object newValue); + /** + * Converts the value on the given path. + * + * @param path path to be converted set + * @param valueConverter Converter object to be invoked (or lambda:)) + * @param filters filters + * @return a document context + */ + DocumentContext convert(String path, ValueConverter valueConverter, Predicate... filters); + + /** + * Converts the value on the given path. + * + * @param path path to be converted set + * @param valueConverter Converter object to be invoked (or lambda:)) + * @return a document context + */ + DocumentContext convert(JsonPath path, ValueConverter valueConverter); + /** * Deletes the given path * @@ -116,6 +137,7 @@ public interface WriteContext { */ DocumentContext put(String path, String key, Object value, Predicate... filters); + /** * Add or update the key with a the given value at the given path * @@ -126,4 +148,25 @@ public interface WriteContext { */ DocumentContext put(JsonPath path, String key, Object value); -} + /** + * Renames the last key element of a given path. + * @param path The path to the old key. Should be resolved to a map + * or an array including map items. + * @param oldKeyName The old key name. + * @param newKeyName The new key name. + * @param filters filters. + * @return a document content. + */ + DocumentContext renameKey(String path, String oldKeyName, String newKeyName, Predicate... filters); + + /** + * Renames the last key element of a given path. + * @param path The path to the old key. Should be resolved to a map + * or an array including map items. + * @param oldKeyName The old key name. + * @param newKeyName The new key name. + * @return a document content. + */ + DocumentContext renameKey(JsonPath path, String oldKeyName, String newKeyName); + +} \ No newline at end of file diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/JsonReader.java b/json-path/src/main/java/com/jayway/jsonpath/internal/JsonReader.java index 5e072bfb..fad5c6a1 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/JsonReader.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/JsonReader.java @@ -36,6 +36,7 @@ import java.util.LinkedList; import java.util.List; import static com.jayway.jsonpath.JsonPath.compile; +import static com.jayway.jsonpath.JsonPath.parse; import static com.jayway.jsonpath.internal.Utils.notEmpty; import static com.jayway.jsonpath.internal.Utils.notNull; import static java.util.Arrays.asList; @@ -212,6 +213,18 @@ public class JsonReader implements ParseContext, DocumentContext { return this; } + @Override + public DocumentContext convert(String path, ValueConverter valueConverter, Predicate... filters) { + convert(compile(path, filters), valueConverter); + return this; + } + + @Override + public DocumentContext convert(JsonPath path, ValueConverter valueConverter) { + path.convert(json, valueConverter, configuration); + return this; + } + @Override public DocumentContext delete(String path, Predicate... filters) { return delete(compile(path, filters)); @@ -249,6 +262,23 @@ public class JsonReader implements ParseContext, DocumentContext { return put(compile(path, filters), key, value); } + @Override + public DocumentContext renameKey(String path, String oldKeyName, String newKeyName, Predicate... filters) { + return renameKey(compile(path, filters), oldKeyName, newKeyName); + } + + @Override + public DocumentContext renameKey(JsonPath path, String oldKeyName, String newKeyName) { + List modified = path.renameKey(json, oldKeyName, newKeyName, configuration.addOptions(Option.AS_PATH_LIST)); + if(logger.isDebugEnabled()){ + for (String p : modified) { + logger.debug("Rename path {} new value {}", p, newKeyName); + } + } + return this; + } + + @Override public DocumentContext put(JsonPath path, String key, Object value){ List modified = path.put(json, key, value, configuration.addOptions(Option.AS_PATH_LIST)); diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/PathRef.java b/json-path/src/main/java/com/jayway/jsonpath/internal/PathRef.java index b5192869..6a2031fb 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/PathRef.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/PathRef.java @@ -1,10 +1,10 @@ package com.jayway.jsonpath.internal; -import com.jayway.jsonpath.Configuration; -import com.jayway.jsonpath.InvalidModificationException; +import com.jayway.jsonpath.*; import com.jayway.jsonpath.spi.json.JsonProvider; import java.util.Collection; +import java.util.List; public abstract class PathRef implements Comparable { @@ -17,6 +17,9 @@ public abstract class PathRef implements Comparable { @Override public void set(Object newVal, Configuration configuration) {} + @Override + public void convert(ValueConverter valueConverter, Configuration configuration) {} + @Override public void delete(Configuration configuration) {} @@ -25,6 +28,10 @@ public abstract class PathRef implements Comparable { @Override public void put(String key, Object newVal, Configuration configuration) {} + + @Override + public void renameKey(String oldKeyName, String newKeyName, Configuration configuration) {} + }; protected Object parent; @@ -38,12 +45,32 @@ public abstract class PathRef implements Comparable { public abstract void set(Object newVal, Configuration configuration); + public abstract void convert(ValueConverter valueConverter, Configuration configuration); + public abstract void delete(Configuration configuration); public abstract void add(Object newVal, Configuration configuration); public abstract void put(String key, Object newVal, Configuration configuration); + public abstract void renameKey(String oldKey,String newKeyName, Configuration configuration); + + protected void renameInMap(Object targetMap, String oldKeyName, String newKeyName, Configuration configuration){ + if(configuration.jsonProvider().isMap(targetMap)){ + if(configuration.jsonProvider().getMapValue(targetMap, oldKeyName) == JsonProvider.UNDEFINED){ + throw new PathNotFoundException("No results for Key "+oldKeyName+" found in map!"); + } + configuration.jsonProvider().setProperty(targetMap, newKeyName, configuration.jsonProvider().getMapValue(targetMap, oldKeyName)); + configuration.jsonProvider().removeProperty(targetMap, oldKeyName); + } else { + throw new InvalidModificationException("Can only rename properties in a map"); + } + } + + protected boolean targetInvalid(Object target){ + return target == JsonProvider.UNDEFINED || target == null; + } + @Override public int compareTo(PathRef o) { return this.getAccessor().toString().compareTo(o.getAccessor().toString()) * -1; @@ -81,6 +108,10 @@ public abstract class PathRef implements Comparable { throw new InvalidModificationException("Invalid delete operation"); } + public void convert(ValueConverter valueConverter, Configuration configuration){ + throw new InvalidModificationException("Invalid convert operation"); + } + @Override public void delete(Configuration configuration) { throw new InvalidModificationException("Invalid delete operation"); @@ -103,27 +134,45 @@ public abstract class PathRef implements Comparable { throw new InvalidModificationException("Invalid put operation. $ is not a map"); } } + + @Override + public void renameKey(String oldKeyName, String newKeyName, Configuration configuration) { + Object target = parent; + if(targetInvalid(target)){ + return; + } + renameInMap(target, oldKeyName, newKeyName, configuration); + } + } + private static class ArrayIndexPathRef extends PathRef { private int index; + private Object value; private ArrayIndexPathRef(Object parent, int index) { super(parent); this.index = index; + this.value = ((List) parent).get(index); } public void set(Object newVal, Configuration configuration){ configuration.jsonProvider().setArrayIndex(parent, index, newVal); } + public void convert(ValueConverter valueConverter, Configuration configuration){ + Object currentValue = configuration.jsonProvider().getArrayIndex(parent, index); + configuration.jsonProvider().setArrayIndex(parent, index, valueConverter.convert(currentValue)); + } + public void delete(Configuration configuration){ - configuration.jsonProvider().removeProperty(parent, index); + configuration.jsonProvider().removeProperty(parent, value); } public void add(Object value, Configuration configuration){ Object target = configuration.jsonProvider().getArrayIndex(parent, index); - if(target == JsonProvider.UNDEFINED || target == null){ + if(targetInvalid(target)){ return; } if(configuration.jsonProvider().isArray(target)){ @@ -135,7 +184,7 @@ public abstract class PathRef implements Comparable { public void put(String key, Object value, Configuration configuration){ Object target = configuration.jsonProvider().getArrayIndex(parent, index); - if(target == JsonProvider.UNDEFINED || target == null){ + if(targetInvalid(target)){ return; } if(configuration.jsonProvider().isMap(target)){ @@ -145,6 +194,15 @@ public abstract class PathRef implements Comparable { } } + @Override + public void renameKey(String oldKeyName, String newKeyName, Configuration configuration) { + Object target = configuration.jsonProvider().getArrayIndex(parent, index); + if(targetInvalid(target)){ + return; + } + renameInMap(target, oldKeyName, newKeyName, configuration); + } + @Override public Object getAccessor() { return index; @@ -175,13 +233,20 @@ public abstract class PathRef implements Comparable { configuration.jsonProvider().setProperty(parent, property, newVal); } + @Override + public void convert(ValueConverter valueConverter, Configuration configuration) { + Object currentValue = configuration.jsonProvider().getMapValue(parent, property); + configuration.jsonProvider().setProperty(parent, property, valueConverter.convert(currentValue)); + } + + public void delete(Configuration configuration){ configuration.jsonProvider().removeProperty(parent, property); } public void add(Object value, Configuration configuration){ Object target = configuration.jsonProvider().getMapValue(parent, property); - if(target == JsonProvider.UNDEFINED || target == null){ + if(targetInvalid(target)){ return; } if(configuration.jsonProvider().isArray(target)){ @@ -193,7 +258,7 @@ public abstract class PathRef implements Comparable { public void put(String key, Object value, Configuration configuration){ Object target = configuration.jsonProvider().getMapValue(parent, property); - if(target == JsonProvider.UNDEFINED || target == null){ + if(targetInvalid(target)){ return; } if(configuration.jsonProvider().isMap(target)){ @@ -203,6 +268,15 @@ public abstract class PathRef implements Comparable { } } + @Override + public void renameKey(String oldKeyName, String newKeyName, Configuration configuration) { + Object target = configuration.jsonProvider().getMapValue(parent, property); + if(targetInvalid(target)){ + return; + } + renameInMap(target, oldKeyName, newKeyName, configuration); + } + @Override public Object getAccessor() { return property; @@ -223,6 +297,12 @@ public abstract class PathRef implements Comparable { configuration.jsonProvider().setProperty(parent, property, newVal); } } + public void convert(ValueConverter valueConverter, Configuration configuration) { + for (String property : properties) { + Object currentValue = configuration.jsonProvider().getMapValue(parent, property); + configuration.jsonProvider().setProperty(parent, property, valueConverter.convert(currentValue)); + } + } public void delete(Configuration configuration){ for (String property : properties) { @@ -237,7 +317,12 @@ public abstract class PathRef implements Comparable { @Override public void put(String key, Object newVal, Configuration configuration) { - throw new InvalidModificationException("Add can not be performed to multiple properties"); + throw new InvalidModificationException("Put can not be performed to multiple properties"); + } + + @Override + public void renameKey(String oldKeyName, String newKeyName, Configuration configuration) { + throw new InvalidModificationException("Rename can not be performed to multiple properties"); } @Override diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/ValueConverter.java b/json-path/src/main/java/com/jayway/jsonpath/internal/ValueConverter.java new file mode 100644 index 00000000..ca2b31b3 --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/ValueConverter.java @@ -0,0 +1,10 @@ +package com.jayway.jsonpath.internal; + +/** + * Created by tom on 29/04/15. + */ +public interface ValueConverter { + + public Object convert(Object currentValue); + +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/spi/json/AbstractJsonProvider.java b/json-path/src/main/java/com/jayway/jsonpath/spi/json/AbstractJsonProvider.java index 56cfb47c..f95102e5 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/spi/json/AbstractJsonProvider.java +++ b/json-path/src/main/java/com/jayway/jsonpath/spi/json/AbstractJsonProvider.java @@ -99,16 +99,16 @@ public abstract class AbstractJsonProvider implements JsonProvider { * Removes a value in an object or array * * @param obj an array or an object - * @param key a String key or a numerical index to remove + * @param key a String key or an object in a Collection to be removed. */ @SuppressWarnings("unchecked") public void removeProperty(Object obj, Object key) { if (isMap(obj)) ((Map) obj).remove(key.toString()); else { - List list = (List) obj; - int index = key instanceof Integer ? (Integer) key : Integer.parseInt(key.toString()); - list.remove(index); + Collection collection = (Collection) obj; + //int index = key instanceof Integer ? (Integer) key : Integer.parseInt(key.toString()); + collection.remove(key); } } diff --git a/json-path/src/test/java/com/jayway/jsonpath/WriteTest.java b/json-path/src/test/java/com/jayway/jsonpath/WriteTest.java index d295a8a7..aa4e2446 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/WriteTest.java +++ b/json-path/src/test/java/com/jayway/jsonpath/WriteTest.java @@ -1,11 +1,9 @@ package com.jayway.jsonpath; +import com.jayway.jsonpath.internal.ValueConverter; import org.junit.Test; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; +import java.util.*; import static com.jayway.jsonpath.JsonPath.parse; import static java.util.Collections.emptyMap; @@ -126,6 +124,15 @@ public class WriteTest extends BaseTest { assertThat(res).containsExactly("reference"); } + @Test + public void an_array_criteria_with_multiple_results_can_be_deleted(){ + String deletePath = "$._embedded.mandates[?(@.count=~/0/)]"; + DocumentContext documentContext = JsonPath.parse(getClass().getResourceAsStream("/json_array_multiple_delete.json")); + documentContext.delete(deletePath); + List result = documentContext.read(deletePath); + assertThat(result.size()).isEqualTo(0); + } + @Test public void multi_prop_delete() { @@ -214,6 +221,96 @@ public class WriteTest extends BaseTest { parse(model).set("$[?(@.a == 'a-val')]", 1); } + @Test + public void a_path_can_be_renamed(){ + Object o = parse(JSON_DOCUMENT).renameKey("$.store", "book", "updated-book").json(); + List result = parse(o).read("$.store.updated-book"); + + assertThat(result).isNotEmpty(); + } + + @Test + public void keys_in_root_containing_map_can_be_renamed(){ + Object o = parse(JSON_DOCUMENT).renameKey("$", "store", "new-store").json(); + List result = parse(o).read("$.new-store[*]"); + assertThat(result).isNotEmpty(); + } + @Test + public void map_array_items_can_be_renamed(){ + Object o = parse(JSON_DOCUMENT).renameKey("$.store.book[*]", "category", "renamed-category").json(); + List result = parse(o).read("$.store.book[*].renamed-category"); + assertThat(result).isNotEmpty(); + } + + @Test(expected = InvalidModificationException.class) + public void non_map_array_items_cannot_be_renamed(){ + List model = new LinkedList(); + model.add(1); + model.add(2); + parse(model).renameKey("$[*]", "oldKey", "newKey"); + } + + @Test(expected = InvalidModificationException.class) + public void multiple_properties_cannot_be_renamed(){ + parse(JSON_DOCUMENT).renameKey("$.store.book[*]['author', 'category']", "old-key", "new-key"); + } + + @Test(expected = PathNotFoundException.class) + public void non_existent_key_rename_not_allowed(){ + Object o = parse(JSON_DOCUMENT).renameKey("$", "fake", "new-fake").json(); + } + + @Test(expected = InvalidModificationException.class) + public void rootCannotBeConverted(){ + ValueConverter valueConverter = new ValueConverter() { + @Override + public Object convert(Object currentValue) { + return currentValue.toString()+"converted"; + } + }; + Object o = parse(JSON_DOCUMENT).convert("$", valueConverter).json(); + } + + @Test + public void single_match_value_can_be_converted(){ + ValueConverter valueConverter = new ToStringValueConverterImpl(); + String stringResult = parse(JSON_DOCUMENT).convert("$.string-property", valueConverter).read("$.string-property"); + assertThat(stringResult.endsWith("converted")).isTrue(); + } + + @Test + public void object_can_be_converted(){ + ValueConverter valueConverter = new ToStringValueConverterImpl(); + DocumentContext documentContext = parse(JSON_DOCUMENT); + Object list = documentContext.read("$..book"); + assertThat(list).isInstanceOf(List.class); + String result = ((List)documentContext.convert("$..book", valueConverter).read("$..book")).get(0); + assertThat(result).isInstanceOf(String.class); + assertThat(((String)result).endsWith("converted")).isTrue(); + } + + @Test + public void multi_match_path_can_be_converted(){ + ValueConverter valueConverter = new ToStringValueConverterImpl(); + List doubleResult = parse(JSON_DOCUMENT).read("$..display-price"); + for(Double dRes : doubleResult){ + assertThat(dRes).isInstanceOf(Double.class); + } + List stringResult = parse(JSON_DOCUMENT).convert("$..display-price", valueConverter).read("$..display-price"); + for(String sRes : stringResult){ + assertThat(sRes).isInstanceOf(String.class); + assertThat(sRes.endsWith("converted")).isTrue(); + } + } + + // Helper converter implementation for test cases. + private class ToStringValueConverterImpl implements ValueConverter{ + + @Override + public Object convert(Object currentValue) { + return currentValue.toString()+"converted"; + } + } -} +} \ No newline at end of file diff --git a/json-path/src/test/resources/json_array_multiple_delete.json b/json-path/src/test/resources/json_array_multiple_delete.json new file mode 100644 index 00000000..a5178cb1 --- /dev/null +++ b/json-path/src/test/resources/json_array_multiple_delete.json @@ -0,0 +1,54 @@ +{ + "_embedded": { + "mandates": [ + { + "count": "0", + "difference": "+0" + }, + { + "count": "0", + "difference": "+0" + }, + { + "count": "0", + "difference": "+0" + }, + { + "count": "0", + "difference": "+0" + }, + { + "count": "0", + "difference": "+0" + }, + { + "count": "10", + "difference": "+0" + }, + { + "count": "34", + "difference": "+0" + }, + { + "count": "2", + "difference": "+0" + }, + { + "count": "4", + "difference": "+0" + }, + { + "count": "0", + "difference": "+0" + }, + { + "count": "0", + "difference": "+0" + }, + { + "count": "0", + "difference": "+0" + } + ] + } +} \ No newline at end of file