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 copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2013-2015 ForgeRock AS.
015 */
016
017package org.forgerock.json.resource;
018
019import static java.util.Arrays.asList;
020import static org.forgerock.http.util.Paths.*;
021
022import java.util.Collection;
023import java.util.Iterator;
024import java.util.Locale;
025import java.util.NoSuchElementException;
026import java.util.regex.Pattern;
027
028/**
029 * A relative path, or URL, to a resource. A resource path is an ordered list of
030 * zero or more path elements in big-endian order. The string representation of
031 * a resource path conforms to the URL path encoding rules defined in <a
032 * href="http://tools.ietf.org/html/rfc3986#section-3.3">RFC 3986 section
033 * 3.3</a>:
034 *
035 * <pre>
036 * {@code
037 * path          = path-abempty    ; begins with "/" or is empty
038 *                 / ...
039 *
040 * path-abempty  = *( "/" segment )
041 * segment       = *pchar
042 * pchar         = unreserved / pct-encoded / sub-delims / ":" / "@"
043 *
044 * unreserved    = ALPHA / DIGIT / "-" / "." / "_" / "~"
045 * pct-encoded   = "%" HEXDIG HEXDIG
046 * sub-delims    = "!" / "$" / "&" / "'" / "(" / ")"
047 *                 / "*" / "+" / "," / ";" / "="
048 *
049 * HEXDIG        =  DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
050 * ALPHA         =  %x41-5A / %x61-7A   ; A-Z / a-z
051 * DIGIT         =  %x30-39             ; 0-9
052 * }
053 * </pre>
054 *
055 * The empty resource path having zero path elements may be obtained by calling
056 * {@link #empty()}. Resource paths are case insensitive and empty path elements
057 * are not allowed. In addition, resource paths will be automatically trimmed
058 * such that any leading or trailing slashes are removed. In other words, all
059 * resource paths will be considered to be "relative". At the moment the
060 * relative path elements "." and ".." are not supported.
061 * <p>
062 * New resource paths can be created from their string representation using
063 * {@link #resourcePath(String)}, or by deriving new resource paths from existing
064 * values, e.g. using {@link #parent()} or {@link #child(Object)}.
065 * <p>
066 * Example:
067 *
068 * <pre>
069 * ResourcePath base = ResourcePath.valueOf(&quot;commons/rest&quot;);
070 * ResourcePath child = base.child(&quot;hello world&quot;);
071 * child.toString(); // commons/rest/hello%20world
072 *
073 * ResourcePath user = base.child(&quot;users&quot;).child(123);
074 * user.toString(); // commons/rest/users/123
075 * </pre>
076 */
077public final class ResourcePath implements Comparable<ResourcePath>, Iterable<String> {
078    private static final ResourcePath EMPTY = new ResourcePath();
079
080    /**
081     * Returns the empty resource path whose string representation is the empty
082     * string and which has zero path elements.
083     *
084     * @return The empty resource path.
085     */
086    public static ResourcePath empty() {
087        return EMPTY;
088    }
089
090    /**
091     * Creates a new resource path using the provided path template and
092     * unencoded path elements. This method first URL encodes each of the path
093     * elements and then substitutes them into the template using
094     * {@link String#format(String, Object...)}. Finally, the formatted string
095     * is parsed as a resource path using {@link #resourcePath(String)}.
096     * <p>
097     * This method may be useful in cases where the structure of a resource path
098     * is not known at compile time, for example, it may be obtained from a
099     * 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}