001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyrighted [year] [name of copyright owner]".
013 *
014 * Copyright 2011-2015 ForgeRock AS.
015 */
016
017package org.forgerock.json;
018
019import java.net.URI;
020import java.net.URISyntaxException;
021import java.util.ArrayList;
022import java.util.Arrays;
023import java.util.Iterator;
024import java.util.NoSuchElementException;
025
026/**
027 * Identifies a specific value within a JSON structure. Conforms with
028 * <a href="http://tools.ietf.org/html/draft-pbryan-zyp-json-pointer-02">draft-pbryan-zip-json-pointer-02</a>.
029 */
030public class JsonPointer implements Iterable<String> {
031
032    /** The reference tokens that make-up the JSON pointer. */
033    private String[] tokens = new String[0];
034
035    /**
036     * Constructs a JSON pointer, identifying the root value of a JSON structure.
037     */
038    public JsonPointer() {
039        // empty tokens represents pointer to root value
040    }
041
042    /**
043     * Constructs a JSON pointer, identifying the specified pointer value.
044     *
045     * @param pointer a string containing the JSON pointer of the value to identify.
046     * @throws JsonException if the pointer is malformed.
047     */
048    public JsonPointer(String pointer) {
049        String[] split = pointer.split("/", -1);
050        int length = split.length;
051        ArrayList<String> list = new ArrayList<>(length);
052        for (int n = 0; n < length; n++) {
053            if (n == 0 && split[n].length() == 0) {
054                continue; // leading slash ignored
055            } else if (n == length - 1 && split[n].length() == 0) {
056                continue; // trailing slash ignored
057            } else {
058                list.add(decode(split[n]));
059            }
060        }
061        tokens = list.toArray(tokens);
062    }
063
064    /**
065     * Constructs a JSON pointer from an array of reference tokens.
066     *
067     * @param tokens an array of string reference tokens.
068     */
069    public JsonPointer(String[] tokens) {
070        this.tokens = Arrays.copyOf(tokens, tokens.length);
071    }
072
073    /**
074     * Constructs a JSON pointer from an iterable collection of reference tokens.
075     *
076     * @param iterable an iterable collection of reference tokens.
077     */
078    public JsonPointer(Iterable<String> iterable) {
079        ArrayList<String> list = new ArrayList<>();
080        for (String element : iterable) {
081            list.add(element);
082        }
083        tokens = list.toArray(tokens);
084    }
085
086    /**
087     * Encodes a reference token into a string value suitable to expressing in a JSON
088     * pointer string value.
089     *
090     * @param value the reference token value to be encoded.
091     * @return the encode reference token value.
092     */
093    private String encode(String value) {
094        try {
095            return new URI(null, null, null, null, value).toASCIIString().substring(1).replaceAll("/", "%2F");
096        } catch (URISyntaxException use) { // shouldn't happen
097            throw new IllegalStateException(use.getMessage());
098        }
099    }
100
101    /**
102     * Decodes a reference token into a string value that the pointer maintains.
103     *
104     * @param value the reference token value to decode.
105     * @return the decoded reference token value.
106     * @throws JsonException if the reference token value is malformed.
107     */
108    private String decode(String value) {
109        try {
110            return new URI("#" + value).getFragment();
111        } catch (URISyntaxException use) {
112            throw new JsonException(use.getMessage());
113        }
114    }
115
116    /**
117     * Returns the number of reference tokens in the pointer.
118     *
119     * @return the number of reference tokens in the pointer.
120     */
121    public int size() {
122        return tokens.length;
123    }
124
125    /**
126     * Returns the reference token at the specified position.
127     *
128     * @param index the index of the reference token to return.
129     * @return the reference token at the specified position.
130     * @throws IndexOutOfBoundsException if the index is out of range.
131     */
132    public String get(int index) {
133        if (index < 0 || index >= tokens.length) {
134            throw new IndexOutOfBoundsException();
135        }
136        return tokens[index];
137    }
138
139    /**
140     * Returns a newly allocated array of strings, containing the pointer's reference tokens.
141     * No references to the array are maintained by the pointer. Hence, the caller is free to
142     * modify it.
143     *
144     * @return a newly allocated array of strings, containing the pointer's reference tokens.
145     */
146    public String[] toArray() {
147        return Arrays.copyOf(tokens, tokens.length);
148    }
149
150    /**
151     * Returns a pointer to the parent of the JSON value identified by this JSON pointer,
152     * or {@code null} if the pointer has no parent JSON value (i.e. references document root).
153     *
154     * @return a pointer to the parent of of this JSON pointer. Can be null.
155     */
156    public JsonPointer parent() {
157        JsonPointer parent = null;
158        if (this.tokens.length > 0) {
159            parent = new JsonPointer();
160            parent.tokens = Arrays.copyOf(this.tokens, this.tokens.length - 1);
161        }
162        return parent;
163    }
164
165    /**
166     * Returns a pointer containing all but the first reference token contained
167     * in this pointer, or {@code /} if this pointer contains less than 2
168     * reference tokens.
169     * <p>
170     * This method yields the following results: <blockquote>
171     * <table cellpadding=1 cellspacing=0 summary="Examples illustrating usage of relativePointer">
172     * <tr>
173     * <th>Input</th>
174     * <th>Output</th>
175     * </tr>
176     * <tr>
177     * <td align=left>/</td>
178     * <td align=left><tt>/</tt></td>
179     * </tr>
180     * <tr>
181     * <td align=left>/a</td>
182     * <td align=left><tt>/</tt></td>
183     * </tr>
184     * <tr>
185     * <td align=left>/a/b</td>
186     * <td align=left>/b</td>
187     * </tr>
188     * <tr>
189     * <td align=left>/a/b/c</td>
190     * <td align=left>/b/c</td>
191     * </tr>
192     * </table>
193     * </blockquote>
194     *
195     * @return A pointer containing all but the first reference token contained
196     *         in this pointer.
197     */
198    public JsonPointer relativePointer() {
199        return tokens.length > 0 ? relativePointer(tokens.length - 1) : this;
200    }
201
202    /**
203     * Returns a pointer containing the last {@code sz} reference tokens
204     * contained in this pointer.
205     * <p>
206     * This method yields the following results: <blockquote>
207     * <table cellpadding=1 cellspacing=0 summary="Examples illustrating usage of relativePointer">
208     * <tr>
209     * <th>Input</th>
210     * <th>sz</th>
211     * <th>Output</th>
212     * </tr>
213     * <tr>
214     * <td align=left>/a/b/c</td>
215     * <td align=center>0</td>
216     * <td align=left>/</td>
217     * </tr>
218     * <tr>
219     * <td align=left>/a/b/c</td>
220     * <td align=center>1</td>
221     * <td align=left>/c</td>
222     * </tr>
223     * <tr>
224     * <td align=left>/a/b/c</td>
225     * <td align=center>2</td>
226     * <td align=left>/b/c</td>
227     * </tr>
228     * <tr>
229     * <td align=left>/a/b/c</td>
230     * <td align=center>3</td>
231     * <td align=left>/a/b/c</td>
232     * </tr>
233     * </table>
234     * </blockquote>
235     *
236     * @param sz
237     *            The number of trailing reference tokens to retain.
238     * @return A pointer containing the last {@code sz} reference tokens
239     *         contained in this pointer.
240     * @throws IndexOutOfBoundsException
241     *             If {@code sz} is negative or greater than {@code size()}.
242     */
243    public JsonPointer relativePointer(int sz) {
244        int length = tokens.length;
245        if (sz < 0 || sz > length) {
246            throw new IndexOutOfBoundsException();
247        } else if (sz == length) {
248            return this;
249        } else if (sz == 0) {
250            return new JsonPointer();
251        } else {
252            JsonPointer relativePointer = new JsonPointer();
253            relativePointer.tokens = Arrays.copyOfRange(tokens, length - sz, length);
254            return relativePointer;
255        }
256    }
257
258    /**
259     * Returns the last (leaf) reference token of the JSON pointer, or {@code null} if the
260     * pointer contains no reference tokens (i.e. references document root).
261     *
262     * @return the last (leaf) reference token of the JSON pointer if it exists, {@code null} otherwise
263     */
264    public String leaf() {
265        return tokens.length > 0 ? tokens[tokens.length - 1] : null;
266    }
267
268    /**
269     * Returns a new JSON pointer, which identifies a specified child member of the
270     * object identified by this pointer.
271     *
272     * @param child the name of the child member to identify.
273     * @return the child JSON pointer.
274     * @throws NullPointerException if {@code child} is {@code null}.
275     */
276    public JsonPointer child(String child) {
277        if (child == null) {
278            throw new NullPointerException();
279        }
280        JsonPointer pointer = new JsonPointer();
281        pointer.tokens = Arrays.copyOf(this.tokens, this.tokens.length + 1);
282        pointer.tokens[pointer.tokens.length - 1] = child;
283        return pointer;
284    }
285
286    /**
287     * Returns a new JSON pointer, which identifies a specified child element of the
288     * array identified by this pointer.
289     *
290     * @param child the index of the child element to identify.
291     * @return the child JSON pointer.
292     * @throws IndexOutOfBoundsException if {@code child} is less than zero.
293     */
294    public JsonPointer child(int child) {
295        if (child < 0) {
296            throw new IndexOutOfBoundsException();
297        }
298        return child(Integer.toString(child));
299    }
300
301    /**
302     * Returns {@code true} if this pointer identifies the root value of a JSON
303     * structure. More specifically, it returns {@code true} if this pointer
304     * does not contain any reference tokens (i.e. {@code size() == 0}).
305     *
306     * @return {@code true} if this pointer identifies the root value of a JSON
307     *         structure.
308     */
309    public boolean isEmpty() {
310        return size() == 0;
311    }
312
313    /**
314     * Returns an iterator over the pointer's reference tokens.
315     *
316     * @return an iterator over the pointer's reference tokens.
317     */
318    @Override
319    public Iterator<String> iterator() {
320        return new Iterator<String>() {
321            int cursor = 0;
322            @Override
323            public boolean hasNext() {
324                return cursor < tokens.length;
325            }
326            @Override
327            public String next() {
328                if (cursor >= tokens.length) {
329                    throw new NoSuchElementException();
330                }
331                return tokens[cursor++];
332            }
333            @Override
334            public void remove() {
335                throw new UnsupportedOperationException();
336            }
337        };
338    }
339
340    /**
341     * Returns the JSON pointer string value.
342     *
343     * @return the JSON pointer string value.
344     */
345    @Override
346    public String toString() {
347        final StringBuilder sb = new StringBuilder();
348        for (String token : tokens) {
349            sb.append('/').append(encode(token));
350        }
351        if (sb.length() == 0) {
352            sb.append('/');
353        }
354        return sb.toString();
355    }
356
357    /**
358     * Compares the specified object with this pointer for equality. Returns {@code true} if
359     * and only if the specified object is also a JSON pointer, both pointers have the same
360     * size, and all corresponding pairs of reference tokens in the two pointers are equal.
361     *
362     * @param o the object to be compared for equality with this pointer.
363     * @return {@code true} if the specified object is equal to this pointer.
364     */
365    @Override
366    public boolean equals(Object o) {
367        return o instanceof JsonPointer
368                && ((JsonPointer) o).size() == size()
369                && Arrays.equals(tokens, ((JsonPointer) o).tokens);
370    }
371
372    /**
373     * Returns the hash code value for this pointer.
374     *
375     * @return the hash code value for this pointer.
376     */
377    @Override
378    public int hashCode() {
379        return Arrays.hashCode(tokens);
380    }
381}