View Javadoc
1   /*
2    * The contents of this file are subject to the terms of the Common Development and
3    * Distribution License (the License). You may not use this file except in compliance with the
4    * License.
5    *
6    * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
7    * specific language governing permission and limitations under the License.
8    *
9    * When distributing Covered Software, include this CDDL Header Notice in each file and include
10   * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
11   * Header, with the fields enclosed by brackets [] replaced by your own identifying
12   * information: "Portions Copyrighted [year] [name of copyright owner]".
13   *
14   * Copyright 2011-2016 ForgeRock AS.
15   */
16  
17  package org.forgerock.json;
18  
19  import java.net.URI;
20  import java.net.URISyntaxException;
21  import java.util.ArrayList;
22  import java.util.Arrays;
23  import java.util.Iterator;
24  import java.util.NoSuchElementException;
25  
26  /**
27   * Identifies a specific value within a JSON structure. Conforms with
28   * <a href="http://tools.ietf.org/html/draft-pbryan-zyp-json-pointer-02">draft-pbryan-zip-json-pointer-02</a>.
29   */
30  public class JsonPointer implements Iterable<String> {
31  
32      /** The reference tokens that make-up the JSON pointer. */
33      private String[] tokens = new String[0];
34  
35      /**
36       * Constructs a JSON pointer, identifying the root value of a JSON structure.
37       */
38      public JsonPointer() {
39          // empty tokens represents pointer to root value
40      }
41  
42      /**
43       * Constructs a JSON pointer, identifying the specified pointer value.
44       *
45       * @param pointer a string containing the JSON pointer of the value to identify.
46       * @throws JsonException if the pointer is malformed.
47       */
48      public JsonPointer(String pointer) {
49          String[] split = pointer.split("/", -1);
50          int length = split.length;
51          ArrayList<String> list = new ArrayList<>(length);
52          for (int n = 0; n < length; n++) {
53              if (n == 0 && split[n].length() == 0) {
54                  continue; // leading slash ignored
55              } else if (n == length - 1 && split[n].length() == 0) {
56                  continue; // trailing slash ignored
57              } else {
58                  list.add(decode(split[n]));
59              }
60          }
61          tokens = list.toArray(tokens);
62      }
63  
64      /**
65       * Constructs a JSON pointer from an array of reference tokens.
66       *
67       * @param tokens an array of string reference tokens.
68       */
69      public JsonPointer(String... tokens) {
70          this.tokens = Arrays.copyOf(tokens, tokens.length);
71      }
72  
73      /**
74       * Constructs a JSON pointer from an iterable collection of reference tokens.
75       *
76       * @param iterable an iterable collection of reference tokens.
77       */
78      public JsonPointer(Iterable<String> iterable) {
79          ArrayList<String> list = new ArrayList<>();
80          for (String element : iterable) {
81              list.add(element);
82          }
83          tokens = list.toArray(tokens);
84      }
85  
86      /**
87       * Constructs a JSON pointer, identifying the specified pointer value.
88       *
89       * @param pointer a string containing the JSON pointer of the value to identify.
90       * @return The new JSON pointer
91       * @throws JsonException if the pointer is malformed.
92       */
93      public static JsonPointer ptr(final String pointer) {
94          return new JsonPointer(pointer);
95      }
96  
97      /**
98       * Constructs a JSON pointer from an array of reference tokens.
99       *
100      * @param tokens an array of string reference tokens.
101      * @return The new json pointer
102      */
103     public static JsonPointer ptr(final String... tokens) {
104         return new JsonPointer(tokens);
105     }
106 
107     /**
108      * Constructs a JSON pointer from an iterable collection of reference tokens.
109      *
110      * @param iterable an iterable collection of reference tokens.
111      * @return The new json pointer
112      */
113     public static JsonPointer ptr(final Iterable<String> iterable) {
114         return new JsonPointer(iterable);
115     }
116 
117     /**
118      * Encodes a reference token into a string value suitable to expressing in a JSON
119      * pointer string value.
120      *
121      * @param value the reference token value to be encoded.
122      * @return the encode reference token value.
123      */
124     private String encode(String value) {
125         try {
126             return new URI(null, null, null, null, value).toASCIIString().substring(1).replaceAll("/", "%2F");
127         } catch (URISyntaxException use) { // shouldn't happen
128             throw new IllegalStateException(use.getMessage());
129         }
130     }
131 
132     /**
133      * Decodes a reference token into a string value that the pointer maintains.
134      *
135      * @param value the reference token value to decode.
136      * @return the decoded reference token value.
137      * @throws JsonException if the reference token value is malformed.
138      */
139     private String decode(String value) {
140         try {
141             return new URI("#" + value).getFragment();
142         } catch (URISyntaxException use) {
143             throw new JsonException(use.getMessage());
144         }
145     }
146 
147     /**
148      * Returns the number of reference tokens in the pointer.
149      *
150      * @return the number of reference tokens in the pointer.
151      */
152     public int size() {
153         return tokens.length;
154     }
155 
156     /**
157      * Returns the reference token at the specified position.
158      *
159      * @param index the index of the reference token to return.
160      * @return the reference token at the specified position.
161      * @throws IndexOutOfBoundsException if the index is out of range.
162      */
163     public String get(int index) {
164         if (index < 0 || index >= tokens.length) {
165             throw new IndexOutOfBoundsException();
166         }
167         return tokens[index];
168     }
169 
170     /**
171      * Returns a newly allocated array of strings, containing the pointer's reference tokens.
172      * No references to the array are maintained by the pointer. Hence, the caller is free to
173      * modify it.
174      *
175      * @return a newly allocated array of strings, containing the pointer's reference tokens.
176      */
177     public String[] toArray() {
178         return Arrays.copyOf(tokens, tokens.length);
179     }
180 
181     /**
182      * Returns a pointer to the parent of the JSON value identified by this JSON pointer,
183      * or {@code null} if the pointer has no parent JSON value (i.e. references document root).
184      *
185      * @return a pointer to the parent of of this JSON pointer. Can be null.
186      */
187     public JsonPointer parent() {
188         JsonPointer parent = null;
189         if (this.tokens.length > 0) {
190             parent = new JsonPointer();
191             parent.tokens = Arrays.copyOf(this.tokens, this.tokens.length - 1);
192         }
193         return parent;
194     }
195 
196     /**
197      * Returns a pointer containing all but the first reference token contained
198      * in this pointer, or {@code /} if this pointer contains less than 2
199      * reference tokens.
200      * <p>
201      * This method yields the following results: <blockquote>
202      * <table cellpadding=1 cellspacing=0 summary="Examples illustrating usage of relativePointer">
203      * <tr>
204      * <th>Input</th>
205      * <th>Output</th>
206      * </tr>
207      * <tr>
208      * <td align=left>/</td>
209      * <td align=left><tt>/</tt></td>
210      * </tr>
211      * <tr>
212      * <td align=left>/a</td>
213      * <td align=left><tt>/</tt></td>
214      * </tr>
215      * <tr>
216      * <td align=left>/a/b</td>
217      * <td align=left>/b</td>
218      * </tr>
219      * <tr>
220      * <td align=left>/a/b/c</td>
221      * <td align=left>/b/c</td>
222      * </tr>
223      * </table>
224      * </blockquote>
225      *
226      * @return A pointer containing all but the first reference token contained
227      *         in this pointer.
228      */
229     public JsonPointer relativePointer() {
230         return tokens.length > 0 ? relativePointer(tokens.length - 1) : this;
231     }
232 
233     /**
234      * Returns a pointer containing the last {@code sz} reference tokens
235      * contained in this pointer.
236      * <p>
237      * This method yields the following results: <blockquote>
238      * <table cellpadding=1 cellspacing=0 summary="Examples illustrating usage of relativePointer">
239      * <tr>
240      * <th>Input</th>
241      * <th>sz</th>
242      * <th>Output</th>
243      * </tr>
244      * <tr>
245      * <td align=left>/a/b/c</td>
246      * <td align=center>0</td>
247      * <td align=left>/</td>
248      * </tr>
249      * <tr>
250      * <td align=left>/a/b/c</td>
251      * <td align=center>1</td>
252      * <td align=left>/c</td>
253      * </tr>
254      * <tr>
255      * <td align=left>/a/b/c</td>
256      * <td align=center>2</td>
257      * <td align=left>/b/c</td>
258      * </tr>
259      * <tr>
260      * <td align=left>/a/b/c</td>
261      * <td align=center>3</td>
262      * <td align=left>/a/b/c</td>
263      * </tr>
264      * </table>
265      * </blockquote>
266      *
267      * @param sz
268      *            The number of trailing reference tokens to retain.
269      * @return A pointer containing the last {@code sz} reference tokens
270      *         contained in this pointer.
271      * @throws IndexOutOfBoundsException
272      *             If {@code sz} is negative or greater than {@code size()}.
273      */
274     public JsonPointer relativePointer(int sz) {
275         int length = tokens.length;
276         if (sz < 0 || sz > length) {
277             throw new IndexOutOfBoundsException();
278         } else if (sz == length) {
279             return this;
280         } else if (sz == 0) {
281             return new JsonPointer();
282         } else {
283             JsonPointer relativePointer = new JsonPointer();
284             relativePointer.tokens = Arrays.copyOfRange(tokens, length - sz, length);
285             return relativePointer;
286         }
287     }
288 
289     /**
290      * Returns the last (leaf) reference token of the JSON pointer, or {@code null} if the
291      * pointer contains no reference tokens (i.e. references document root).
292      *
293      * @return the last (leaf) reference token of the JSON pointer if it exists, {@code null} otherwise
294      */
295     public String leaf() {
296         return tokens.length > 0 ? tokens[tokens.length - 1] : null;
297     }
298 
299     /**
300      * Returns a new JSON pointer, which identifies a specified child member of the
301      * object identified by this pointer.
302      *
303      * @param child the name of the child member to identify.
304      * @return the child JSON pointer.
305      * @throws NullPointerException if {@code child} is {@code null}.
306      */
307     public JsonPointer child(String child) {
308         if (child == null) {
309             throw new NullPointerException();
310         }
311         JsonPointer pointer = new JsonPointer();
312         pointer.tokens = Arrays.copyOf(this.tokens, this.tokens.length + 1);
313         pointer.tokens[pointer.tokens.length - 1] = child;
314         return pointer;
315     }
316 
317     /**
318      * Returns a new JSON pointer, which identifies a specified child element of the
319      * array identified by this pointer.
320      *
321      * @param child the index of the child element to identify.
322      * @return the child JSON pointer.
323      * @throws IndexOutOfBoundsException if {@code child} is less than zero.
324      */
325     public JsonPointer child(int child) {
326         if (child < 0) {
327             throw new IndexOutOfBoundsException();
328         }
329         return child(Integer.toString(child));
330     }
331 
332     /**
333      * Returns {@code true} if this pointer identifies the root value of a JSON
334      * structure. More specifically, it returns {@code true} if this pointer
335      * does not contain any reference tokens (i.e. {@code size() == 0}).
336      *
337      * @return {@code true} if this pointer identifies the root value of a JSON
338      *         structure.
339      */
340     public boolean isEmpty() {
341         return size() == 0;
342     }
343 
344     /**
345      * Returns an iterator over the pointer's reference tokens.
346      *
347      * @return an iterator over the pointer's reference tokens.
348      */
349     @Override
350     public Iterator<String> iterator() {
351         return new Iterator<String>() {
352             int cursor = 0;
353             @Override
354             public boolean hasNext() {
355                 return cursor < tokens.length;
356             }
357             @Override
358             public String next() {
359                 if (cursor >= tokens.length) {
360                     throw new NoSuchElementException();
361                 }
362                 return tokens[cursor++];
363             }
364             @Override
365             public void remove() {
366                 throw new UnsupportedOperationException();
367             }
368         };
369     }
370 
371     /**
372      * Returns the JSON pointer string value.
373      *
374      * @return the JSON pointer string value.
375      */
376     @Override
377     public String toString() {
378         final StringBuilder sb = new StringBuilder();
379         for (String token : tokens) {
380             sb.append('/').append(encode(token));
381         }
382         if (sb.length() == 0) {
383             sb.append('/');
384         }
385         return sb.toString();
386     }
387 
388     /**
389      * Compares the specified object with this pointer for equality. Returns {@code true} if
390      * and only if the specified object is also a JSON pointer, both pointers have the same
391      * size, and all corresponding pairs of reference tokens in the two pointers are equal.
392      *
393      * @param o the object to be compared for equality with this pointer.
394      * @return {@code true} if the specified object is equal to this pointer.
395      */
396     @Override
397     public boolean equals(Object o) {
398         return o instanceof JsonPointer
399                 && ((JsonPointer) o).size() == size()
400                 && Arrays.equals(tokens, ((JsonPointer) o).tokens);
401     }
402 
403     /**
404      * Returns the hash code value for this pointer.
405      *
406      * @return the hash code value for this pointer.
407      */
408     @Override
409     public int hashCode() {
410         return Arrays.hashCode(tokens);
411     }
412 }