All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.inversion.json.JSFind Maven / Gradle / Ivy

The newest version!
package io.inversion.json;

import io.inversion.utils.Utils;

import java.util.*;
import java.util.stream.Stream;

public interface JSFind {

    JSNode getJson();

    /**
     * A heroically permissive finder supporting JSON Pointer, JSONPath and
     * a simple 'dot and wildcard' type of system like so:
     * 'propName.childPropName.*.skippedGenerationPropsName.4.fifthArrayNodeChildPropsName.**.recursivelyFoundPropsName'.
     *
     * 

* All forms are internally converted into a 'master' form before processing. This master * simply uses '.' to separate property names and array indexes and uses uses '*' to represent * a single level wildcard and '**' to represent a recursive wildcard. For example: *

    *
  • 'myProp' finds 'myProp' in this node. *
  • 'myProp.childProp' finds 'childProp' on 'myProp' *
  • 'myArrayProp.2.*' finds all properties of the third element of the 'myArrayProp' *
  • '*.myProp' finds 'myProp' in any of the children of this node. *
  • '**.myProp' finds 'myProp' anywhere in my descendents. *
  • '**.myProp.*.value' finds 'value' as a grandchild anywhere under me. *
  • '**.*' returns every element of the document. *
  • '**.5' gets the 6th element of every array. *
  • '**.book[?(@.isbn)]' finds all books with an isbn *
  • '**.[?(@.author = 'Herman Melville')]' finds all book with author 'Herman Melville' *
*

* Arrays indexes are treated just like property names but with integer names. * For example "myObject.4.nextProperty" finds "nextProperty" on the 5th element * in the "myObject" array. * *

* JSON Pointer is the least expressive supported form and uses '/' characters to separate properties. * To support JSON Pointer, we simply replace all '/' characters for "." characters before * processing. * *

* JSON Path is more like XML XPath but uses '.' instead of '/' to separate properties. * Technically JSON Path statements are supposed to start with '$.' but that is optional here. * The best part about JSON Path is the query filters that let you conditionally select * elements. *

* Below is the implementation status of various JSON Path features: *

    *
  • SUPPORTED $.store.book[*].author //the authors of all books in the store *
  • SUPPORTED $..author //all authors *
  • SUPPORTED $.store..price //the prices of all books *
  • SUPPORTED $..book[2] //the third book *
  • SUPPORTED $..book[?(@.price@lt;10)] //all books priced @lt; 10 *
  • SUPPORTED $..[?(@.price@lt;10)] //find any node with a price property *
  • SUPPORTED $..[?(@.*.price@lt;10)] //find the parent of any node with a price property *
  • SUPPORTED $..book[?(@.author = 'Herman Melville')] //all books where 'Herman Melville' is the author *
  • SUPPORTED $..* //all members of JSON structure. *
  • SUPPORTED $..book[(@.length-1)] //the last book in order *
  • SUPPORTED $..book[-1:] //the last book in order *
  • SUPPORTED $..book[0,1] //the first two books *
  • SUPPORTED $..book[:2] //the first two books *
  • SUPPORTED $..book[?(@.isbn)] //find all books with an isbn property *
  • SUPPORTED $..[?(@.isbn)] //find any node with an isbn property *
  • SUPPORTED $..[?(@.*.isbn)] //find the parent of any node with an isbn property *
  • SUPPORTED $..[?(@.*.*.isbn)] //find the grandparent of any node with an isbn property * * *
*

* The JSON Path following boolean comparison operators are supported: *

    *
  • = *
  • @gt; *
  • @lt; *
  • @gt;= *
  • @lt;= *
  • != *
* *

* JsonPath bracket-notation such as "$['store']['book'][0]['title']" * is currently not supported. * * @param qty the maximum number of results * @param pathExpressions defines the properties to find * @return an array of found values * @see JSON Pointer * @see JSON Path * @see JSON Path */ default JSList findAll(int qty, String... pathExpressions) { JSList found = new JSList(); for(int i =0; pathExpressions != null && i> visited) { JSONPathTokenizer tok = new JSONPathTokenizer(// "['\"", //openQuoteStr "]'\"", //closeQuoteStr "]", //breakIncludedChars ".", //breakExcludedChars "", //unquotedIgnoredChars ". \t", //leadingIgnoredChars pathExpression //chars ); List path = tok.asList(); return findAll0(path, qty, collected, visited); } default List findAll0(List path, int qty, List collected, HashMap> visited) { JSNode json = getJson(); //-- infinite recursion protection //-- you can visit a path more than once trying different parts of the search path //-- but you can only visit a node once for any given permutation of the path. String pathStr = path.toString(); Set old = visited.get(pathStr); if (old == null) { old = new HashSet(); visited.put(pathStr, old); } if (old.contains(this)) return collected; old.add(this); //-- end infinite recursion protection if (qty > 1 && collected.size() >= qty) return collected; String nextSegment = path.get(0); if ("*".equals(nextSegment)) { if (path.size() == 1) { Collection values = json.values(); for (Object value : values) { if (!collected.contains(value) && (qty < 1 || collected.size() < qty)) collected.add(value); } } else { List nextPath = path.subList(1, path.size()); for (Object value : json.values()) { if (value instanceof JSNode) { ((JSNode) value).findAll0(nextPath, qty, collected, visited); } } } } else if ("**".equals(nextSegment)) { if (path.size() != 1) { List nextPath = path.subList(1, path.size()); this.findAll0(nextPath, qty, collected, visited); for (Object value : json.values()) { if (value instanceof JSNode) { ((JSNode) value).findAll0(path, qty, collected, visited); } } } } //else if (this instanceof JSList && nextSegment.startsWith("[") && nextSegment.endsWith("]")){ else if (nextSegment.startsWith("[") && nextSegment.endsWith("]")) { //this is a JSONPath filter that is not just an array index String expr = nextSegment.substring(1, nextSegment.length() - 1).trim(); if (expr.startsWith("?(") && expr.endsWith(")")) { JSONPathTokenizer tokenizer = new JSONPathTokenizer(// "'\"", //openQuoteStr "'\"", //closeQuoteStr "?=<>!", //breakIncludedChars...breakAfter "]=<>! ", //breakExcludedChars...breakBefore "[()", //unquotedIgnoredChars "]. \t", //leadingIgnoredChars expr); String token; String func = null; String subpath = null; String op = null; String value = null; //-- Choices after tokenization //-- $..book[2] -> 2 //-- $..book[author] -> author //-- $..book[(@.length-1)] -> @_length-1 //-- $..book[-1:] -> -1: //-- $..book[0,1] -> 0,1 //-- $..book[:2] -> :2 //-- $..book[?(@.isbn)] -> ? @_isbn //-- $..book[?(@.price<10)] -> ? while ((token = tokenizer.next()) != null) { if (token.equals("?")) { func = "?"; continue; } if (token.startsWith("@_")) { subpath = token.substring(2); } else if (Utils.in(token, "=", ">", "<", "!")) { if (op == null) op = token; else op += token; } else if (subpath != null && op != null && value == null) { value = token; if (json.isList()) { for (Object child : json.values()) { if (child instanceof JSNode) { List found = ((JSNode) child).findAll0(subpath, -1, new ArrayList(), visited); for (Object val : found) { if (eval(val, op, value)) { if (!collected.contains(child) && (qty < 1 || collected.size() < qty)) collected.add(child); } } } } } else { List found = findAll0(subpath, -1, new ArrayList(), visited); for (Object val : found) { if (eval(val, op, value)) { if (!collected.contains(this) && (qty < 1 || collected.size() < qty)) { collected.add(this); break; } } } } func = null; subpath = null; op = null; value = null; } } //$..book[?(@.isbn)] -- checks for the existence of a property if ("?".equals(func) && subpath != null) { if (op != null || value != null) { //unparseable...do nothing } if (json.isList()) { for (Object child : json.values()) { if (child instanceof JSNode) { List found = ((JSNode) child).findAll0(subpath, -1, new ArrayList(), visited); for (Object val : found) { if (!collected.contains(child) && (qty < 1 || collected.size() < qty)) collected.add(child); } } } } else { List found = findAll0(subpath, -1, new ArrayList(), visited); if (found.size() > 0) { if (!collected.contains(this) && (qty < 1 || collected.size() < qty)) collected.add(this); } } } } else { //-- $..book[(@.length-1)] -> @_length-1 //-- $..book[-1:] -> -1: //-- $..book[0,1] -> 0,1 //-- $..book[:2] -> :2 if (json.isList()) { int length = ((JSList) this).size(); List found = new ArrayList(); if (expr.startsWith("(@_length-")) { int index = Integer.parseInt(expr.substring(expr.indexOf("-") + 1, expr.length() - 1).trim()); if (length - index > 0) { found.add(json.get(length - index)); } } else if (expr.startsWith(":")) { int count = Integer.parseInt(expr.substring(1).trim()); for (int i = 0; i < length && i < count; i++) { found.add(json.get(count)); } } else if (expr.endsWith(":")) { int idx = Integer.parseInt(expr.substring(0, expr.length() - 1).trim()) * -1; if (idx <= length) found.add(json.get(length - idx)); } else { try { int start = Integer.parseInt(expr.substring(0, expr.indexOf(":")).trim()); int end = Integer.parseInt(expr.substring(expr.indexOf(":") + 1).trim()); for (int i = start; i <= end && i < length; i++) { found.add(json.get(i)); } } catch (Exception ex) { System.out.println(expr); ex.printStackTrace(); } } if (found.size() > 0) { if (path.size() > 1) { //TODO: this is a dead assignment...need a test case here List nextPath = path.subList(1, path.size()); } else { collected.addAll(found); } } } } } else { Object found = json.get(nextSegment); if (found != null) { if (path.size() == 1) { if (!collected.contains(found) && (qty < 1 || collected.size() < qty)) collected.add(found); } else if (found instanceof JSNode) { ((JSNode) found).findAll0(path.subList(1, path.size()), qty, collected, visited); } } } return collected; } /** * Convenience overloading of {@link #findAll(int, String...)} that returns the first item found * * @param pathExpression specifies the properties to find * @return the first item found at pathExpression * @see #findAll(int, String...) */ default Object find(String pathExpression) { JSList found = findAll(1, pathExpression); if (found.size() > 0) return found.get(0); return null; } /** * Convenience overloading of {@link #find(String)} * * @param pathExpression specifies the nodes to find * @return the first value found at pathExpression cast as a JSMap if exists else null * @throws ClassCastException if the object found is not a JSNode * @see #find(String) */ default JSMap findMap(String pathExpression) { return (JSMap) find(pathExpression); } /** * Convenience overloading of {@link #find(String)} * * @param pathExpression specifies the nodes to find * @return the first value found at pathExpression cast as a JSNode if exists else null * @throws ClassCastException if the object found is not a JSNode * @see #find(String) */ default JSNode findNode(String pathExpression) { return (JSNode) find(pathExpression); } /** * Convenience overloading of {@link #find(String)} * * @param pathExpression specifies the properties to find * @return the first value found at pathExpression cast as a JSList if exists else null * @throws ClassCastException if the object found is not a JSList * @see #find(String) */ default JSList findList(String pathExpression) { return (JSList) find(pathExpression); } /** * Convenience overloading of {@link #find(String)} * * @param pathExpression specifies the properties to find * @return the first value found at pathExpression stringified if exists else null * @see #find(String) */ default String findString(String pathExpression) { Object found = find(pathExpression); if (found != null) return found.toString(); return null; } /** * Convenience overloading of {@link #find(String)} * * @param pathExpression specifies the properties to find * @return the first value found at pathExpression stringified and parsed as an int if exists else -1 * @see #find(String) */ default int findInt(String pathExpression) { Object found = find(pathExpression); if (found != null) return Utils.atoi(found); return -1; } /** * Convenience overloading of {@link #find(String)} * * @param pathExpression specifies the properties to find * @return the first value found at pathExpression stringified and parsed as a long if exists else -1 * @see #find(String) */ default long findLong(String pathExpression) { Object found = find(pathExpression); if (found != null) return Utils.atol(found); return -1; } /** * Convenience overloading of {@link #find(String)} * * @param pathExpression specifies the properties to find * @return the first value found at pathExpression stringified and parsed as a double if exists else -1 * @see #find(String) */ default double findDouble(String pathExpression) { Object found = find(pathExpression); if (found != null) return Utils.atod(found); return -1; } /** * Convenience overloading of {@link #find(String)} * * @param pathExpression specifies the properties to find * @return the first value found at pathExpression stringified and parsed as a boolean if exists else false * @see #find(String) */ default boolean findBoolean(String pathExpression) { Object found = find(pathExpression); if (found != null) return Utils.atob(found); return false; } /** * Convenience overloading of {@link #findAll(int, String...)} * * @param pathExpressions specifies the properties to find * @return all items found for pathExpression * @see #findAll(int, String...) */ default JSList findAll(String... pathExpressions) { return findAll(-1, pathExpressions); } /** * Convenience overloading of {@link #findAll(int, String...)} * * @param pathExpressions specifies the properties to find * @return all items found for pathExpression cast as a List * @see #findAll(int, String...) */ default List findMaps(String... pathExpressions) { return (List)findAll(pathExpressions); } default List findLists(String... pathExpressions) { return (List)findAll(pathExpressions); } default Stream streamAll() { List all = findAll("**.*").asList(); all.add(this); return all.stream(); } static boolean eval(Object var, String op, Object value) { value = Utils.dequote(value.toString()); if (var instanceof Number) { try { value = Double.parseDouble(value.toString()); } catch (Exception ex) { //ok, value was not a number...ignore } } if (var instanceof Boolean) { try { value = Boolean.parseBoolean(value.toString()); } catch (Exception ex) { //ok, value was not a boolean...ignore } } int comp = ((Comparable) var).compareTo(value); switch (op) { case "=": return comp == 0; case ">": return comp > 0; case ">=": return comp >= 0; case "<": return comp < 0; case "<=": return comp <= 0; case "!=": return comp != 0; default: throw new UnsupportedOperationException("Unknown operator '" + op + "'"); } } /** * Simply replaces "/" with "." *

* Slashes in property names (seriously a stupid idea anyway) which is supported * by JSON Pointer is not supported. * * @param jsonPointer a slash based path expression * @return a dot based path expression */ static String fromJsonPointer(String jsonPointer) { if (jsonPointer.charAt(0) == '#') { jsonPointer = jsonPointer.substring(1); if (jsonPointer.charAt(0) == '/') jsonPointer = jsonPointer.substring(1); } return jsonPointer.replace('/', '.'); } /** * Converts a proper json path statement into its "relaxed dotted wildcard" form * so that it is easier to parse. */ static String fromJsonPath(String jsonPath) { if (jsonPath.charAt(0) == '#') { jsonPath = jsonPath.substring(1); } if (jsonPath.charAt(0) == '$') jsonPath = jsonPath.substring(1); jsonPath = jsonPath.replace("@.", "@_"); //from jsonpath spec..switching to "_" to make parsing easier jsonPath = jsonPath.replaceAll("([a-zA-Z])\\[", "$1.["); //from json path spec array[index] converted to array.[index]. to support array.index.value legacy format. jsonPath = jsonPath.replace("..", "**."); //translate from jsonpath format jsonPath = jsonPath.replaceAll("([a-zA-Z])[*]", "$1.*"); //translate from jsonpath format jsonPath = jsonPath.replaceAll("([a-zA-Z])\\[([0-9]*)\\]", "$1.$2"); // x[1] to x.1 jsonPath = jsonPath.replaceAll("([a-zA-Z])\\[([0-9]*)\\]", "$1.$2"); // x[1] to x.1 jsonPath = jsonPath.replaceAll("\\.\\[([0-9]*)\\]", ".$1"); //translate .[1]. to .1. */ jsonPath = jsonPath.replaceAll("\\[([0-9]*)\\]", "$1"); // [123] to 123 ...catches a root array jsonPath = jsonPath.replace("[*]", "*"); //System.out.println(pathStr); return jsonPath; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy