Implemented DataNodePath class so DataNodes can be searched by path
This commit is contained in:
parent
404cfa00ae
commit
942e26bb53
2 changed files with 257 additions and 0 deletions
198
src/zutil/parser/DataNodePath.java
Normal file
198
src/zutil/parser/DataNodePath.java
Normal 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>.<name></code></td><td>Dot-notated child</td></tr>
|
||||||
|
* <tr><td><code>['<name>']</code></td><td>Bracket-notated child</td></tr>
|
||||||
|
* <tr><td><code>[<number>]</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>['<name>' , '<name>']</code></td><td>Bracket-notated children</td></tr>
|
||||||
|
// TODO: * <tr><td><code>[<number> , <number>]</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>[?(<expression>)]</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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
59
test/zutil/parser/DataNodePathTest.java
Normal file
59
test/zutil/parser/DataNodePathTest.java
Normal 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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue