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 }