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 copyright [year] [name of copyright owner]".
13 *
14 * Copyright 2013-2015 ForgeRock AS.
15 */
16
17 package org.forgerock.json.resource;
18
19 import static java.util.Arrays.asList;
20 import static org.forgerock.http.util.Paths.*;
21
22 import java.util.Collection;
23 import java.util.Iterator;
24 import java.util.Locale;
25 import java.util.NoSuchElementException;
26 import java.util.regex.Pattern;
27
28 /**
29 * A relative path, or URL, to a resource. A resource path is an ordered list of
30 * zero or more path elements in big-endian order. The string representation of
31 * a resource path conforms to the URL path encoding rules defined in <a
32 * href="http://tools.ietf.org/html/rfc3986#section-3.3">RFC 3986 section
33 * 3.3</a>:
34 *
35 * <pre>
36 * {@code
37 * path = path-abempty ; begins with "/" or is empty
38 * / ...
39 *
40 * path-abempty = *( "/" segment )
41 * segment = *pchar
42 * pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
43 *
44 * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
45 * pct-encoded = "%" HEXDIG HEXDIG
46 * sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
47 * / "*" / "+" / "," / ";" / "="
48 *
49 * HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
50 * ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
51 * DIGIT = %x30-39 ; 0-9
52 * }
53 * </pre>
54 *
55 * The empty resource path having zero path elements may be obtained by calling
56 * {@link #empty()}. Resource paths are case insensitive and empty path elements
57 * are not allowed. In addition, resource paths will be automatically trimmed
58 * such that any leading or trailing slashes are removed. In other words, all
59 * resource paths will be considered to be "relative". At the moment the
60 * relative path elements "." and ".." are not supported.
61 * <p>
62 * New resource paths can be created from their string representation using
63 * {@link #resourcePath(String)}, or by deriving new resource paths from existing
64 * values, e.g. using {@link #parent()} or {@link #child(Object)}.
65 * <p>
66 * Example:
67 *
68 * <pre>
69 * ResourcePath base = ResourcePath.valueOf("commons/rest");
70 * ResourcePath child = base.child("hello world");
71 * child.toString(); // commons/rest/hello%20world
72 *
73 * ResourcePath user = base.child("users").child(123);
74 * user.toString(); // commons/rest/users/123
75 * </pre>
76 */
77 public final class ResourcePath implements Comparable<ResourcePath>, Iterable<String> {
78 private static final ResourcePath EMPTY = new ResourcePath();
79
80 /**
81 * Returns the empty resource path whose string representation is the empty
82 * string and which has zero path elements.
83 *
84 * @return The empty resource path.
85 */
86 public static ResourcePath empty() {
87 return EMPTY;
88 }
89
90 /**
91 * Creates a new resource path using the provided path template and
92 * unencoded path elements. This method first URL encodes each of the path
93 * elements and then substitutes them into the template using
94 * {@link String#format(String, Object...)}. Finally, the formatted string
95 * is parsed as a resource path using {@link #resourcePath(String)}.
96 * <p>
97 * This method may be useful in cases where the structure of a resource path
98 * is not known at compile time, for example, it may be obtained from a
99 * configuration file. Example usage:
100 *
101 * <pre>
102 * String template = "rest/users/%s"
103 * ResourcePath path = ResourcePath.format(template, "bjensen");
104 * </pre>
105 *
106 * @param template
107 * The resource path template.
108 * @param pathElements
109 * The path elements to be URL encoded and then substituted into
110 * the template.
111 * @return The formatted template parsed as a resource path.
112 * @throws IllegalArgumentException
113 * If the formatted template contains empty path elements.
114 * @see org.forgerock.http.util.Paths#urlEncode(Object)
115 */
116 public static ResourcePath format(final String template, final Object... pathElements) {
117 final String[] encodedPathElements = new String[pathElements.length];
118 for (int i = 0; i < pathElements.length; i++) {
119 encodedPathElements[i] = urlEncode(pathElements[i]);
120 }
121 return resourcePath(String.format(template, (Object[]) encodedPathElements));
122 }
123
124 /**
125 * Compiled regular expression for splitting resource paths into path
126 * elements.
127 */
128 private static final Pattern PATH_SPLITTER = Pattern.compile("/");
129
130 /**
131 * Parses the provided string representation of a resource path.
132 *
133 * @param path
134 * The URL-encoded resource path to be parsed.
135 * @return The provided string representation of a resource path.
136 * @throws IllegalArgumentException
137 * If the resource path contains empty path elements.
138 * @see #toString()
139 */
140 public static ResourcePath resourcePath(final String path) {
141 return valueOf(path);
142 }
143
144 /**
145 * Parses the provided string representation of a resource path.
146 *
147 * @param path
148 * The URL-encoded resource path to be parsed.
149 * @return The provided string representation of a resource path.
150 * @throws IllegalArgumentException
151 * If the resource path contains empty path elements.
152 * @see #toString()
153 */
154 public static ResourcePath valueOf(final String path) {
155 if (path.isEmpty()) {
156 // Fast-path.
157 return EMPTY;
158 }
159
160 // Split on path separators and trim leading slash or trailing slash.
161 final String[] elements = PATH_SPLITTER.split(path, -1);
162 final int sz = elements.length;
163 final int startIndex = elements[0].isEmpty() ? 1 : 0;
164 final int endIndex = sz > 1 && elements[sz - 1].isEmpty() ? sz - 1 : sz;
165 if (startIndex == endIndex) {
166 return EMPTY;
167 }
168
169 // Normalize the path elements checking for empty elements.
170 final StringBuilder trimmedPath = new StringBuilder(path.length());
171 final StringBuilder normalizedPath = new StringBuilder(path.length());
172 for (int i = startIndex; i < endIndex; i++) {
173 final String element = elements[i];
174 if (element.isEmpty()) {
175 throw new IllegalArgumentException("Resource path '" + path
176 + "' contains empty path elements");
177 }
178 final String normalizedElement = normalizePathElement(element, true);
179 if (i != startIndex) {
180 trimmedPath.append('/');
181 normalizedPath.append('/');
182 }
183 trimmedPath.append(element);
184 normalizedPath.append(normalizedElement);
185 }
186 return new ResourcePath(trimmedPath.toString(), normalizedPath.toString(), endIndex - startIndex);
187 }
188
189 private static String normalizePathElement(final String element, final boolean needsDecoding) {
190 if (needsDecoding) {
191 return urlEncode(urlDecode(element).toLowerCase(Locale.ENGLISH));
192 } else {
193 return element.toLowerCase(Locale.ENGLISH);
194 }
195 }
196
197 private final String path; // uri encoded
198 private final String normalizedPath; // uri encoded
199 private final int size;
200
201 /**
202 * Creates a new empty resource path whose string representation is the
203 * empty string and which has zero path elements. This method is provided in
204 * order to comply with the Java Collections Framework recommendations.
205 * However, it is recommended that applications use {@link #empty()} in
206 * order to avoid unnecessary memory allocation.
207 */
208 public ResourcePath() {
209 this.path = this.normalizedPath = "";
210 this.size = 0;
211 }
212
213 /**
214 * Creates a new resource path having the provided path elements.
215 *
216 * @param pathElements
217 * The unencoded path elements.
218 */
219 public ResourcePath(final Collection<? extends Object> pathElements) {
220 int i = 0;
221 final StringBuilder pathBuilder = new StringBuilder();
222 final StringBuilder normalizedPathBuilder = new StringBuilder();
223 for (final Object element : pathElements) {
224 final String s = element.toString();
225 if (i > 0) {
226 pathBuilder.append('/');
227 normalizedPathBuilder.append('/');
228 }
229 final String encodedPathElement = urlEncode(s);
230 pathBuilder.append(encodedPathElement);
231 final String normalizedPathElement = normalizePathElement(s, false);
232 normalizedPathBuilder.append(urlEncode(normalizedPathElement));
233 i++;
234 }
235 this.path = pathBuilder.toString();
236 this.normalizedPath = normalizedPathBuilder.toString();
237 this.size = pathElements.size();
238 }
239
240 /**
241 * Creates a new resource path having the provided path elements.
242 *
243 * @param pathElements
244 * The unencoded path elements.
245 */
246 public ResourcePath(final Object... pathElements) {
247 this(asList(pathElements));
248 }
249
250 private ResourcePath(final String path, final String normalizedPath, final int size) {
251 this.path = path;
252 this.normalizedPath = normalizedPath;
253 this.size = size;
254 }
255
256 /**
257 * Creates a new resource path which is a child of this resource path. The
258 * returned resource path will have the same path elements as this resource
259 * path and, in addition, the provided path element.
260 *
261 * @param pathElement
262 * The unencoded child path element.
263 * @return A new resource path which is a child of this resource path.
264 */
265 public ResourcePath child(final Object pathElement) {
266 final String s = pathElement.toString();
267 final String encodedPathElement = urlEncode(s);
268 final String normalizedPathElement = normalizePathElement(s, false);
269 final String normalizedEncodedPathElement = urlEncode(normalizedPathElement);
270 if (isEmpty()) {
271 return new ResourcePath(encodedPathElement, normalizedEncodedPathElement, 1);
272 } else {
273 final String newPath = path + "/" + encodedPathElement;
274 final String newNormalizedPath = normalizedPath + "/" + normalizedEncodedPathElement;
275 return new ResourcePath(newPath, newNormalizedPath, size + 1);
276 }
277 }
278
279 /**
280 * Compares this resource path with the provided resource path. Resource
281 * paths are compared case sensitively and ancestors sort before
282 * descendants.
283 *
284 * @param o
285 * {@inheritDoc}
286 * @return {@inheritDoc}
287 */
288 @Override
289 public int compareTo(final ResourcePath o) {
290 return normalizedPath.compareTo(o.normalizedPath);
291 }
292
293 /**
294 * Creates a new resource path which is a descendant of this resource path.
295 * The returned resource path will have be formed of the concatenation of
296 * this resource path and the provided resource path.
297 *
298 * @param suffix
299 * The resource path to be appended to this resource path.
300 * @return A new resource path which is a descendant of this resource path.
301 */
302 public ResourcePath concat(final ResourcePath suffix) {
303 if (isEmpty()) {
304 return suffix;
305 } else if (suffix.isEmpty()) {
306 return this;
307 } else {
308 final String newPath = path + "/" + suffix.path;
309 final String newNormalizedPath = normalizedPath + "/" + suffix.normalizedPath;
310 return new ResourcePath(newPath, newNormalizedPath, size + suffix.size);
311 }
312 }
313
314 /**
315 * Creates a new resource path which is a descendant of this resource path.
316 * The returned resource path will have be formed of the concatenation of
317 * this resource path and the provided resource path.
318 *
319 * @param suffix
320 * The resource path to be appended to this resource path.
321 * @return A new resource path which is a descendant of this resource path.
322 * @throws IllegalArgumentException
323 * If the the suffix contains empty path elements.
324 */
325 public ResourcePath concat(final String suffix) {
326 return concat(resourcePath(suffix));
327 }
328
329 /**
330 * Returns {@code true} if {@code obj} is a resource path having the exact
331 * same elements as this resource path.
332 *
333 * @param obj
334 * The object to be compared.
335 * @return {@code true} if {@code obj} is a resource path having the exact
336 * same elements as this resource path.
337 */
338 @Override
339 public boolean equals(final Object obj) {
340 if (this == obj) {
341 return true;
342 } else if (obj instanceof ResourcePath) {
343 return normalizedPath.equals(((ResourcePath) obj).normalizedPath);
344 } else {
345 return false;
346 }
347 }
348
349 /**
350 * Returns the path element at the specified position in this resource path.
351 * The path element at position 0 is the top level element (closest to
352 * root).
353 *
354 * @param index
355 * The index of the path element to be returned, where 0 is the
356 * top level element.
357 * @return The path element at the specified position in this resource path.
358 * @throws IndexOutOfBoundsException
359 * If the index is out of range (index < 0 || index >= size()).
360 */
361 public String get(final int index) {
362 if (index < 0 || index >= size) {
363 throw new IndexOutOfBoundsException();
364 }
365 int startIndex = 0;
366 int endIndex = nextElementEndIndex(path, 0);
367 for (int i = 0; i < index; i++) {
368 startIndex = endIndex + 1;
369 endIndex = nextElementEndIndex(path, startIndex);
370 }
371 return urlDecode(path.substring(startIndex, endIndex));
372 }
373
374 /**
375 * Returns a hash code for this resource path.
376 *
377 * @return A hash code for this resource path.
378 */
379 @Override
380 public int hashCode() {
381 return normalizedPath.hashCode();
382 }
383
384 /**
385 * Returns a resource path which is a subsequence of the path elements
386 * contained in this resource path beginning with the first element (0) and
387 * ending with the element at position {@code endIndex-1}. The returned
388 * resource path will therefore have the size {@code endIndex}. Calling this
389 * method is equivalent to:
390 *
391 * <pre>
392 * subSequence(0, endIndex);
393 * </pre>
394 *
395 * @param endIndex
396 * The end index, exclusive.
397 * @return A resource path which is a subsequence of the path elements
398 * contained in this resource path.
399 * @throws IndexOutOfBoundsException
400 * If {@code endIndex} is bigger than {@code size()}.
401 */
402 public ResourcePath head(final int endIndex) {
403 return subSequence(0, endIndex);
404 }
405
406 /**
407 * Returns {@code true} if this resource path contains no path elements.
408 *
409 * @return {@code true} if this resource path contains no path elements.
410 */
411 public boolean isEmpty() {
412 return size == 0;
413 }
414
415 /**
416 * Returns an iterator over the path elements in this resource path. The
417 * returned iterator will not support the {@link Iterator#remove()} method
418 * and will return path elements starting with index 0, then 1, then 2, etc.
419 *
420 * @return An iterator over the path elements in this resource path.
421 */
422 @Override
423 public Iterator<String> iterator() {
424 return new Iterator<String>() {
425 private int startIndex = 0;
426 private int endIndex = nextElementEndIndex(path, 0);
427
428 @Override
429 public boolean hasNext() {
430 return startIndex < path.length();
431 }
432
433 @Override
434 public String next() {
435 if (!hasNext()) {
436 throw new NoSuchElementException();
437 }
438 final String element = path.substring(startIndex, endIndex);
439 startIndex = endIndex + 1;
440 endIndex = nextElementEndIndex(path, startIndex);
441 return urlDecode(element);
442 }
443
444 @Override
445 public void remove() {
446 throw new UnsupportedOperationException();
447 }
448
449 };
450 }
451
452 /**
453 * Returns the last path element in this resource path. Calling this method
454 * is equivalent to:
455 *
456 * <pre>
457 * resourcePath.get(resourcePath.size() - 1);
458 * </pre>
459 *
460 * @return The last path element in this resource path.
461 */
462 public String leaf() {
463 return get(size() - 1);
464 }
465
466 /**
467 * Returns the resource path which is the immediate parent of this resource
468 * path, or {@code null} if this resource path is empty.
469 *
470 * @return The resource path which is the immediate parent of this resource
471 * path, or {@code null} if this resource path is empty.
472 */
473 public ResourcePath parent() {
474 switch (size()) {
475 case 0:
476 return null;
477 case 1:
478 return EMPTY;
479 default:
480 final String newPath = path.substring(0, path.lastIndexOf('/') /* safe */);
481 final String newNormalizedPath =
482 normalizedPath.substring(0, normalizedPath.lastIndexOf('/') /* safe */);
483 return new ResourcePath(newPath, newNormalizedPath, size - 1);
484 }
485 }
486
487 /**
488 * Returns the number of elements in this resource path, or 0 if it is
489 * empty.
490 *
491 * @return The number of elements in this resource path, or 0 if it is
492 * empty.
493 */
494 public int size() {
495 return size;
496 }
497
498 /**
499 * Returns {@code true} if this resource path is equal to or begins with the
500 * provided resource resource path.
501 *
502 * @param prefix
503 * The resource path prefix.
504 * @return {@code true} if this resource path is equal to or begins with the
505 * provided resource resource path.
506 */
507 public boolean startsWith(final ResourcePath prefix) {
508 if (size == prefix.size) {
509 return equals(prefix);
510 } else if (size < prefix.size) {
511 return false;
512 } else if (prefix.size == 0) {
513 return true;
514 } else {
515 return normalizedPath.startsWith(prefix.normalizedPath)
516 && normalizedPath.charAt(prefix.normalizedPath.length()) == '/';
517 }
518 }
519
520 /**
521 * Returns {@code true} if this resource path is equal to or begins with the
522 * provided resource resource path.
523 *
524 * @param prefix
525 * The resource path prefix.
526 * @return {@code true} if this resource path is equal to or begins with the
527 * provided resource resource path.
528 * @throws IllegalArgumentException
529 * If the the prefix contains empty path elements.
530 */
531 public boolean startsWith(final String prefix) {
532 return startsWith(resourcePath(prefix));
533 }
534
535 /**
536 * Returns a resource path which is a subsequence of the path elements
537 * contained in this resource path beginning with the element at position
538 * {@code beginIndex} and ending with the element at position
539 * {@code endIndex-1}. The returned resource path will therefore have the
540 * size {@code endIndex - beginIndex}.
541 *
542 * @param beginIndex
543 * The beginning index, inclusive.
544 * @param endIndex
545 * The end index, exclusive.
546 * @return A resource path which is a subsequence of the path elements
547 * contained in this resource path.
548 * @throws IndexOutOfBoundsException
549 * If {@code beginIndex} is negative, or {@code endIndex} is
550 * bigger than {@code size()}, or if {@code beginIndex} is
551 * bigger than {@code endIndex}.
552 */
553 public ResourcePath subSequence(final int beginIndex, final int endIndex) {
554 if (beginIndex < 0 || endIndex > size || beginIndex > endIndex) {
555 throw new IndexOutOfBoundsException();
556 }
557 if (beginIndex == 0 && endIndex == size) {
558 return this;
559 }
560 if (endIndex - beginIndex == 0) {
561 return EMPTY;
562 }
563 final String subPath = subPath(path, beginIndex, endIndex);
564 final String subNormalizedPath = subPath(normalizedPath, beginIndex, endIndex);
565 return new ResourcePath(subPath, subNormalizedPath, endIndex - beginIndex);
566 }
567
568 /**
569 * Returns a resource path which is a subsequence of the path elements
570 * contained in this resource path beginning with the element at position
571 * {@code beginIndex} and ending with the last element in this resource
572 * path. The returned resource path will therefore have the size
573 * {@code size() - beginIndex}. Calling this method is equivalent to:
574 *
575 * <pre>
576 * subSequence(beginIndex, size());
577 * </pre>
578 *
579 * @param beginIndex
580 * The beginning index, inclusive.
581 * @return A resource path which is a subsequence of the path elements
582 * contained in this resource path.
583 * @throws IndexOutOfBoundsException
584 * If {@code beginIndex} is negative, or if {@code beginIndex}
585 * is bigger than {@code size()}.
586 */
587 public ResourcePath tail(final int beginIndex) {
588 return subSequence(beginIndex, size);
589 }
590
591 /**
592 * Returns the URL path encoded string representation of this resource path.
593 *
594 * @return The URL path encoded string representation of this resource path.
595 * @see #resourcePath(String)
596 */
597 @Override
598 public String toString() {
599 return path;
600 }
601
602 private int nextElementEndIndex(final String s, final int startIndex) {
603 final int index = s.indexOf('/', startIndex);
604 return index < 0 ? s.length() : index;
605 }
606
607 private String subPath(final String s, final int beginIndex, final int endIndex) {
608 int startCharIndex = 0;
609 int endCharIndex = nextElementEndIndex(s, 0);
610 for (int i = 0; i < beginIndex; i++) {
611 startCharIndex = endCharIndex + 1;
612 endCharIndex = nextElementEndIndex(s, startCharIndex);
613 }
614 int tmpStartCharIndex;
615 for (int i = beginIndex + 1; i < endIndex; i++) {
616 tmpStartCharIndex = endCharIndex + 1;
617 endCharIndex = nextElementEndIndex(s, tmpStartCharIndex);
618 }
619 return s.substring(startCharIndex, endCharIndex);
620 }
621 }