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 }