From 942e26bb5331ddd9901e9dea0f6c504935679ae7 Mon Sep 17 00:00:00 2001 From: Ziver Koc Date: Mon, 27 Mar 2023 01:32:40 +0200 Subject: [PATCH] Implemented DataNodePath class so DataNodes can be searched by path --- src/zutil/parser/DataNodePath.java | 198 ++++++++++++++++++++++++ test/zutil/parser/DataNodePathTest.java | 59 +++++++ 2 files changed, 257 insertions(+) create mode 100644 src/zutil/parser/DataNodePath.java create mode 100644 test/zutil/parser/DataNodePathTest.java diff --git a/src/zutil/parser/DataNodePath.java b/src/zutil/parser/DataNodePath.java new file mode 100644 index 0000000..91019be --- /dev/null +++ b/src/zutil/parser/DataNodePath.java @@ -0,0 +1,198 @@ +package zutil.parser; + +import zutil.ObjectUtil; +import zutil.StringUtil; + +import java.util.ArrayList; +import java.util.List; + +/** + * This is a class that implements the JSONPath syntax to lookup eƶements of a DataNode structure. + *
+ * Operators (From JSONPath spec): + * + * + * + * + * + * + * + *
OperatorDescription
$The root element to query. This starts all path expressions.
*Wildcard. Available anywhere a name or numeric are required.
.<name>Dot-notated child
['<name>']Bracket-notated child
[<number>]Array index
+ * + * Examples: + * + * + * + * + *
JSONPath Result
$.store.book[*].author a list of all authors for the books in the store
$.store.* all things in store, which are some books and a red bicycle.
+ * + * @see JSONPath Reference + */ +// TODO: * ['<name>' , '<name>']Bracket-notated children +// TODO: * [<number> , <number>]Array indexes +// TODO: * @The current node being processed by a filter predicate. +// TODO: * ..Deep scan. Available anywhere a name is required. +// TODO: * [start:end]Array slice operator +// TODO: * [?(<expression>)]Filter expression. Expression must evaluate to a boolean value. +public class DataNodePath { + private final String path; + private List pathEntityList; + + + public DataNodePath(String path) { + this.path = path; + pathEntityList = parsePath(path); + } + + private static List parsePath(String path) { + ArrayList pathList = new ArrayList<>(); + + if (ObjectUtil.isEmpty(path) || path.charAt(0) != '$') + throw new IllegalArgumentException("Path string must start with $"); + + char operator = 0; + StringBuilder buffer = new StringBuilder(); + + for (int i = 1; i <= path.length(); i++) { + char c = (i < path.length() ? path.charAt(i) : 0); + switch (c) { + case 0: // End of String + case '.': + case '[': + case ']': + case '*': + // Finalize previous operand + if (c == '*') { + pathList.add(new AllChildPathEntity()); + } else if (operator == '.') { + pathList.add(new NamedChildPathEntity(buffer.toString())); + } else if (operator == '[') { + if (buffer.charAt(0) == '\'' && buffer.charAt(buffer.length()-1) == '\'') + pathList.add(new NamedChildPathEntity(StringUtil.trim(buffer.toString(), '\''))); + else + pathList.add(new IndexChildPathEntity(Integer.parseInt(buffer.toString()))); + } + + buffer.delete(0, buffer.length()); + + // Start next operand + operator = c; + break; + + case '\'': // Read in everything until next quote + buffer.append(c); + for (; i < path.length(); i++) { + char c2 = path.charAt(i); + + buffer.append(c2); + if (c2 == c) + break; + } + break; + + default: + buffer.append(c); + } + } + + return pathList; + } + + + /** + * @return the String path this object was initialized with. + */ + public String getPath() { + return path; + } + + + /** + * This method will search the path and return the node that is found. + * + * @param path the path to search for. + * @param rootNode the root node where the path search should start from. + * @return a DataNode corresponding to the configured path. + */ + public static DataNode search(String path, DataNode rootNode) { + DataNodePath pather = new DataNodePath(path); + return pather.search(rootNode); + } + + /** + * This method will execute the path and return the node that is found. + * + * @param rootNode the root node where the path search should start from. + * @return a DataNode corresponding to the configured path. + */ + public DataNode search(DataNode rootNode) { + DataNode node = rootNode; + + for (PathEntity entity : pathEntityList) { + node = entity.getNextNode(node); + + if (node == null) break; + } + return node; + } + + // ****************************************** + // Path Entities + // ****************************************** + + private interface PathEntity { + /** + * @param rootNode the parent node the next node should be fetched from. + * @return the next node based on the entity logic. Null if the next node is unavailable. + */ + DataNode getNextNode(DataNode rootNode); + } + + /** + * This entity returns the child by key name. + */ + private static class NamedChildPathEntity implements PathEntity { + private String childName; + + public NamedChildPathEntity(String childName) { + this.childName = childName; + } + + @Override + public DataNode getNextNode(DataNode rootNode) { + if (rootNode.isMap()) + return rootNode.get(childName); + return null; + } + } + + /** + * This entity returns the child by key name. + */ + private static class IndexChildPathEntity implements PathEntity { + private int index; + + public IndexChildPathEntity(int index) { + this.index = index; + } + + @Override + public DataNode getNextNode(DataNode rootNode) { + if (rootNode.isList()) + return rootNode.get(index); + return null; + } + } + + /** + * This entity returns the child by key name. + */ + private static class AllChildPathEntity implements PathEntity { + @Override + public DataNode getNextNode(DataNode rootNode) { + if (rootNode.isList() || rootNode.isMap()) + return rootNode; + return null; + } + } +} diff --git a/test/zutil/parser/DataNodePathTest.java b/test/zutil/parser/DataNodePathTest.java new file mode 100644 index 0000000..dcef511 --- /dev/null +++ b/test/zutil/parser/DataNodePathTest.java @@ -0,0 +1,59 @@ +package zutil.parser; + +import org.junit.Test; +import zutil.parser.json.JSONParser; +import zutil.parser.json.JSONWriter; + +import static org.junit.Assert.*; + +public class DataNodePathTest { + + @Test + public void invalidPath() { + assertThrows(IllegalArgumentException.class, () -> DataNodePath.search("", new DataNode(DataNode.DataType.Map))); + assertThrows(IllegalArgumentException.class, () -> DataNodePath.search("aa", new DataNode(DataNode.DataType.Map))); + assertThrows(IllegalArgumentException.class, () -> DataNodePath.search("11", new DataNode(DataNode.DataType.Map))); + } + + @Test + public void mapPath() { + String json = "{child1: 'test', child2: {child3: 'banana'}}"; + DataNode rootNode = JSONParser.read(json); + + assertEquals("test", DataNodePath.search("$.child1", rootNode).getString()); + assertEquals("banana", DataNodePath.search("$.child2.child3", rootNode).getString()); + + assertEquals("test", DataNodePath.search("$['child1']", rootNode).getString()); + assertEquals("banana", DataNodePath.search("$['child2']['child3']", rootNode).getString()); + } + + @Test + public void arrayPath() { + String json = "{child1: ['test', {child2: 'banana'}]}"; + DataNode rootNode = JSONParser.read(json); + + assertEquals("test", DataNodePath.search("$.child1[0]", rootNode).getString()); + assertEquals("banana", DataNodePath.search("$.child1[1].child2", rootNode).getString()); + } + + @Test + public void starPath() { + String json = "{}"; + DataNode rootNode = JSONParser.read(json); + + assertNull(DataNodePath.search("$.child1.*", rootNode)); + assertNull(DataNodePath.search("$.child1[*]", rootNode)); + + json = "{child1: []}"; + rootNode = JSONParser.read(json); + + assertEquals("[]", JSONWriter.toString(DataNodePath.search("$.child1.*", rootNode))); + assertEquals("[]", JSONWriter.toString(DataNodePath.search("$.child1[*]", rootNode))); + + json = "{child1: ['test1', 'test2']}"; + rootNode = JSONParser.read(json); + + assertEquals("[\"test1\", \"test2\"]", JSONWriter.toString(DataNodePath.search("$.child1.*", rootNode))); + assertEquals("[\"test1\", \"test2\"]", JSONWriter.toString(DataNodePath.search("$.child1[*]", rootNode))); + } +} \ No newline at end of file