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

com.aerospike.documentapi.JsonPathParser Maven / Gradle / Ivy

package com.aerospike.documentapi;

import com.aerospike.client.cdt.*;
import com.aerospike.documentapi.pathparts.PathPart;
import com.aerospike.documentapi.pathparts.ListPathPart;
import com.aerospike.documentapi.pathparts.MapPathPart;

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Utility class for parsing JSON paths
 * Not useful outside of this package, hence package visibility
 */
public class JsonPathParser {
    static final String JSON_PATH_SEPARATOR = ".";
    static final String DOCUMENT_ROOT_TOKEN = "$";

    // Paths should match this pattern i.e. key[index1][index2]...
    static final Pattern PATH_PATTERN = Pattern.compile("^([^\\[^\\]]*)(\\[(\\d+)\\])*$");
    // This pattern to extract index1,index2 ...
    static final Pattern INDEX_PATTERN = Pattern.compile("(\\[(\\d+)\\])");

    private final List jsonPathQueryIndications = new ArrayList<>(Arrays.asList("[*]", "..", "[?"));

    // Store our representation of the individual path parts
    JsonPathObject jsonPathObject = new JsonPathObject();

    JsonPathParser() {}

    /**
     * Turn json path as string into PathPart recommendation
     * @param jsonString a given JSON as String
     * @return List
     * @throws JsonParseException a JsonParseException will be thrown in case of an error.
     */
    JsonPathObject parse(String jsonString) throws JsonParseException {
        if (jsonString.charAt(0) != '$') {
            throw new JsonPrefixException(jsonString);
        }
        StringTokenizer tokenizer = new StringTokenizer(jsonString, JSON_PATH_SEPARATOR);
        if (!tokenizer.nextToken().equals(DOCUMENT_ROOT_TOKEN)) {
            throw new JsonPrefixException(jsonString);
        }

        Integer index = getFirstIndexOfAQueryIndication(jsonString);
        // Query is required
        if (index != null) {
            /*
                Split the jsonString into 2 parts:
                    1. A string of path parts before a query operator to fetch the smallest Json possible from Aerospike.
                    2. A string of the remaining JsonPath to later use for executing a JsonPath query on the fetched Json from Aerospike.

                For example:
                $.store.book[*].author
                store.book will be fetched from Aerospike and a JsonPath book[*].author query will be executed on the fetched results from Aerospike (key = book, value = nested Json).
            */
            jsonPathObject.setRequiresJsonPathQuery(true);
            String aerospikePathPartsString = jsonString.substring(0, index);
            String jsonPathPathPartsString = jsonString.substring(index);
            jsonPathObject.setJsonPathSecondStepQuery(jsonPathPathPartsString);
            tokenizer = new StringTokenizer(aerospikePathPartsString, JSON_PATH_SEPARATOR);
            if (!tokenizer.nextToken().equals(DOCUMENT_ROOT_TOKEN)) {
                throw new JsonPrefixException(jsonString);
            }
        }

        while (tokenizer.hasMoreTokens()) {
            parsePathPart(tokenizer.nextToken());
        }

        return jsonPathObject;
    }

    /**
     * Utility internal method to process the individual path parts
     * Appends the found path parts to the list of path parts already found
     * Expected form of pathPart is key[index1][index2]
     * @param pathPart pathPart to Parse.
     * @throws JsonParseException a JsonParseException will be thrown in case of an error.
     */
    private void parsePathPart(String pathPart) throws JsonParseException {
        Matcher keyMatcher = PATH_PATTERN.matcher(pathPart);
        if ((!pathPart.contains("[")) && (!pathPart.contains("]"))) {
            // ignore * wildcard after a dot, its the same as ending with a .path
            if (!pathPart.equals("*")) {
                jsonPathObject.addPathPart(new MapPathPart(pathPart));
            }
        } else if (keyMatcher.find()) {
            String key = keyMatcher.group(1);
            jsonPathObject.addPathPart(new MapPathPart(key));
            Matcher indexMatcher = INDEX_PATTERN.matcher(pathPart);

            while (indexMatcher.find()) {
                jsonPathObject.addPathPart(new ListPathPart(Integer.parseInt(indexMatcher.group(2))));
            }
        } else {
            throw new JsonPathException(pathPart);
        }
    }

    public static PathPart extractLastPathPart(List pathParts) {
        return pathParts.get(pathParts.size() - 1);
    }

    public static PathPart extractLastPathPartAndModifyList(List pathParts) {
        return pathParts.remove(pathParts.size() - 1);
    }

    /**
     * Given a list of path parts, convert this to the list of contexts you would need
     * to retrieve the JSON path represented by the list of path parts
     * @param pathParts pathParts list to convert.
     * @return An array of contexts (CTXs).
     */
    public static CTX[] pathPartsToContextsArray(List pathParts) {
        List contextList = new Vector<>();
        for (PathPart pathPart : pathParts) {
            contextList.add(pathPart.toAerospikeContext());
        }
        return contextList.toArray(new CTX[contextList.size()]);
    }

    private Integer getFirstIndexOfAQueryIndication(String jsonPath) {
        return jsonPathQueryIndications.stream()
                .map(jsonPath::indexOf)
                .filter(index -> index > 0)
                .min(Integer::compare)
                .orElse(null); // in case there no match for a query indication
    }

    /**
     * Different types of json path exception
     */
    public static abstract class JsonParseException extends Exception {
        String jsonString;
        JsonParseException(String s) {
            jsonString = s;
        }
    }

    public static class JsonPrefixException extends JsonParseException {
        JsonPrefixException(String s) {
            super(s);
        }

        public String toString() {
            return jsonString + " should start with a $";
        }
    }

    public static class JsonPathException extends JsonParseException {
        JsonPathException(String s) {
            super(s);
        }

        public String toString() {
            return jsonString + " does not match key[number] format";
        }
    }

    public static class ListException extends JsonParseException {
        ListException(String s) {super(s);}

        public String toString() {return "You can't append to a document root";}
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy