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 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(&quot;commons/rest&quot;);
70   * ResourcePath child = base.child(&quot;hello world&quot;);
71   * child.toString(); // commons/rest/hello%20world
72   *
73   * ResourcePath user = base.child(&quot;users&quot;).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, &quot;bjensen&quot;);
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 &lt; 0 || index &gt;= 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 }