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):
+ *
+ * | Operator | Description |
+ * $ | 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