Implemented DataNodePath class so DataNodes can be searched by path

This commit is contained in:
Ziver Koc 2023-03-27 01:32:40 +02:00
parent 404cfa00ae
commit 942e26bb53
2 changed files with 257 additions and 0 deletions

View file

@ -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.
* <br>
* Operators (From JSONPath spec):
* <table>
* <tr><th>Operator</th><th>Description</th></tr>
* <tr><td><code>$</code></td><td>The root element to query. This starts all path expressions.</td></tr>
* <tr><td><code>*</code></td><td>Wildcard. Available anywhere a name or numeric are required.</td></tr>
* <tr><td><code>.&lt;name&gt;</code></td><td>Dot-notated child</td></tr>
* <tr><td><code>['&lt;name&gt;']</code></td><td>Bracket-notated child</td></tr>
* <tr><td><code>[&lt;number&gt;]</code></td><td>Array index</td></tr>
* </table>
*
* Examples:
* <table>
* <tr><td> <strong>JSONPath</strong> </td><td> <strong>Result</strong> </td></tr>
* <tr><td class="lft"><code>$.store.book[*].author</code> </td><td class="lft">a list of all authors for the books in the store </td></tr>
* <tr><td class="lft"><code>$.store.*</code> </td><td class="lft">all things in store, which are some books and a red bicycle. </td></tr>
* </table>
*
* @see <a href="https://github.com/json-path/JsonPath">JSONPath Reference</a>
*/
// TODO: * <tr><td><code>['&lt;name&gt;' , '&lt;name&gt;']</code></td><td>Bracket-notated children</td></tr>
// TODO: * <tr><td><code>[&lt;number&gt; , &lt;number&gt;]</code></td><td>Array indexes</td></tr>
// TODO: * <tr><td><code>@</code></td><td>The current node being processed by a filter predicate.</td></tr>
// TODO: * <tr><td><code>..</code></td><td>Deep scan. Available anywhere a name is required.</td></tr>
// TODO: * <tr><td><code>[start:end]</code></td><td>Array slice operator</td></tr>
// TODO: * <tr><td><code>[?(&lt;expression&gt;)]</code></td><td>Filter expression. Expression must evaluate to a boolean value.</td></tr>
public class DataNodePath {
private final String path;
private List<PathEntity> pathEntityList;
public DataNodePath(String path) {
this.path = path;
pathEntityList = parsePath(path);
}
private static List<PathEntity> parsePath(String path) {
ArrayList<PathEntity> 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;
}
}
}

View file

@ -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)));
}
}