From 693fe1710b9ea02b25a4fd12fb71d6629d0d2ea2 Mon Sep 17 00:00:00 2001 From: PLASH SPEED Date: Thu, 3 Jul 2025 15:30:28 +0800 Subject: [PATCH 1/3] support NestedJsonPath process --- .../jsonpath/NestedJsonPathWrapper.java | 380 ++++++++++++++++++ 1 file changed, 380 insertions(+) create mode 100644 json-path/src/main/java/com/jayway/jsonpath/NestedJsonPathWrapper.java diff --git a/json-path/src/main/java/com/jayway/jsonpath/NestedJsonPathWrapper.java b/json-path/src/main/java/com/jayway/jsonpath/NestedJsonPathWrapper.java new file mode 100644 index 00000000..d5932428 --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/NestedJsonPathWrapper.java @@ -0,0 +1,380 @@ +package com.jayway.jsonpath; + +import java.io.Serializable; +import java.util.Set; +import java.util.HashSet; +import java.util.Map; +import java.util.HashMap; +import java.util.Objects; + + +/** + * JSON path wrapper that supports accessing nested JSON strings using delimiter syntax. + * + *

Extends {@link DocumentContext} functionality to handle JSON strings embedded within JSON objects. + * Uses special delimiter syntax {@code {field}} to access nested JSON content.

+ * + *

Key Features

+ * + * + *

Usage Examples

+ *
+ * // Basic usage
+ * // Data example: {"user": {"name": "Alice"}}
+ * Map<String, Object> jsonData = new HashMap<>();
+ * NestedJsonPathWrapper wrapper = NestedJsonPathWrapper.wrapJsonOrMap(jsonData);
+ * wrapper.putValueByPath("$.user.name", "Alice");
+ * Object name = wrapper.getValueByPath("$.user.name");        // Gets "Alice"
+ *
+ * // Nested JSON access with default delimiters
+ * Map<String, Object> nestedData = new HashMap<>();
+ * nestedData.put("profile", "{\"name\":\"John\",\"age\":30,\"address\":{\"city\":\"NYC\",\"zip\":\"10001\"}}");
+ * nestedData.put("items", "[{\"id\":1,\"name\":\"item1\"},{\"id\":2,\"name\":\"item2\"}]");
+ * nestedData.put("tags", "[\"java\",\"json\",\"api\"]");
+ *
+ * NestedJsonPathWrapper nestedWrapper = NestedJsonPathWrapper.wrapJsonOrMap(nestedData);
+ *
+ * // Nested object access
+ * String name = (String) nestedWrapper.getValueByPath("$.{profile}.name");           // Gets "John"
+ * String city = (String) nestedWrapper.getValueByPath("$.{profile}.address.city");  // Gets "NYC"
+ * nestedWrapper.putValueByPath("$.{profile}.age", 35);                              // Update age
+ *
+ * // Nested array access
+ * String itemName = (String) nestedWrapper.getValueByPath("$.{items}[0].name");     // Gets "item1"
+ * String tag = (String) nestedWrapper.getValueByPath("$.{tags}[1]");                // Gets "json"
+ * nestedWrapper.putValueByPath("$.{items}[0].name", "newItem1");                    // Update item name
+ *
+ * // JSONPath functions on nested data
+ * Integer itemCount = (Integer) nestedWrapper.getValueByPath("$.{items}.length()"); // Gets array length
+ * Object allIds = nestedWrapper.getValueByPath("$.{items}[*].id");                  // Gets all IDs
+ *
+ * // Custom delimiters - when default {} conflicts with JSON keys
+ * // Data example: {"profile": "{\"name\":\"John\"}", "{profile}": "{\"name\":\"Jane\"}"}
+ * Map<String, Object> data = new HashMap<>();
+ * data.put("profile", "{\"name\":\"John\"}");        // Normal key
+ * data.put("{profile}", "{\"name\":\"Jane\"}");      // Key contains {}
+ *
+ * NestedJsonPathWrapper customWrapper = NestedJsonPathWrapper.wrapJsonOrMap(data, true, "«", "»");
+ * Object value1 = customWrapper.getValueByPath("$.«profile».name");      // Gets "John"
+ * Object value2 = customWrapper.getValueByPath("$.«{profile}».name");    // Gets "Jane"
+ * 
+ * + * @author Hercules + * @version 1.0 + * @see DocumentContext + * @see JsonPath + * @since 1.0 + */ +public class NestedJsonPathWrapper implements Serializable { + + /** The wrapped DocumentContext instance for JSON operations */ + private transient DocumentContext __self; + + /** Default opening delimiter for nested JSON access */ + private static final String DEFAULT_OPEN_DELIMITER = "{"; + + /** Default closing delimiter for nested JSON access */ + private static final String DEFAULT_CLOSE_DELIMITER = "}"; + + /** Default JsonPath configuration with safe path operations */ + private static final Configuration DEFAULT_CONFIGURATION = Configuration.defaultConfiguration() + .addOptions(Option.DEFAULT_PATH_LEAF_TO_NULL, Option.SUPPRESS_EXCEPTIONS); + + /** Opening delimiter for nested JSON field identification */ + private transient final String openDelimiter; + + /** Closing delimiter for nested JSON field identification */ + private transient final String closeDelimiter; + + /** Whether lazy loading is enabled for nested document changes */ + private transient final boolean lazyLoading; + + /** JsonPath configuration for parsing behavior */ + private transient final Configuration configuration; + + /** + * When you using lazyLoading mode,this cache will storage all child Document Path which it‘s has changed. + * This cache will destroy when user invokes the toJson() Or toJsonString() method + */ + private transient final Set changedSecondaryDocumentPath = new HashSet<>(); + + /** + * Cache for secondary document context wrappers. + * + *

This map stores {@link NestedJsonPathWrapper} instances for nested JSON + * strings, keyed by their path expressions. This allows for efficient reuse + * of parsed nested documents.

+ */ + private transient final Map __dic = new HashMap<>(); + + private NestedJsonPathWrapper(boolean lazyLoading, String openDelimiter, String closeDelimiter, Configuration configuration) { + this.lazyLoading = lazyLoading; + this.configuration = configuration != null ? configuration : DEFAULT_CONFIGURATION; + if(!Objects.equals(openDelimiter,closeDelimiter)) { + this.openDelimiter = openDelimiter; + this.closeDelimiter = closeDelimiter; + }else{ + throw new IllegalArgumentException("openDelimiter and closeDelimiter must be different"); + } + } + + private void setDocumentContext(DocumentContext documentContext) { + __self = documentContext; + } + + + /** Returns the wrapped DocumentContext instance */ + private DocumentContext getDocumentContext() { + return __self; + } + + /** Returns the document as JSON string, processing lazy loading changes first */ + public String toJsonString() { + processLazyLoadingContext(); + return __self != null ? __self.jsonString() : null; + } + + /** Returns the document as Java object, processing lazy loading changes first */ + public Object toJson() { + processLazyLoadingContext(); + return __self != null ? __self.json() : null; + } + + private void processLazyLoadingContext() { + if (lazyLoading) { + synchronized (changedSecondaryDocumentPath) { + for (String secondaryPath : changedSecondaryDocumentPath) { + NestedJsonPathWrapper child = __dic.get(secondaryPath); + if (child != null) { + String value = child.toJsonString(); + String originalPath = secondaryPath.replace(openDelimiter, "") + .replace(closeDelimiter, ""); + __self.set(originalPath, value); + } + } + changedSecondaryDocumentPath.clear(); + } + } + } + + /** + * Creates a wrapper from JSON string or Map with lazy loading enabled. + * + * @param param JSON string or Map instance + * @return new NestedJsonPathWrapper with lazy loading, or null if param is null + * @throws UnsupportedOperationException if parameter type is not supported + */ + public static NestedJsonPathWrapper wrapJsonOrMap(Object param) { + return wrapJsonOrMap(param, true); + } + + /** + * Creates a wrapper from JSON string or Map with configurable lazy loading. + * + * @param param JSON string or Map instance + * @param lazyLoading true for lazy loading, false for immediate updates + * @return new NestedJsonPathWrapper, or null if param is null + * @throws UnsupportedOperationException if parameter type is not supported + */ + public static NestedJsonPathWrapper wrapJsonOrMap(Object param, boolean lazyLoading) { + return wrapJsonOrMap(param, lazyLoading, DEFAULT_OPEN_DELIMITER, DEFAULT_CLOSE_DELIMITER); + } + + /** + * Creates a wrapper with custom JsonPath configuration. + * + * @param param JSON string or Map instance + * @param configuration JsonPath configuration, or null for default + * @return new NestedJsonPathWrapper, or null if param is null + * @throws UnsupportedOperationException if parameter type is not supported + */ + public static NestedJsonPathWrapper wrapJsonOrMap(Object param, Configuration configuration) { + return wrapJsonOrMap(param, true, DEFAULT_OPEN_DELIMITER, DEFAULT_CLOSE_DELIMITER, configuration); + } + + /** + * Creates a wrapper with configurable lazy loading and custom JsonPath configuration. + * + * @param param JSON string or Map instance + * @param lazyLoading true for lazy loading, false for immediate updates + * @param configuration JsonPath configuration, or null for default + * @return new NestedJsonPathWrapper, or null if param is null + * @throws UnsupportedOperationException if parameter type is not supported + */ + public static NestedJsonPathWrapper wrapJsonOrMap(Object param, boolean lazyLoading, Configuration configuration) { + return wrapJsonOrMap(param, lazyLoading, DEFAULT_OPEN_DELIMITER, DEFAULT_CLOSE_DELIMITER, configuration); + } + + /** + * Creates a wrapper with custom delimiters for nested JSON access. + * + * @param param JSON string or Map instance + * @param lazyLoading true for lazy loading, false for immediate updates + * @param openDelimiter opening delimiter for nested JSON fields + * @param closeDelimiter closing delimiter for nested JSON fields + * @return new NestedJsonPathWrapper, or null if param is null + * @throws UnsupportedOperationException if parameter type is not supported + * @throws IllegalArgumentException if delimiters are the same + */ + public static NestedJsonPathWrapper wrapJsonOrMap(Object param, boolean lazyLoading, String openDelimiter, String closeDelimiter) { + return wrapJsonOrMap(param, lazyLoading, openDelimiter, closeDelimiter, null); + } + + /** + * Creates a wrapper with full configuration options including custom delimiters and JsonPath configuration. + * + * @param param JSON string or Map instance + * @param lazyLoading true for lazy loading, false for immediate updates + * @param openDelimiter opening delimiter for nested JSON fields + * @param closeDelimiter closing delimiter for nested JSON fields + * @param configuration JsonPath configuration, or null for default + * @return new NestedJsonPathWrapper, or null if param is null + * @throws UnsupportedOperationException if parameter type is not supported + * @throws IllegalArgumentException if delimiters are the same + */ + public static NestedJsonPathWrapper wrapJsonOrMap(Object param, boolean lazyLoading, String openDelimiter, String closeDelimiter, Configuration configuration) { + if (param == null) { + return null; + } + if (param instanceof Map) { + return wrapDocumentContext(parseMap((Map) param, configuration), lazyLoading, openDelimiter, closeDelimiter, configuration); + } else if (param instanceof String) { + return wrapDocumentContext(parseJson(Objects.toString(param), configuration), lazyLoading, openDelimiter, closeDelimiter, configuration); + } + throw new UnsupportedOperationException("unsupported data type:" + param.getClass().getName()); + } + + /** Parses JSON string with default safe configuration */ + private static DocumentContext parseJson(String json) { + return parseJson(json, null); + } + + /** Parses JSON string with specified configuration */ + private static DocumentContext parseJson(String json, Configuration configuration) { + Configuration config = configuration != null ? configuration : DEFAULT_CONFIGURATION; + return JsonPath.using(config).parse(json); + } + + /** Parses Map with default safe configuration */ + private static DocumentContext parseMap(Map data) { + return parseMap(data, null); + } + + /** Parses Map with specified configuration */ + private static DocumentContext parseMap(Map data, Configuration configuration) { + Configuration config = configuration != null ? configuration : DEFAULT_CONFIGURATION; + return JsonPath.using(config).parse(data); + } + + /** Wraps DocumentContext with specified configuration */ + public static NestedJsonPathWrapper wrapDocumentContext(DocumentContext documentContext, boolean lazyLoading, String openDelimiter, String closeDelimiter) { + return wrapDocumentContext(documentContext, lazyLoading, openDelimiter, closeDelimiter, null); + } + + /** Wraps DocumentContext with full configuration including JsonPath settings */ + public static NestedJsonPathWrapper wrapDocumentContext(DocumentContext documentContext, boolean lazyLoading, String openDelimiter, String closeDelimiter, Configuration configuration) { + NestedJsonPathWrapper wrapper = new NestedJsonPathWrapper(lazyLoading, openDelimiter, closeDelimiter, configuration); + wrapper.setDocumentContext(documentContext); + return wrapper; + } + + /** + * Sets a value at the specified JSON path, supporting nested JSON access with delimiter syntax. + * + * @param path JSON path (e.g., "$.user.name" or "$.{profile}.age") + * @param value value to set + * @return true if successful, false if path is invalid + */ + public synchronized boolean putValueByPath(String path, Object value) { + if (strIsBlank(path)) { + return false; + } + if (path.contains(openDelimiter)) { + int index = path.indexOf(closeDelimiter); + String childPath = "$" + path.substring(index + 1); + NestedJsonPathWrapper childPathWrapper = discoverChildDocumentContextWrapper(path); + if(childPathWrapper == null){ + return false; + } + childPathWrapper.putValueByPath(childPath, value); + String rootPath = path.substring(0, index + 1); + String originalRootPath = rootPath.replace(openDelimiter, "") + .replace(closeDelimiter, ""); + if (lazyLoading) { + changedSecondaryDocumentPath.add(rootPath); + } else { + __self.set(originalRootPath, __dic.get(rootPath).getDocumentContext().jsonString()); + } + } else { + int index = path.lastIndexOf('.'); + String rootPath = path.substring(0, index); + String key = path.substring(index + 1); + __self.put(rootPath, key, value); + } + return true; + } + + /** + * Gets a value from the specified JSON path, supporting nested JSON access with delimiter syntax. + * + * @param path JSON path (e.g., "$.user.name" or "$.{profile}.age") + * @return value at the path, or null if not found + */ + public Object getValueByPath(String path) { + if (strIsBlank(path)) { + return null; + } + if (path.contains(openDelimiter)) { + int index = path.indexOf(closeDelimiter); + String childPath = "$" + path.substring(index + 1); + NestedJsonPathWrapper childPathWrapper = discoverChildDocumentContextWrapper(path); + return childPathWrapper != null ? childPathWrapper.getValueByPath(childPath) : null; + } else { + return __self != null ? __self.read(path) : null; + } + } + + + /** Discovers and caches child document wrapper for nested JSON access */ + private NestedJsonPathWrapper discoverChildDocumentContextWrapper(String path) { + int index = path.indexOf(closeDelimiter); + String rootPath = path.substring(0, index + 1); + String originalRootPath = rootPath.replace(openDelimiter, "") + .replace(closeDelimiter, ""); + if (!__dic.containsKey(rootPath)) { + synchronized (__dic) { + if (!__dic.containsKey(rootPath)) { + String insideValue = __self.read(originalRootPath); + if (strIsBlank(insideValue)) { + __dic.put(rootPath, null); + } else { + DocumentContext documentContext = parseJson(insideValue, this.configuration); + NestedJsonPathWrapper wrapper = wrapDocumentContext(documentContext, this.lazyLoading, this.openDelimiter, this.closeDelimiter, this.configuration); + __dic.put(rootPath, wrapper); + } + } + } + } + return __dic.get(rootPath); + } + + private boolean strIsBlank(CharSequence cs) { + int strLen = (cs == null ? 0 : cs.length()); + if (strLen == 0) { + return true; + } else { + for(int i = 0; i < strLen; ++i) { + if (!Character.isWhitespace(cs.charAt(i))) { + return false; + } + } + return true; + } + } +} From fda5646ea6bb9b0a0420ff4bf0fc71942575cff3 Mon Sep 17 00:00:00 2001 From: PLASH SPEED Date: Wed, 9 Jul 2025 12:41:49 +0800 Subject: [PATCH 2/3] support NestedJsonPath process --- .../main/java/com/jayway/jsonpath/NestedJsonPathWrapper.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/json-path/src/main/java/com/jayway/jsonpath/NestedJsonPathWrapper.java b/json-path/src/main/java/com/jayway/jsonpath/NestedJsonPathWrapper.java index d5932428..66f07594 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/NestedJsonPathWrapper.java +++ b/json-path/src/main/java/com/jayway/jsonpath/NestedJsonPathWrapper.java @@ -302,7 +302,9 @@ public synchronized boolean putValueByPath(String path, Object value) { if(childPathWrapper == null){ return false; } - childPathWrapper.putValueByPath(childPath, value); + if(!childPathWrapper.putValueByPath(childPath, value)){ + return false; + } String rootPath = path.substring(0, index + 1); String originalRootPath = rootPath.replace(openDelimiter, "") .replace(closeDelimiter, ""); From 33860de499fcd862486fcee7d16cd949cf29f80b Mon Sep 17 00:00:00 2001 From: PLASH SPEED Date: Wed, 9 Jul 2025 13:41:02 +0800 Subject: [PATCH 3/3] fix bug --- .../main/java/com/jayway/jsonpath/NestedJsonPathWrapper.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/json-path/src/main/java/com/jayway/jsonpath/NestedJsonPathWrapper.java b/json-path/src/main/java/com/jayway/jsonpath/NestedJsonPathWrapper.java index 66f07594..63c5b7a3 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/NestedJsonPathWrapper.java +++ b/json-path/src/main/java/com/jayway/jsonpath/NestedJsonPathWrapper.java @@ -313,13 +313,14 @@ public synchronized boolean putValueByPath(String path, Object value) { } else { __self.set(originalRootPath, __dic.get(rootPath).getDocumentContext().jsonString()); } + return true; } else { int index = path.lastIndexOf('.'); String rootPath = path.substring(0, index); String key = path.substring(index + 1); __self.put(rootPath, key, value); + return Objects.equals(value, __self.read(path)); } - return true; } /**