com.redhat.lightblue.util.JsonDoc.java Source code

Java tutorial

Introduction

Here is the source code for com.redhat.lightblue.util.JsonDoc.java

Source

/*
 2013 Red Hat, Inc. and/or its affiliates.
    
 This file is part of lightblue.
    
 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.
    
 program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.
    
 You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
/*
 Copyright 2013 Red Hat, Inc. and/or its affiliates.
    
 This file is part of lightblue.
    
 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.
    
 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.
    
 You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.redhat.lightblue.util;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * Wrapper class around JSOn documents
 */
public class JsonDoc implements Serializable {

    private static final long serialVersionUID = 1l;

    private final transient JsonNode docRoot;

    private static final Resolver DEFAULT_RESOLVER = new Resolver();
    private static final Resolver CREATING_RESOLVER = new CreatingResolver();

    private static final class Iteration {
        private Iterator<JsonNode> iterator;
        private JsonNode currentNode;
        private int index;

        boolean next() {
            if (iterator.hasNext()) {
                currentNode = iterator.next();
                index++;
                return true;
            } else {
                return false;
            }
        }

        /**
         * @return the currentNode
         */
        public JsonNode getCurrentNode() {
            return currentNode;
        }

        /**
         * @return the index
         */
        public int getIndex() {
            return index;
        }
    }

    /**
     * Internal class that overrides the behavior for '*' processing during path
     * resolution
     */
    private static final class CursorResolver extends Resolver {
        private Iteration[] iterators;

        @Override
        protected JsonNode handleAny(Path p, JsonNode node, int level) {
            JsonNode output = null;
            if (iterators == null) {
                int n = p.numSegments();
                iterators = new Iteration[n];
            }
            if (node instanceof ArrayNode) {
                Iteration itr = iterators[level];
                if (itr == null) {
                    itr = new Iteration();
                    iterators[level] = itr;
                }
                itr.index = -1;
                itr.iterator = ((ArrayNode) node).elements();
                if (itr.next()) {
                    output = itr.getCurrentNode();
                }
            }
            return output;
        }
    }

    /**
     * Internal class containing the algorithm for path resolution starting from
     * a node and path level. Handling of '*' is overridable, by default, throws
     * an exception
     */
    private static class Resolver {
        public JsonNode resolve(Path p, final JsonNode node, int level) {
            JsonNode output = node;

            int n = p.numSegments();
            for (int l = level; l < n; l++) {
                String name = p.head(l);
                JsonNode newOutput;
                if (name.equals(Path.ANY)) {
                    newOutput = handleAny(p, output, l);
                } else if (name.equals(Path.THIS)) {
                    continue;
                } else if (name.equals(Path.PARENT)) {
                    output = node.findParent(p.head(findNextNonRealtiveSegment(p, l)));
                    continue;
                } else if (output instanceof ArrayNode) {
                    int index = Integer.valueOf(name);
                    if (index < 0) {
                        newOutput = ((ArrayNode) output).get(((ArrayNode) output).size() + index);
                    } else {
                        newOutput = ((ArrayNode) output).get(index);
                    }
                } else if (output instanceof ObjectNode) {
                    newOutput = output.get(name);
                } else {
                    newOutput = null;
                }
                if (newOutput == null) {
                    newOutput = handleNullChild(output, p, l);
                }

                output = newOutput;

                if (output == null) {
                    break;
                }

            }
            return output;
        }

        protected JsonNode handleNullChild(JsonNode parent, Path p, int level) {
            return null;
        }

        protected JsonNode handleAny(Path p, JsonNode node, int level) {
            throw new IllegalArgumentException(p.toString());
        }

    }

    private static int findNextNonRealtiveSegment(Path path, int currentPosition) {
        int indexOfSegment = 0;

        for (int i = currentPosition; i < path.numSegments(); i++) {
            String segment = path.head(i);
            if (!Path.THIS.equals(segment) && !Path.PARENT.equals(segment)) {
                indexOfSegment = i;
                break;
            }
        }

        return indexOfSegment;
    }

    /**
     * Given a path p=x_1.x_2...x_n, it creates all the intermediate nodes
     * x_1...x_(n-1), but not the node x_n. However, the node x_(n-1) is created
     * correctly depending on the x_n: if x_n is an index, x_(n-1) is an
     * ArrayNode, otherwise x_(n-1) is an object node.
     */
    private static class CreatingResolver extends Resolver {
        @Override
        protected JsonNode handleNullChild(JsonNode parent, Path p, int level) {
            // This function is called because 'parent' does not have
            // a child with name p[level]. So, we will add that
            // child. If p[level+1] is an index, then p[level] must be
            // an array, otherwise, p[level] must be an object node. 

            // First check if p is long enough. There must be one more
            // after level
            if (p.numSegments() <= level + 1) {
                return null;
            }
            // Now determine the child type
            boolean childIsArray = p.isIndex(level + 1);
            if (parent instanceof ArrayNode) {
                ArrayNode arr = (ArrayNode) parent;
                int index = p.getIndex(level);
                // Extend the array to include this index
                int size = arr.size();
                while (size < index) {
                    arr.addNull();
                    size++;
                }
                // Now add the new node. 
                if (childIsArray) {
                    return arr.addArray();
                } else {
                    return arr.addObject();
                }
            } else {
                if (childIsArray) {
                    return ((ObjectNode) parent).putArray(p.head(level));
                } else {
                    return ((ObjectNode) parent).putObject(p.head(level));
                }
            }
        }
    }

    /**
     * A cursor that iterates through all elements of a document that matches
     * the path. If the path has no '*', the initialization code finds the node
     * if any, and the iteration runs only once. If the path contains '*',
     * iterators for all arrays corresponding to '*' are kept in CursorResolver.
     *
     * The algorithms is somewhat complicated because not all elements of the
     * array are guaranteed to have the same structure. For instance, a path of
     * the form x.*.y, when evaluated on a document of the form:
     *
     * <pre>
     *   x : [
     *        { a:1 },
     *        { y:2 },
     *        { y:3 }
     *    ]
     * </pre>
     *
     * the iterator starts iterating from the second element of the array x,
     * because x.0.y does not exist.
     */
    private class PathCursor implements KeyValueCursor<Path, JsonNode> {

        private final Path path;
        private final MutablePath mpath;
        private final CursorResolver resolver = new CursorResolver();

        private JsonNode nextNode;
        private boolean ended = false;
        private boolean nextFound = false;
        private JsonNode currentNode;
        private Path currentPath;

        public PathCursor(Path p) {
            path = p;
            nextNode = resolver.resolve(path, docRoot, 0);
            if (nextNode != null) {
                nextFound = true;
            }
            if (resolver.iterators == null) {
                ended = true;
                mpath = null;
            } else {
                mpath = new MutablePath(path);
            }
        }

        @Override
        public Path getCurrentKey() {
            return currentPath;
        }

        @Override
        public JsonNode getCurrentValue() {
            return currentNode;
        }

        @Override
        public boolean hasNext() {
            if (!nextFound && !ended) {
                nextNode = seekNext();
            }
            return nextFound;
        }

        @Override
        public void next() {
            if (!nextFound && !ended) {
                nextNode = seekNext();
            }
            if (nextFound) {
                if (resolver.iterators != null) {
                    int i = 0;
                    for (Iteration x : resolver.iterators) {
                        if (x != null) {
                            mpath.set(i, x.getIndex());
                        }
                        i++;
                    }
                    currentPath = mpath.immutableCopy();
                } else {
                    currentPath = path;
                }
                currentNode = nextNode;
            } else {
                currentPath = null;
                currentNode = null;
            }
            nextFound = false;
            nextNode = null;
        }

        private JsonNode seekNext() {
            nextFound = false;
            JsonNode node = null;
            if (resolver.iterators != null) {
                int n = resolver.iterators.length;
                int level = n - 1;
                boolean done = false;
                do {
                    Iteration itr = resolver.iterators[level];
                    if (itr != null && itr.next()) {
                        node = resolver.resolve(path, itr.getCurrentNode(), level + 1);
                        if (node != null) {
                            nextFound = true;
                            done = true;
                        } else {
                            continue;
                        }
                    } else {
                        level--;
                        if (level < 0) {
                            done = true;
                            ended = true;
                        }
                    }
                } while (!done);
            }
            return node;
        }
    }

    /**
     * Creates a JsonDoc with the given root
     */
    public JsonDoc(JsonNode doc) {
        this.docRoot = doc;
    }

    /**
     * Returns the root node
     */
    public JsonNode getRoot() {
        return docRoot;
    }

    /**
     * Returns a cursor that iterates all nodes of the document in a depth-first manner
     */
    public JsonNodeCursor cursor() {
        return cursor(Path.EMPTY);
    }

    /**
     * Returns a cursor that iterates all nodes of the document in a
     * depth first manner, but uses <code>p</code> as a prefix to all
     * the paths during iteration. This method is meant to be used for
     * a JsonDoc rooted at an intermediate node in a Json node tree.
     */
    public JsonNodeCursor cursor(Path p) {
        return cursor(docRoot, p);
    }

    /**
     * Returns a cursor that iterates all the nodes under the given
     * root, where the root is an intermediate node in a Json document
     * accessed by path 'p'. Path can be empty, meaning the 'root' is
     * the real document root.
     */
    public static JsonNodeCursor cursor(JsonNode root, Path p) {
        return new JsonNodeCursor(p, root);
    }

    /**
     * Returns all nodes matching the path. The path can contain *
     *
     * @param p The path
     *
     * Returns a cursor iterating through all nodes of arrays, if any
     */
    public KeyValueCursor<Path, JsonNode> getAllNodes(Path p) {
        return new PathCursor(p);
    }

    /**
     * Returns a node matching a path
     *
     * @param p The path
     *
     * The path cannot contain *.
     *
     * @returns The node, or null if the node cannot be found
     */
    public JsonNode get(Path p) {
        return get(docRoot, p);
    }

    /**
     * Static utility to resolve a path relative to a node
     */
    public static JsonNode get(JsonNode root, Path p) {
        return DEFAULT_RESOLVER.resolve(p, root, 0);
    }

    /**
     * Modifies an existing node value
     *
     * @param p Path to modify
     * @param newValue new value to set. If null, path is removed from the doc.
     * @param createPath If true, creates all intermediate nodes if they don't
     * exist
     *
     * @return Old value
     */
    public JsonNode modify(Path p, JsonNode newValue, boolean createPath) {
        return modify(docRoot, p, newValue, createPath);
    }

    /**
     * Modifies an existing node value
     *
     * @param root The root node
     * @param p Path to modify
     * @param newValue new value to set. If null, path is removed from the doc.
     * @param createPath If true, creates all intermediate nodes if they don't
     * exist
     *
     * @return Old value
     */
    public static JsonNode modify(JsonNode root, Path p, JsonNode newValue, boolean createPath) {
        int n = p.numSegments();
        if (n == 0) {
            throw new IllegalArgumentException(UtilConstants.ERR_CANT_SET_EMPTY_PATH_VALUE);
        }
        Path parent = p.prefix(-1);
        // Parent must be a container node
        JsonNode parentNode = getParentNode(root, parent, createPath, p);
        JsonNode oldValue;
        String last = p.getLast();
        if (parentNode instanceof ObjectNode) {
            oldValue = modifyObjectNode(parentNode, newValue, last, parent);
        } else {
            oldValue = modifyArrayNode((ArrayNode) parentNode, newValue, last, p);
        }
        return oldValue;
    }

    /**
     * Return a list of JsonDoc objects from the given Json node
     *
     * @psram data Json document containing one or more documents
     *
     * The Json document is either an ArrayNode containing Json documents at
     * each element, or an ObjectNode containing only one document.
     */
    public static List<JsonDoc> docList(JsonNode data) {
        ArrayList<JsonDoc> docs = null;
        if (data != null) {
            if (data instanceof ArrayNode) {
                docs = new ArrayList<>(((ArrayNode) data).size());
                for (Iterator<JsonNode> itr = ((ArrayNode) data).elements(); itr.hasNext();) {
                    docs.add(new JsonDoc(itr.next()));
                }
            } else if (data instanceof ObjectNode) {
                docs = new ArrayList<>(1);
                docs.add(new JsonDoc(data));
            }
        }
        return docs;
    }

    /**
     * Combines all Json documents in a list into a single Json document
     *
     * @param docs List of JsonDoc objects
     * @param nodeFactory Json node factory
     *
     * @return If the list has only one document, returns an ObjectNode,
     * otherwise returns an array node containing each document in array
     * elements
     */
    public static JsonNode listToDoc(List<JsonDoc> docs, JsonNodeFactory nodeFactory) {
        if (docs == null) {
            return null;
        } else if (docs.isEmpty()) {
            return nodeFactory.arrayNode();
        } else if (docs.size() == 1) {
            return docs.get(0).getRoot();
        } else {
            ArrayNode node = nodeFactory.arrayNode();
            for (JsonDoc doc : docs) {
                node.add(doc.getRoot());
            }
            return node;
        }
    }

    /**
     * Returns a deep copy of the current document
     */
    public JsonDoc copy() {
        return new JsonDoc(docRoot.deepCopy());
    }

    private static JsonNode getParentNode(JsonNode docRoot, Path parent, boolean createPath, Path p) {
        JsonNode parentNode = DEFAULT_RESOLVER.resolve(parent, docRoot, 0);
        if (parentNode == null && createPath) {
            CREATING_RESOLVER.resolve(p, docRoot, 0);
            parentNode = DEFAULT_RESOLVER.resolve(parent, docRoot, 0);
        }
        if (parentNode != null) {
            if (!parentNode.isContainerNode()) {
                throw new IllegalArgumentException(parent.toString() + UtilConstants.ERR_IS_NOT_A_CONTAINER + p);
            }
        } else {
            throw new IllegalArgumentException(UtilConstants.ERR_PARENT_DOESNT_EXIST + p);
        }
        return parentNode;
    }

    private static JsonNode modifyObjectNode(JsonNode parentNode, JsonNode newValue, String last, Path p) {
        JsonNode oldValue;
        if (Util.isNumber(last)) {
            throw new IllegalArgumentException(UtilConstants.ERR_INVALID_INDEXED_ACCESS + p);
        }
        ObjectNode obj = (ObjectNode) parentNode;
        if (newValue == null) {
            oldValue = obj.get(last);
            obj.remove(last);
        } else {
            oldValue = obj.replace(last, newValue);
        }
        return oldValue;
    }

    private static JsonNode modifyArrayNode(ArrayNode parentNode, JsonNode newValue, String last, Path p) {
        JsonNode oldValue;
        ArrayNode arr = (ArrayNode) parentNode;
        int index;
        try {
            index = Integer.valueOf(last);
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException(UtilConstants.ERR_EXPECTED_ARRAY_INDEX + p);
        }
        int size = arr.size();
        while (size < index) {
            arr.addNull();
            size++;
        }
        if (index < 0) {
            index = size + index;
        }
        if (index < size && newValue != null) {
            oldValue = arr.get(index);
            arr.set(index, newValue);
        } else if (newValue == null) {
            oldValue = arr.get(index);
            arr.remove(index);
        } else {
            oldValue = null;
            arr.add(newValue);
        }
        return oldValue;
    }

    @Override
    public String toString() {
        return docRoot.toString();
    }
}