JsonPointer.java

/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions Copyrighted [year] [name of copyright owner]".
 *
 * Copyright 2011-2016 ForgeRock AS.
 */

package org.forgerock.json;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.NoSuchElementException;

/**
 * Identifies a specific value within a JSON structure. Conforms with
 * <a href="http://tools.ietf.org/html/draft-pbryan-zyp-json-pointer-02">draft-pbryan-zip-json-pointer-02</a>.
 */
public class JsonPointer implements Iterable<String> {

    /** The reference tokens that make-up the JSON pointer. */
    private String[] tokens = new String[0];

    /**
     * Constructs a JSON pointer, identifying the root value of a JSON structure.
     */
    public JsonPointer() {
        // empty tokens represents pointer to root value
    }

    /**
     * Constructs a JSON pointer, identifying the specified pointer value.
     *
     * @param pointer a string containing the JSON pointer of the value to identify.
     * @throws JsonException if the pointer is malformed.
     */
    public JsonPointer(String pointer) {
        String[] split = pointer.split("/", -1);
        int length = split.length;
        ArrayList<String> list = new ArrayList<>(length);
        for (int n = 0; n < length; n++) {
            if (n == 0 && split[n].length() == 0) {
                continue; // leading slash ignored
            } else if (n == length - 1 && split[n].length() == 0) {
                continue; // trailing slash ignored
            } else {
                list.add(decode(split[n]));
            }
        }
        tokens = list.toArray(tokens);
    }

    /**
     * Constructs a JSON pointer from an array of reference tokens.
     *
     * @param tokens an array of string reference tokens.
     */
    public JsonPointer(String... tokens) {
        this.tokens = Arrays.copyOf(tokens, tokens.length);
    }

    /**
     * Constructs a JSON pointer from an iterable collection of reference tokens.
     *
     * @param iterable an iterable collection of reference tokens.
     */
    public JsonPointer(Iterable<String> iterable) {
        ArrayList<String> list = new ArrayList<>();
        for (String element : iterable) {
            list.add(element);
        }
        tokens = list.toArray(tokens);
    }

    /**
     * Constructs a JSON pointer, identifying the specified pointer value.
     *
     * @param pointer a string containing the JSON pointer of the value to identify.
     * @return The new JSON pointer
     * @throws JsonException if the pointer is malformed.
     */
    public static JsonPointer ptr(final String pointer) {
        return new JsonPointer(pointer);
    }

    /**
     * Constructs a JSON pointer from an array of reference tokens.
     *
     * @param tokens an array of string reference tokens.
     * @return The new json pointer
     */
    public static JsonPointer ptr(final String... tokens) {
        return new JsonPointer(tokens);
    }

    /**
     * Constructs a JSON pointer from an iterable collection of reference tokens.
     *
     * @param iterable an iterable collection of reference tokens.
     * @return The new json pointer
     */
    public static JsonPointer ptr(final Iterable<String> iterable) {
        return new JsonPointer(iterable);
    }

    /**
     * Encodes a reference token into a string value suitable to expressing in a JSON
     * pointer string value.
     *
     * @param value the reference token value to be encoded.
     * @return the encode reference token value.
     */
    private String encode(String value) {
        try {
            return new URI(null, null, null, null, value).toASCIIString().substring(1).replaceAll("/", "%2F");
        } catch (URISyntaxException use) { // shouldn't happen
            throw new IllegalStateException(use.getMessage());
        }
    }

    /**
     * Decodes a reference token into a string value that the pointer maintains.
     *
     * @param value the reference token value to decode.
     * @return the decoded reference token value.
     * @throws JsonException if the reference token value is malformed.
     */
    private String decode(String value) {
        try {
            return new URI("#" + value).getFragment();
        } catch (URISyntaxException use) {
            throw new JsonException(use.getMessage());
        }
    }

    /**
     * Returns the number of reference tokens in the pointer.
     *
     * @return the number of reference tokens in the pointer.
     */
    public int size() {
        return tokens.length;
    }

    /**
     * Returns the reference token at the specified position.
     *
     * @param index the index of the reference token to return.
     * @return the reference token at the specified position.
     * @throws IndexOutOfBoundsException if the index is out of range.
     */
    public String get(int index) {
        if (index < 0 || index >= tokens.length) {
            throw new IndexOutOfBoundsException();
        }
        return tokens[index];
    }

    /**
     * Returns a newly allocated array of strings, containing the pointer's reference tokens.
     * No references to the array are maintained by the pointer. Hence, the caller is free to
     * modify it.
     *
     * @return a newly allocated array of strings, containing the pointer's reference tokens.
     */
    public String[] toArray() {
        return Arrays.copyOf(tokens, tokens.length);
    }

    /**
     * Returns a pointer to the parent of the JSON value identified by this JSON pointer,
     * or {@code null} if the pointer has no parent JSON value (i.e. references document root).
     *
     * @return a pointer to the parent of of this JSON pointer. Can be null.
     */
    public JsonPointer parent() {
        JsonPointer parent = null;
        if (this.tokens.length > 0) {
            parent = new JsonPointer();
            parent.tokens = Arrays.copyOf(this.tokens, this.tokens.length - 1);
        }
        return parent;
    }

    /**
     * Returns a pointer containing all but the first reference token contained
     * in this pointer, or {@code /} if this pointer contains less than 2
     * reference tokens.
     * <p>
     * This method yields the following results: <blockquote>
     * <table cellpadding=1 cellspacing=0 summary="Examples illustrating usage of relativePointer">
     * <tr>
     * <th>Input</th>
     * <th>Output</th>
     * </tr>
     * <tr>
     * <td align=left>/</td>
     * <td align=left><tt>/</tt></td>
     * </tr>
     * <tr>
     * <td align=left>/a</td>
     * <td align=left><tt>/</tt></td>
     * </tr>
     * <tr>
     * <td align=left>/a/b</td>
     * <td align=left>/b</td>
     * </tr>
     * <tr>
     * <td align=left>/a/b/c</td>
     * <td align=left>/b/c</td>
     * </tr>
     * </table>
     * </blockquote>
     *
     * @return A pointer containing all but the first reference token contained
     *         in this pointer.
     */
    public JsonPointer relativePointer() {
        return tokens.length > 0 ? relativePointer(tokens.length - 1) : this;
    }

    /**
     * Returns a pointer containing the last {@code sz} reference tokens
     * contained in this pointer.
     * <p>
     * This method yields the following results: <blockquote>
     * <table cellpadding=1 cellspacing=0 summary="Examples illustrating usage of relativePointer">
     * <tr>
     * <th>Input</th>
     * <th>sz</th>
     * <th>Output</th>
     * </tr>
     * <tr>
     * <td align=left>/a/b/c</td>
     * <td align=center>0</td>
     * <td align=left>/</td>
     * </tr>
     * <tr>
     * <td align=left>/a/b/c</td>
     * <td align=center>1</td>
     * <td align=left>/c</td>
     * </tr>
     * <tr>
     * <td align=left>/a/b/c</td>
     * <td align=center>2</td>
     * <td align=left>/b/c</td>
     * </tr>
     * <tr>
     * <td align=left>/a/b/c</td>
     * <td align=center>3</td>
     * <td align=left>/a/b/c</td>
     * </tr>
     * </table>
     * </blockquote>
     *
     * @param sz
     *            The number of trailing reference tokens to retain.
     * @return A pointer containing the last {@code sz} reference tokens
     *         contained in this pointer.
     * @throws IndexOutOfBoundsException
     *             If {@code sz} is negative or greater than {@code size()}.
     */
    public JsonPointer relativePointer(int sz) {
        int length = tokens.length;
        if (sz < 0 || sz > length) {
            throw new IndexOutOfBoundsException();
        } else if (sz == length) {
            return this;
        } else if (sz == 0) {
            return new JsonPointer();
        } else {
            JsonPointer relativePointer = new JsonPointer();
            relativePointer.tokens = Arrays.copyOfRange(tokens, length - sz, length);
            return relativePointer;
        }
    }

    /**
     * Returns the last (leaf) reference token of the JSON pointer, or {@code null} if the
     * pointer contains no reference tokens (i.e. references document root).
     *
     * @return the last (leaf) reference token of the JSON pointer if it exists, {@code null} otherwise
     */
    public String leaf() {
        return tokens.length > 0 ? tokens[tokens.length - 1] : null;
    }

    /**
     * Returns a new JSON pointer, which identifies a specified child member of the
     * object identified by this pointer.
     *
     * @param child the name of the child member to identify.
     * @return the child JSON pointer.
     * @throws NullPointerException if {@code child} is {@code null}.
     */
    public JsonPointer child(String child) {
        if (child == null) {
            throw new NullPointerException();
        }
        JsonPointer pointer = new JsonPointer();
        pointer.tokens = Arrays.copyOf(this.tokens, this.tokens.length + 1);
        pointer.tokens[pointer.tokens.length - 1] = child;
        return pointer;
    }

    /**
     * Returns a new JSON pointer, which identifies a specified child element of the
     * array identified by this pointer.
     *
     * @param child the index of the child element to identify.
     * @return the child JSON pointer.
     * @throws IndexOutOfBoundsException if {@code child} is less than zero.
     */
    public JsonPointer child(int child) {
        if (child < 0) {
            throw new IndexOutOfBoundsException();
        }
        return child(Integer.toString(child));
    }

    /**
     * Returns {@code true} if this pointer identifies the root value of a JSON
     * structure. More specifically, it returns {@code true} if this pointer
     * does not contain any reference tokens (i.e. {@code size() == 0}).
     *
     * @return {@code true} if this pointer identifies the root value of a JSON
     *         structure.
     */
    public boolean isEmpty() {
        return size() == 0;
    }

    /**
     * Returns an iterator over the pointer's reference tokens.
     *
     * @return an iterator over the pointer's reference tokens.
     */
    @Override
    public Iterator<String> iterator() {
        return new Iterator<String>() {
            int cursor = 0;
            @Override
            public boolean hasNext() {
                return cursor < tokens.length;
            }
            @Override
            public String next() {
                if (cursor >= tokens.length) {
                    throw new NoSuchElementException();
                }
                return tokens[cursor++];
            }
            @Override
            public void remove() {
                throw new UnsupportedOperationException();
            }
        };
    }

    /**
     * Returns the JSON pointer string value.
     *
     * @return the JSON pointer string value.
     */
    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder();
        for (String token : tokens) {
            sb.append('/').append(encode(token));
        }
        if (sb.length() == 0) {
            sb.append('/');
        }
        return sb.toString();
    }

    /**
     * Compares the specified object with this pointer for equality. Returns {@code true} if
     * and only if the specified object is also a JSON pointer, both pointers have the same
     * size, and all corresponding pairs of reference tokens in the two pointers are equal.
     *
     * @param o the object to be compared for equality with this pointer.
     * @return {@code true} if the specified object is equal to this pointer.
     */
    @Override
    public boolean equals(Object o) {
        return o instanceof JsonPointer
                && ((JsonPointer) o).size() == size()
                && Arrays.equals(tokens, ((JsonPointer) o).tokens);
    }

    /**
     * Returns the hash code value for this pointer.
     *
     * @return the hash code value for this pointer.
     */
    @Override
    public int hashCode() {
        return Arrays.hashCode(tokens);
    }
}