1 /* 2 * Copyright 2016, Google Inc. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions are 6 * met: 7 * 8 * * Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * * Redistributions in binary form must reproduce the above 11 * copyright notice, this list of conditions and the following disclaimer 12 * in the documentation and/or other materials provided with the 13 * distribution. 14 * * Neither the name of Google Inc. nor the names of its 15 * contributors may be used to endorse or promote products derived from 16 * this software without specific prior written permission. 17 * 18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 */ 30 31 package com.google.api.pathtemplate; 32 33 import com.google.auto.value.AutoValue; 34 import com.google.common.base.Splitter; 35 import com.google.common.collect.ImmutableList; 36 import com.google.common.collect.ImmutableMap; 37 import com.google.common.collect.Lists; 38 import com.google.common.collect.Maps; 39 import java.io.UnsupportedEncodingException; 40 import java.net.URLDecoder; 41 import java.net.URLEncoder; 42 import java.util.ArrayList; 43 import java.util.List; 44 import java.util.ListIterator; 45 import java.util.Map; 46 import java.util.Objects; 47 import java.util.Set; 48 import java.util.regex.Matcher; 49 import java.util.regex.Pattern; 50 import javax.annotation.Nullable; 51 52 /** 53 * Represents a path template. 54 * 55 * <p>Templates use the syntax of the API platform; see the protobuf of HttpRule for details. A 56 * template consists of a sequence of literals, wildcards, and variable bindings, where each binding 57 * can have a sub-path. A string representation can be parsed into an instance of {@link 58 * PathTemplate}, which can then be used to perform matching and instantiation. 59 * 60 * <p>Matching and instantiation deals with unescaping and escaping using URL encoding rules. For 61 * example, if a template variable for a single segment is instantiated with a string like {@code 62 * "a/b"}, the slash will be escaped to {@code "%2f"}. (Note that slash will not be escaped for a 63 * multiple-segment variable, but other characters will). The literals in the template itself are 64 * <em>not</em> escaped automatically, and must be already URL encoded. 65 * 66 * <p>Here is an example for a template using simple variables: 67 * 68 * <pre>{@code 69 * PathTemplate template = PathTemplate.create("v1/shelves/{shelf}/books/{book}"); 70 * assert template.matches("v2/shelves") == false; 71 * Map<String, String> values = template.match("v1/shelves/s1/books/b1"); 72 * Map<String, String> expectedValues = new HashMap<>(); 73 * expectedValues.put("shelf", "s1"); 74 * expectedValues.put("book", "b1"); 75 * assert values.equals(expectedValues); 76 * assert template.instantiate(values).equals("v1/shelves/s1/books/b1"); 77 * }</pre> 78 * 79 * Templates can use variables which match sub-paths. Example: 80 * 81 * <pre>{@code 82 * PathTemplate template = PathTemplate.create("v1/{name=shelves/*/books/*}"}; 83 * assert template.match("v1/shelves/books/b1") == null; 84 * Map<String, String> expectedValues = new HashMap<>(); 85 * expectedValues.put("name", "shelves/s1/books/b1"); 86 * assert template.match("v1/shelves/s1/books/b1").equals(expectedValues); 87 * }</pre> 88 * 89 * Path templates can also be used with only wildcards. Each wildcard is associated with an implicit 90 * variable {@code $n}, where n is the zero-based position of the wildcard. Example: 91 * 92 * <pre>{@code 93 * PathTemplate template = PathTemplate.create("shelves/*/books/*"}; 94 * assert template.match("shelves/books/b1") == null; 95 * Map<String, String> values = template.match("v1/shelves/s1/books/b1"); 96 * Map<String, String> expectedValues = new HashMap<>(); 97 * expectedValues.put("$0", s1"); 98 * expectedValues.put("$1", "b1"); 99 * assert values.equals(expectedValues); 100 * }</pre> 101 * 102 * Paths input to matching can use URL relative syntax to indicate a host name by prefixing the host 103 * name, as in {@code //somewhere.io/some/path}. The host name is matched into the special variable 104 * {@link #HOSTNAME_VAR}. Patterns are agnostic about host names, and the same pattern can be used 105 * for URL relative syntax and simple path syntax: 106 * 107 * <pre>{@code 108 * PathTemplate template = PathTemplate.create("shelves/*"}; 109 * Map<String, String> expectedValues = new HashMap<>(); 110 * expectedValues.put(PathTemplate.HOSTNAME_VAR, "//somewhere.io"); 111 * expectedValues.put("$0", s1"); 112 * assert template.match("//somewhere.io/shelves/s1").equals(expectedValues); 113 * expectedValues.clear(); 114 * expectedValues.put("$0", s1"); 115 * assert template.match("shelves/s1").equals(expectedValues); 116 * }</pre> 117 * 118 * For the representation of a <em>resource name</em> see {@link TemplatedResourceName}, which is 119 * based on path templates. 120 */ 121 public class PathTemplate { 122 123 /** 124 * A constant identifying the special variable used for endpoint bindings in the result of {@link 125 * #matchFromFullName(String)}. It may also contain protocol string, if its provided in the input. 126 */ 127 public static final String HOSTNAME_VAR = "$hostname"; 128 129 // A regexp to match a custom verb at the end of a path. 130 private static final Pattern CUSTOM_VERB_PATTERN = Pattern.compile(":([^/*}{=]+)$"); 131 132 // A regex to match a hostname with or without protocol. 133 private static final Pattern HOSTNAME_PATTERN = Pattern.compile("^(\\w+:)?//"); 134 135 // A splitter on slash. 136 private static final Splitter SLASH_SPLITTER = Splitter.on('/').trimResults(); 137 138 // A regex to match the valid complex resource ID delimiters. 139 private static final Pattern COMPLEX_DELIMITER_PATTERN = Pattern.compile("[_\\-\\.~]"); 140 141 // A regex to match multiple complex resource ID delimiters. 142 private static final Pattern MULTIPLE_COMPLEX_DELIMITER_PATTERN = 143 Pattern.compile("\\}[_\\-\\.~]{2,}\\{"); 144 145 // A regex to match a missing complex resource ID delimiter. 146 private static final Pattern MISSING_COMPLEX_DELIMITER_PATTERN = Pattern.compile("\\}\\{"); 147 148 // A regex to match invalid complex resource ID delimiters. 149 private static final Pattern INVALID_COMPLEX_DELIMITER_PATTERN = 150 Pattern.compile("\\}[^_\\-\\.~]\\{"); 151 152 // A regex to match a closing segment (end brace) followed by one complex resource ID delimiter. 153 private static final Pattern END_SEGMENT_COMPLEX_DELIMITER_PATTERN = 154 Pattern.compile("\\}[_\\-\\.~]{1}"); 155 156 // Helper Types 157 // ============ 158 159 /** Specifies a path segment kind. */ 160 enum SegmentKind { 161 /** A literal path segment. */ 162 LITERAL, 163 164 /** A custom verb. Can only appear at the end of path. */ 165 CUSTOM_VERB, 166 167 /** A simple wildcard ('*'). */ 168 WILDCARD, 169 170 /** A path wildcard ('**'). */ 171 PATH_WILDCARD, 172 173 /** A field binding start. */ 174 BINDING, 175 176 /** A field binding end. */ 177 END_BINDING, 178 } 179 180 /** Specifies a path segment. */ 181 @AutoValue 182 abstract static class Segment { 183 184 /** A constant for the WILDCARD segment. */ 185 private static final Segment WILDCARD = create(SegmentKind.WILDCARD, "*"); 186 187 /** A constant for the PATH_WILDCARD segment. */ 188 private static final Segment PATH_WILDCARD = create(SegmentKind.PATH_WILDCARD, "**"); 189 190 /** A constant for the END_BINDING segment. */ 191 private static final Segment END_BINDING = create(SegmentKind.END_BINDING, ""); 192 193 /** Creates a segment of given kind and value. */ create(SegmentKind kind, String value)194 private static Segment create(SegmentKind kind, String value) { 195 return new AutoValue_PathTemplate_Segment(kind, value, ""); 196 } 197 198 /** Creates a segment of given kind, value, and complex separator. */ create(SegmentKind kind, String value, String complexSeparator)199 private static Segment create(SegmentKind kind, String value, String complexSeparator) { 200 return new AutoValue_PathTemplate_Segment(kind, value, complexSeparator); 201 } 202 wildcardCreate(String complexSeparator)203 private static Segment wildcardCreate(String complexSeparator) { 204 return new AutoValue_PathTemplate_Segment( 205 SegmentKind.WILDCARD, 206 "*", 207 !complexSeparator.isEmpty() && COMPLEX_DELIMITER_PATTERN.matcher(complexSeparator).find() 208 ? complexSeparator 209 : ""); 210 } 211 212 /** The path segment kind. */ kind()213 abstract SegmentKind kind(); 214 215 /** 216 * The value for the segment. For literals, custom verbs, and wildcards, this reflects the value 217 * as it appears in the template. For bindings, this represents the variable of the binding. 218 */ value()219 abstract String value(); 220 complexSeparator()221 abstract String complexSeparator(); 222 223 /** Returns true of this segment is one of the wildcards, */ isAnyWildcard()224 boolean isAnyWildcard() { 225 return kind() == SegmentKind.WILDCARD || kind() == SegmentKind.PATH_WILDCARD; 226 } 227 separator()228 String separator() { 229 switch (kind()) { 230 case CUSTOM_VERB: 231 return ":"; 232 case END_BINDING: 233 return ""; 234 default: 235 return "/"; 236 } 237 } 238 } 239 240 /** 241 * Creates a path template from a string. The string must satisfy the syntax of path templates of 242 * the API platform; see HttpRule's proto source. 243 * 244 * @throws ValidationException if there are errors while parsing the template. 245 */ create(String template)246 public static PathTemplate create(String template) { 247 return create(template, true); 248 } 249 250 /** 251 * Creates a path template from a string. The string must satisfy the syntax of path templates of 252 * the API platform; see HttpRule's proto source. Url encoding of template variables is disabled. 253 * 254 * @throws ValidationException if there are errors while parsing the template. 255 */ createWithoutUrlEncoding(String template)256 public static PathTemplate createWithoutUrlEncoding(String template) { 257 return create(template, false); 258 } 259 create(String template, boolean urlEncoding)260 private static PathTemplate create(String template, boolean urlEncoding) { 261 return new PathTemplate(parseTemplate(template), urlEncoding); 262 } 263 264 // Instance State and Methods 265 // ========================== 266 267 // List of segments of this template. 268 private final ImmutableList<Segment> segments; 269 270 // Map from variable names to bindings in the template. 271 private final ImmutableMap<String, Segment> bindings; 272 273 // Control use of URL encoding 274 private final boolean urlEncoding; 275 PathTemplate(Iterable<Segment> segments, boolean urlEncoding)276 private PathTemplate(Iterable<Segment> segments, boolean urlEncoding) { 277 this.segments = ImmutableList.copyOf(segments); 278 if (this.segments.isEmpty()) { 279 throw new ValidationException("template cannot be empty."); 280 } 281 Map<String, Segment> bindings = Maps.newLinkedHashMap(); 282 for (Segment seg : this.segments) { 283 if (seg.kind() == SegmentKind.BINDING) { 284 if (bindings.containsKey(seg.value())) { 285 throw new ValidationException("Duplicate binding '%s'", seg.value()); 286 } 287 bindings.put(seg.value(), seg); 288 } 289 } 290 this.bindings = ImmutableMap.copyOf(bindings); 291 this.urlEncoding = urlEncoding; 292 } 293 294 /** Returns the set of variable names used in the template. */ vars()295 public Set<String> vars() { 296 return bindings.keySet(); 297 } 298 299 /** 300 * Returns a template for the parent of this template. 301 * 302 * @throws ValidationException if the template has no parent. 303 */ parentTemplate()304 public PathTemplate parentTemplate() { 305 int i = segments.size(); 306 Segment seg = segments.get(--i); 307 if (seg.kind() == SegmentKind.END_BINDING) { 308 while (i > 0 && segments.get(--i).kind() != SegmentKind.BINDING) {} 309 } 310 if (i == 0) { 311 throw new ValidationException("template does not have a parent"); 312 } 313 return new PathTemplate(segments.subList(0, i), urlEncoding); 314 } 315 316 /** 317 * Returns a template where all variable bindings have been replaced by wildcards, but which is 318 * equivalent regards matching to this one. 319 */ withoutVars()320 public PathTemplate withoutVars() { 321 StringBuilder result = new StringBuilder(); 322 ListIterator<Segment> iterator = segments.listIterator(); 323 boolean start = true; 324 while (iterator.hasNext()) { 325 Segment seg = iterator.next(); 326 switch (seg.kind()) { 327 case BINDING: 328 case END_BINDING: 329 break; 330 default: 331 if (!start) { 332 result.append(seg.separator()); 333 } else { 334 start = false; 335 } 336 result.append(seg.value()); 337 } 338 } 339 return create(result.toString(), urlEncoding); 340 } 341 342 /** 343 * Returns a path template for the sub-path of the given variable. Example: 344 * 345 * <pre>{@code 346 * PathTemplate template = PathTemplate.create("v1/{name=shelves/*/books/*}"); 347 * assert template.subTemplate("name").toString().equals("shelves/*/books/*"); 348 * }</pre> 349 * 350 * The returned template will never have named variables, but only wildcards, which are dealt with 351 * in matching and instantiation using '$n'-variables. See the documentation of {@link 352 * #match(String)} and {@link #instantiate(Map)}, respectively. 353 * 354 * <p>For a variable which has no sub-path, this returns a path template with a single wildcard 355 * ('*'). 356 * 357 * @throws ValidationException if the variable does not exist in the template. 358 */ subTemplate(String varName)359 public PathTemplate subTemplate(String varName) { 360 List<Segment> sub = Lists.newArrayList(); 361 boolean inBinding = false; 362 for (Segment seg : segments) { 363 if (seg.kind() == SegmentKind.BINDING && seg.value().equals(varName)) { 364 inBinding = true; 365 } else if (inBinding) { 366 if (seg.kind() == SegmentKind.END_BINDING) { 367 return PathTemplate.create(toSyntax(sub, true), urlEncoding); 368 } else { 369 sub.add(seg); 370 } 371 } 372 } 373 throw new ValidationException( 374 String.format("Variable '%s' is undefined in template '%s'", varName, this.toRawString())); 375 } 376 377 /** Returns true of this template ends with a literal. */ endsWithLiteral()378 public boolean endsWithLiteral() { 379 return segments.get(segments.size() - 1).kind() == SegmentKind.LITERAL; 380 } 381 382 /** Returns true of this template ends with a custom verb. */ endsWithCustomVerb()383 public boolean endsWithCustomVerb() { 384 return segments.get(segments.size() - 1).kind() == SegmentKind.CUSTOM_VERB; 385 } 386 387 /** 388 * Creates a resource name from this template and a path. 389 * 390 * @throws ValidationException if the path does not match the template. 391 */ parse(String path)392 public TemplatedResourceName parse(String path) { 393 return TemplatedResourceName.create(this, path); 394 } 395 396 /** 397 * Returns the name of a singleton variable used by this template. If the template does not 398 * contain a single variable, returns null. 399 */ 400 @Nullable singleVar()401 public String singleVar() { 402 if (bindings.size() == 1) { 403 return bindings.entrySet().iterator().next().getKey(); 404 } 405 return null; 406 } 407 408 // Template Matching 409 // ================= 410 411 /** 412 * Throws a ValidationException if the template doesn't match the path. The exceptionMessagePrefix 413 * parameter will be prepended to the ValidationException message. 414 */ validate(String path, String exceptionMessagePrefix)415 public void validate(String path, String exceptionMessagePrefix) { 416 if (!matches(path)) { 417 throw new ValidationException( 418 String.format( 419 "%s: Parameter \"%s\" must be in the form \"%s\"", 420 exceptionMessagePrefix, path, this.toString())); 421 } 422 } 423 424 /** 425 * Matches the path, returning a map from variable names to matched values. All matched values 426 * will be properly unescaped using URL encoding rules. If the path does not match the template, 427 * throws a ValidationException. The exceptionMessagePrefix parameter will be prepended to the 428 * ValidationException message. 429 * 430 * <p>If the path starts with '//', the first segment will be interpreted as a host name and 431 * stored in the variable {@link #HOSTNAME_VAR}. 432 * 433 * <p>See the {@link PathTemplate} class documentation for examples. 434 * 435 * <p>For free wildcards in the template, the matching process creates variables named '$n', where 436 * 'n' is the wildcard's position in the template (starting at n=0). For example: 437 * 438 * <pre>{@code 439 * PathTemplate template = PathTemplate.create("shelves/*/books/*"); 440 * Map<String, String> expectedValues = new HashMap<>(); 441 * expectedValues.put("$0", "s1"); 442 * expectedValues.put("$1", "b1"); 443 * assert template.validatedMatch("shelves/s1/books/b2", "User exception string") 444 * .equals(expectedValues); 445 * expectedValues.clear(); 446 * expectedValues.put(HOSTNAME_VAR, "//somewhere.io"); 447 * expectedValues.put("$0", "s1"); 448 * expectedValues.put("$1", "b1"); 449 * assert template.validatedMatch("//somewhere.io/shelves/s1/books/b2", "User exception string") 450 * .equals(expectedValues); 451 * }</pre> 452 * 453 * All matched values will be properly unescaped using URL encoding rules (so long as URL encoding 454 * has not been disabled by the {@link #createWithoutUrlEncoding} method). 455 */ validatedMatch(String path, String exceptionMessagePrefix)456 public Map<String, String> validatedMatch(String path, String exceptionMessagePrefix) { 457 Map<String, String> matchMap = match(path); 458 if (matchMap == null) { 459 throw new ValidationException( 460 String.format( 461 "%s: Parameter \"%s\" must be in the form \"%s\"", 462 exceptionMessagePrefix, path, this.toString())); 463 } 464 return matchMap; 465 } 466 467 /** Returns true if the template matches the path. */ matches(String path)468 public boolean matches(String path) { 469 return match(path) != null; 470 } 471 472 /** 473 * Matches the path, returning a map from variable names to matched values. All matched values 474 * will be properly unescaped using URL encoding rules. If the path does not match the template, 475 * null is returned. 476 * 477 * <p>If the path starts with '//', the first segment will be interpreted as a host name and 478 * stored in the variable {@link #HOSTNAME_VAR}. 479 * 480 * <p>See the {@link PathTemplate} class documentation for examples. 481 * 482 * <p>For free wildcards in the template, the matching process creates variables named '$n', where 483 * 'n' is the wildcard's position in the template (starting at n=0). For example: 484 * 485 * <pre>{@code 486 * PathTemplate template = PathTemplate.create("shelves/*/books/*"); 487 * Map<String, String> expectedValues = new HashMap<>(); 488 * expectedValues.put("$0", "s1"); 489 * expectedValues.put("$1", "b1"); 490 * assert template.match("shelves/s1/books/b2").equals(expectedValues); 491 * expectedValues.clear(); 492 * expectedValues.put(HOSTNAME_VAR, "//somewhere.io"); 493 * expectedValues.put("$0", "s1"); 494 * expectedValues.put("$1", "b1"); 495 * assert template.match("//somewhere.io/shelves/s1/books/b2").equals(expectedValues); 496 * }</pre> 497 * 498 * All matched values will be properly unescaped using URL encoding rules (so long as URL encoding 499 * has not been disabled by the {@link #createWithoutUrlEncoding} method). 500 */ 501 @Nullable match(String path)502 public Map<String, String> match(String path) { 503 return match(path, false); 504 } 505 506 /** 507 * Matches the path, where the first segment is interpreted as the host name regardless of whether 508 * it starts with '//' or not. Example: 509 * 510 * <pre>{@code 511 * Map<String, String> expectedValues = new HashMap<>(); 512 * expectedValues.put(HOSTNAME_VAR, "//somewhere.io"); 513 * expectedValues.put("name", "shelves/s1"); 514 * assert template("{name=shelves/*}").matchFromFullName("somewhere.io/shelves/s1") 515 * .equals(expectedValues); 516 * }</pre> 517 */ 518 @Nullable matchFromFullName(String path)519 public Map<String, String> matchFromFullName(String path) { 520 return match(path, true); 521 } 522 523 // Matches a path. match(String path, boolean forceHostName)524 private Map<String, String> match(String path, boolean forceHostName) { 525 // Quick check for trailing custom verb. 526 Segment last = segments.get(segments.size() - 1); 527 if (last.kind() == SegmentKind.CUSTOM_VERB) { 528 Matcher matcher = CUSTOM_VERB_PATTERN.matcher(path); 529 if (!matcher.find() || !decodeUrl(matcher.group(1)).equals(last.value())) { 530 return null; 531 } 532 path = path.substring(0, matcher.start(0)); 533 } 534 535 Matcher matcher = HOSTNAME_PATTERN.matcher(path); 536 boolean withHostName = matcher.find(); 537 if (withHostName) { 538 path = matcher.replaceFirst(""); 539 } 540 List<String> input = SLASH_SPLITTER.splitToList(path); 541 int inPos = 0; 542 Map<String, String> values = Maps.newLinkedHashMap(); 543 if (withHostName || forceHostName) { 544 if (input.isEmpty()) { 545 return null; 546 } 547 String hostName = input.get(inPos++); 548 if (withHostName) { 549 // Put the // back, so we can distinguish this case from forceHostName. 550 hostName = matcher.group(0) + hostName; 551 } 552 values.put(HOSTNAME_VAR, hostName); 553 } 554 if (withHostName) { 555 inPos = alignInputToAlignableSegment(input, inPos, segments.get(0)); 556 } 557 if (!match(input, inPos, segments, 0, values)) { 558 return null; 559 } 560 return ImmutableMap.copyOf(values); 561 } 562 563 // Aligns input to start of literal value of literal or binding segment if input contains 564 // hostname. alignInputToAlignableSegment(List<String> input, int inPos, Segment segment)565 private int alignInputToAlignableSegment(List<String> input, int inPos, Segment segment) { 566 switch (segment.kind()) { 567 case BINDING: 568 inPos = alignInputPositionToLiteral(input, inPos, segment.value() + "s"); 569 return inPos + 1; 570 case LITERAL: 571 return alignInputPositionToLiteral(input, inPos, segment.value()); 572 } 573 return inPos; 574 } 575 576 // Aligns input to start of literal value if input contains hostname. alignInputPositionToLiteral( List<String> input, int inPos, String literalSegmentValue)577 private int alignInputPositionToLiteral( 578 List<String> input, int inPos, String literalSegmentValue) { 579 for (; inPos < input.size(); inPos++) { 580 if (literalSegmentValue.equals(input.get(inPos))) { 581 return inPos; 582 } 583 } 584 return inPos; 585 } 586 587 // Tries to match the input based on the segments at given positions. Returns a boolean 588 // indicating whether the match was successful. match( List<String> input, int inPos, List<Segment> segments, int segPos, Map<String, String> values)589 private boolean match( 590 List<String> input, 591 int inPos, 592 List<Segment> segments, 593 int segPos, 594 Map<String, String> values) { 595 String currentVar = null; 596 List<String> modifiableInput = new ArrayList<>(input); 597 while (segPos < segments.size()) { 598 Segment seg = segments.get(segPos++); 599 switch (seg.kind()) { 600 case END_BINDING: 601 // End current variable binding scope. 602 currentVar = null; 603 continue; 604 case BINDING: 605 // Start variable binding scope. 606 currentVar = seg.value(); 607 continue; 608 case CUSTOM_VERB: 609 // This is the final segment, and this check should have already been performed by the 610 // caller. The matching value is no longer present in the input. 611 break; 612 case LITERAL: 613 case WILDCARD: 614 if (inPos >= modifiableInput.size()) { 615 // End of input 616 return false; 617 } 618 // Check literal match. 619 String next = decodeUrl(modifiableInput.get(inPos++)); 620 if (seg.kind() == SegmentKind.LITERAL) { 621 if (!seg.value().equals(next)) { 622 // Literal does not match. 623 return false; 624 } 625 } 626 if (seg.kind() == SegmentKind.WILDCARD && !seg.complexSeparator().isEmpty()) { 627 // Parse the complex resource separators one by one. 628 int complexSeparatorIndex = next.indexOf(seg.complexSeparator()); 629 if (complexSeparatorIndex >= 0) { 630 modifiableInput.add(inPos, next.substring(complexSeparatorIndex + 1)); 631 next = next.substring(0, complexSeparatorIndex); 632 modifiableInput.set(inPos - 1, next); 633 } else { 634 // No complex resource ID separator found in the literal when we expected one. 635 return false; 636 } 637 } 638 if (currentVar != null) { 639 // Create or extend current match 640 values.put(currentVar, concatCaptures(values.get(currentVar), next)); 641 } 642 break; 643 case PATH_WILDCARD: 644 // Compute the number of additional input the ** can consume. This 645 // is possible because we restrict patterns to have only one **. 646 int segsToMatch = 0; 647 for (int i = segPos; i < segments.size(); i++) { 648 switch (segments.get(i).kind()) { 649 case BINDING: 650 case END_BINDING: 651 case CUSTOM_VERB: 652 // These segments do not actually consume any input. 653 continue; 654 default: 655 segsToMatch++; 656 } 657 } 658 int available = (modifiableInput.size() - inPos) - segsToMatch; 659 // If this segment is empty, make sure it is still captured. 660 if (available == 0 && !values.containsKey(currentVar)) { 661 values.put(currentVar, ""); 662 } 663 while (available-- > 0) { 664 values.put( 665 currentVar, 666 concatCaptures(values.get(currentVar), decodeUrl(modifiableInput.get(inPos++)))); 667 } 668 } 669 } 670 return inPos == modifiableInput.size(); 671 } 672 concatCaptures(@ullable String cur, String next)673 private static String concatCaptures(@Nullable String cur, String next) { 674 return cur == null ? next : cur + "/" + next; 675 } 676 677 // Template Instantiation 678 // ====================== 679 680 /** 681 * Instantiate the template based on the given variable assignment. Performs proper URL escaping 682 * of variable assignments. 683 * 684 * <p>Note that free wildcards in the template must have bindings of '$n' variables, where 'n' is 685 * the position of the wildcard (starting at 0). See the documentation of {@link #match(String)} 686 * for details. 687 * 688 * @throws ValidationException if a variable occurs in the template without a binding. 689 */ instantiate(Map<String, String> values)690 public String instantiate(Map<String, String> values) { 691 return instantiate(values, false); 692 } 693 694 /** Shortcut for {@link #instantiate(Map)} with a vararg parameter for keys and values. */ instantiate(String... keysAndValues)695 public String instantiate(String... keysAndValues) { 696 ImmutableMap.Builder<String, String> builder = ImmutableMap.builder(); 697 for (int i = 0; i < keysAndValues.length; i += 2) { 698 builder.put(keysAndValues[i], keysAndValues[i + 1]); 699 } 700 return instantiate(builder.build()); 701 } 702 703 /** 704 * Same like {@link #instantiate(Map)} but allows for unbound variables, which are substituted 705 * using their original syntax. Example: 706 * 707 * <pre>{@code 708 * PathTemplate template = PathTemplate.create("v1/shelves/{shelf}/books/{book}"); 709 * Map<String, String> partialMap = new HashMap<>(); 710 * partialMap.put("shelf", "s1"); 711 * assert template.instantiatePartial(partialMap).equals("v1/shelves/s1/books/{book}"); 712 * }</pre> 713 * 714 * The result of this call can be used to create a new template. 715 */ instantiatePartial(Map<String, String> values)716 public String instantiatePartial(Map<String, String> values) { 717 return instantiate(values, true); 718 } 719 instantiate(Map<String, String> values, boolean allowPartial)720 private String instantiate(Map<String, String> values, boolean allowPartial) { 721 StringBuilder result = new StringBuilder(); 722 if (values.containsKey(HOSTNAME_VAR)) { 723 result.append(values.get(HOSTNAME_VAR)); 724 result.append('/'); 725 } 726 boolean continueLast = true; // Whether to not append separator 727 boolean skip = false; // Whether we are substituting a binding and segments shall be skipped. 728 ListIterator<Segment> iterator = segments.listIterator(); 729 String prevSeparator = ""; 730 while (iterator.hasNext()) { 731 Segment seg = iterator.next(); 732 if (!skip && !continueLast) { 733 String separator = 734 prevSeparator.isEmpty() || !iterator.hasNext() ? seg.separator() : prevSeparator; 735 result.append(separator); 736 prevSeparator = seg.complexSeparator().isEmpty() ? seg.separator() : seg.complexSeparator(); 737 } 738 continueLast = false; 739 switch (seg.kind()) { 740 case BINDING: 741 String var = seg.value(); 742 String value = values.get(seg.value()); 743 if (value == null) { 744 if (!allowPartial) { 745 throw new ValidationException( 746 String.format("Unbound variable '%s'. Bindings: %s", var, values)); 747 } 748 // Append pattern to output 749 if (var.startsWith("$")) { 750 // Eliminate positional variable. 751 result.append(iterator.next().value()); 752 iterator.next(); 753 continue; 754 } 755 result.append('{'); 756 result.append(seg.value()); 757 result.append('='); 758 continueLast = true; 759 continue; 760 } 761 Segment next = iterator.next(); 762 Segment nextNext = iterator.next(); 763 boolean pathEscape = 764 next.kind() == SegmentKind.PATH_WILDCARD 765 || nextNext.kind() != SegmentKind.END_BINDING; 766 restore(iterator, iterator.nextIndex() - 2); 767 if (!pathEscape) { 768 result.append(encodeUrl(value)); 769 } else { 770 // For a path wildcard or path of length greater 1, split the value and escape 771 // every sub-segment. 772 boolean first = true; 773 for (String subSeg : SLASH_SPLITTER.split(value)) { 774 if (!first) { 775 result.append('/'); 776 } 777 first = false; 778 result.append(encodeUrl(subSeg)); 779 } 780 } 781 skip = true; 782 continue; 783 case END_BINDING: 784 if (!skip) { 785 result.append('}'); 786 } 787 skip = false; 788 continue; 789 default: 790 if (!skip) { 791 result.append(seg.value()); 792 } 793 } 794 } 795 return result.toString(); 796 } 797 798 // Positional Matching and Instantiation 799 // ===================================== 800 801 /** 802 * Instantiates the template from the given positional parameters. The template must not be build 803 * from named bindings, but only contain wildcards. Each parameter position corresponds to a 804 * wildcard of the according position in the template. 805 */ encode(String... values)806 public String encode(String... values) { 807 ImmutableMap.Builder<String, String> builder = ImmutableMap.builder(); 808 int i = 0; 809 for (String value : values) { 810 builder.put("$" + i++, value); 811 } 812 // We will get an error if there are named bindings which are not reached by values. 813 return instantiate(builder.build()); 814 } 815 816 /** 817 * Matches the template into a list of positional values. The template must not be build from 818 * named bindings, but only contain wildcards. For each wildcard in the template, a value is 819 * returned at corresponding position in the list. 820 */ decode(String path)821 public List<String> decode(String path) { 822 Map<String, String> match = match(path); 823 if (match == null) { 824 throw new IllegalArgumentException( 825 String.format("template '%s' does not match '%s'", this, path)); 826 } 827 List<String> result = Lists.newArrayList(); 828 for (Map.Entry<String, String> entry : match.entrySet()) { 829 String key = entry.getKey(); 830 if (!key.startsWith("$")) { 831 throw new IllegalArgumentException("template must not contain named bindings"); 832 } 833 int i = Integer.parseInt(key.substring(1)); 834 while (result.size() <= i) { 835 result.add(""); 836 } 837 result.set(i, entry.getValue()); 838 } 839 return ImmutableList.copyOf(result); 840 } 841 842 // Template Parsing 843 // ================ 844 parseTemplate(String template)845 private static ImmutableList<Segment> parseTemplate(String template) { 846 // Skip useless leading slash. 847 if (template.startsWith("/")) { 848 template = template.substring(1); 849 } 850 851 // Extract trailing custom verb. 852 Matcher matcher = CUSTOM_VERB_PATTERN.matcher(template); 853 String customVerb = null; 854 if (matcher.find()) { 855 customVerb = matcher.group(1); 856 template = template.substring(0, matcher.start(0)); 857 } 858 859 ImmutableList.Builder<Segment> builder = ImmutableList.builder(); 860 String varName = null; 861 int freeWildcardCounter = 0; 862 int pathWildCardBound = 0; 863 864 for (String seg : Splitter.on('/').trimResults().split(template)) { 865 // Handle _deleted-topic_ for PubSub. 866 if (seg.equals("_deleted-topic_")) { 867 builder.add(Segment.create(SegmentKind.LITERAL, seg)); 868 continue; 869 } 870 871 boolean isLastSegment = (template.indexOf(seg) + seg.length()) == template.length(); 872 boolean isCollectionWildcard = !isLastSegment && (seg.equals("-") || seg.equals("-}")); 873 if (!isCollectionWildcard && isSegmentBeginOrEndInvalid(seg)) { 874 throw new ValidationException("parse error: invalid begin or end character in '%s'", seg); 875 } 876 // Disallow zero or multiple delimiters between variable names. 877 if (MULTIPLE_COMPLEX_DELIMITER_PATTERN.matcher(seg).find() 878 || MISSING_COMPLEX_DELIMITER_PATTERN.matcher(seg).find()) { 879 throw new ValidationException( 880 "parse error: missing or 2+ consecutive delimiter characters in '%s'", seg); 881 } 882 // If segment starts with '{', a binding group starts. 883 boolean bindingStarts = seg.startsWith("{"); 884 boolean implicitWildcard = false; 885 boolean complexDelimiterFound = false; 886 if (bindingStarts) { 887 if (varName != null) { 888 throw new ValidationException("parse error: nested binding in '%s'", template); 889 } 890 seg = seg.substring(1); 891 892 // Check for invalid complex resource ID delimiters. 893 if (INVALID_COMPLEX_DELIMITER_PATTERN.matcher(seg).find()) { 894 throw new ValidationException( 895 "parse error: invalid complex resource ID delimiter character in '%s'", seg); 896 } 897 898 Matcher complexPatternDelimiterMatcher = END_SEGMENT_COMPLEX_DELIMITER_PATTERN.matcher(seg); 899 complexDelimiterFound = !isCollectionWildcard && complexPatternDelimiterMatcher.find(); 900 901 // Look for complex resource names. 902 // Need to handle something like "{user_a}~{user_b}". 903 if (complexDelimiterFound) { 904 builder.addAll(parseComplexResourceId(seg)); 905 } else { 906 int i = seg.indexOf('='); 907 if (i <= 0) { 908 // Possibly looking at something like "{name}" with implicit wildcard. 909 if (seg.endsWith("}")) { 910 // Remember to add an implicit wildcard later. 911 implicitWildcard = true; 912 varName = seg.substring(0, seg.length() - 1).trim(); 913 seg = seg.substring(seg.length() - 1).trim(); 914 } else { 915 throw new ValidationException( 916 "parse error: invalid binding syntax in '%s'", template); 917 } 918 } else if (seg.indexOf('-') <= 0 && isCollectionWildcard) { 919 implicitWildcard = true; 920 } else { 921 // Looking at something like "{name=wildcard}". 922 varName = seg.substring(0, i).trim(); 923 seg = seg.substring(i + 1).trim(); 924 } 925 builder.add(Segment.create(SegmentKind.BINDING, varName)); 926 } 927 } 928 929 if (!complexDelimiterFound) { 930 // If segment ends with '}', a binding group ends. Remove the brace and remember. 931 boolean bindingEnds = seg.endsWith("}"); 932 if (bindingEnds) { 933 seg = seg.substring(0, seg.length() - 1).trim(); 934 } 935 936 // Process the segment, after stripping off "{name=.." and "..}". 937 switch (seg) { 938 case "**": 939 case "*": 940 if ("**".equals(seg)) { 941 pathWildCardBound++; 942 } 943 Segment wildcard = seg.length() == 2 ? Segment.PATH_WILDCARD : Segment.WILDCARD; 944 if (varName == null) { 945 // Not in a binding, turn wildcard into implicit binding. 946 // "*" => "{$n=*}" 947 builder.add(Segment.create(SegmentKind.BINDING, "$" + freeWildcardCounter)); 948 freeWildcardCounter++; 949 builder.add(wildcard); 950 builder.add(Segment.END_BINDING); 951 } else { 952 builder.add(wildcard); 953 } 954 break; 955 case "": 956 if (!bindingEnds) { 957 throw new ValidationException( 958 "parse error: empty segment not allowed in '%s'", template); 959 } 960 // If the wildcard is implicit, seg will be empty. Just continue. 961 break; 962 case "-": 963 builder.add(Segment.WILDCARD); 964 implicitWildcard = false; 965 break; 966 default: 967 builder.add(Segment.create(SegmentKind.LITERAL, seg)); 968 } 969 970 // End a binding. 971 if (bindingEnds && !complexDelimiterFound) { 972 // Reset varName to null for next binding. 973 varName = null; 974 975 if (implicitWildcard) { 976 // Looking at something like "{var}". Insert an implicit wildcard, as it is the same 977 // as "{var=*}". 978 builder.add(Segment.WILDCARD); 979 } 980 builder.add(Segment.END_BINDING); 981 } 982 983 if (pathWildCardBound > 1) { 984 // Report restriction on number of '**' in the pattern. There can be only one, which 985 // enables non-backtracking based matching. 986 throw new ValidationException( 987 "parse error: pattern must not contain more than one path wildcard ('**') in '%s'", 988 template); 989 } 990 } 991 } 992 993 if (customVerb != null) { 994 builder.add(Segment.create(SegmentKind.CUSTOM_VERB, customVerb)); 995 } 996 return builder.build(); 997 } 998 isSegmentBeginOrEndInvalid(String seg)999 private static boolean isSegmentBeginOrEndInvalid(String seg) { 1000 // A segment is invalid if it contains only one character and the character is a delimiter 1001 if (seg.length() == 1 && COMPLEX_DELIMITER_PATTERN.matcher(seg).find()) { 1002 return true; 1003 } 1004 // A segment can start with a delimiter, as long as there is no { right after it. 1005 // A segment can end with a delimiter, as long as there is no } right before it. 1006 // e.g. Invalid: .{well}-{known}, {well}-{known}- 1007 // Valid: .well-known, .well-{known}, .-~{well-known}, these segments are all considered as 1008 // literals for matching 1009 return (COMPLEX_DELIMITER_PATTERN.matcher(seg.substring(0, 1)).find() && seg.charAt(1) == '{') 1010 || (COMPLEX_DELIMITER_PATTERN.matcher(seg.substring(seg.length() - 1)).find() 1011 && seg.charAt(seg.length() - 2) == '}'); 1012 } 1013 parseComplexResourceId(String seg)1014 private static List<Segment> parseComplexResourceId(String seg) { 1015 List<Segment> segments = new ArrayList<>(); 1016 List<String> separatorIndices = new ArrayList<>(); 1017 1018 Matcher complexPatternDelimiterMatcher = END_SEGMENT_COMPLEX_DELIMITER_PATTERN.matcher(seg); 1019 boolean delimiterFound = complexPatternDelimiterMatcher.find(); 1020 1021 while (delimiterFound) { 1022 int delimiterIndex = complexPatternDelimiterMatcher.start(); 1023 if (seg.substring(delimiterIndex).startsWith("}")) { 1024 delimiterIndex += 1; 1025 } 1026 String currDelimiter = seg.substring(delimiterIndex, delimiterIndex + 1); 1027 if (!COMPLEX_DELIMITER_PATTERN.matcher(currDelimiter).find()) { 1028 throw new ValidationException( 1029 "parse error: invalid complex ID delimiter '%s' in '%s'", currDelimiter, seg); 1030 } 1031 separatorIndices.add(currDelimiter); 1032 delimiterFound = complexPatternDelimiterMatcher.find(delimiterIndex + 1); 1033 } 1034 // The last entry does not have a delimiter. 1035 separatorIndices.add(""); 1036 1037 String subVarName = null; 1038 Iterable<String> complexSubsegments = 1039 Splitter.onPattern("\\}[_\\-\\.~]").trimResults().split(seg); 1040 boolean complexSegImplicitWildcard = false; 1041 int currIteratorIndex = 0; 1042 for (String complexSeg : complexSubsegments) { 1043 boolean subsegmentBindingStarts = complexSeg.startsWith("{"); 1044 if (subsegmentBindingStarts) { 1045 if (subVarName != null) { 1046 throw new ValidationException("parse error: nested binding in '%s'", complexSeg); 1047 } 1048 complexSeg = complexSeg.substring(1); 1049 } 1050 subVarName = complexSeg.trim(); 1051 1052 boolean subBindingEnds = complexSeg.endsWith("}"); 1053 int i = complexSeg.indexOf('='); 1054 if (i <= 0) { 1055 // Possibly looking at something like "{name}" with implicit wildcard. 1056 if (subBindingEnds) { 1057 // Remember to add an implicit wildcard later. 1058 complexSegImplicitWildcard = true; 1059 subVarName = complexSeg.substring(0, complexSeg.length() - 1).trim(); 1060 complexSeg = complexSeg.substring(complexSeg.length() - 1).trim(); 1061 } 1062 } else { 1063 // Looking at something like "{name=wildcard}". 1064 subVarName = complexSeg.substring(0, i).trim(); 1065 complexSeg = complexSeg.substring(i + 1).trim(); 1066 if (complexSeg.equals("**")) { 1067 throw new ValidationException( 1068 "parse error: wildcard path not allowed in complex ID resource '%s'", subVarName); 1069 } 1070 } 1071 String complexDelimiter = 1072 currIteratorIndex < separatorIndices.size() 1073 ? separatorIndices.get(currIteratorIndex) 1074 : ""; 1075 segments.add(Segment.create(SegmentKind.BINDING, subVarName, complexDelimiter)); 1076 segments.add(Segment.wildcardCreate(complexDelimiter)); 1077 segments.add(Segment.END_BINDING); 1078 subVarName = null; 1079 1080 currIteratorIndex++; 1081 } 1082 return segments; 1083 } 1084 1085 // Helpers 1086 // ======= 1087 1088 private String encodeUrl(String text) { 1089 if (urlEncoding) { 1090 try { 1091 return URLEncoder.encode(text, "UTF-8"); 1092 } catch (UnsupportedEncodingException e) { 1093 throw new ValidationException("UTF-8 encoding is not supported on this platform"); 1094 } 1095 } else { 1096 // When encoding is disabled, we accept any character except '/' 1097 final String INVALID_CHAR = "/"; 1098 if (text.contains(INVALID_CHAR)) { 1099 throw new ValidationException( 1100 "Invalid character \"" + INVALID_CHAR + "\" in path section \"" + text + "\"."); 1101 } 1102 return text; 1103 } 1104 } 1105 1106 private String decodeUrl(String url) { 1107 if (urlEncoding) { 1108 try { 1109 return URLDecoder.decode(url, "UTF-8"); 1110 } catch (UnsupportedEncodingException e) { 1111 throw new ValidationException("UTF-8 encoding is not supported on this platform"); 1112 } 1113 } else { 1114 return url; 1115 } 1116 } 1117 1118 // Checks for the given segments kind. On success, consumes them. Otherwise leaves 1119 // the list iterator in its state. 1120 private static boolean peek(ListIterator<Segment> segments, SegmentKind... kinds) { 1121 int start = segments.nextIndex(); 1122 boolean success = false; 1123 for (SegmentKind kind : kinds) { 1124 if (!segments.hasNext() || segments.next().kind() != kind) { 1125 success = false; 1126 break; 1127 } 1128 } 1129 if (success) { 1130 return true; 1131 } 1132 restore(segments, start); 1133 return false; 1134 } 1135 1136 // Restores a list iterator back to a given index. 1137 private static void restore(ListIterator<?> segments, int index) { 1138 while (segments.nextIndex() > index) { 1139 segments.previous(); 1140 } 1141 } 1142 1143 // Equality and String Conversion 1144 // ============================== 1145 1146 /** Returns a pretty version of the template as a string. */ 1147 @Override 1148 public String toString() { 1149 return toSyntax(segments, true); 1150 } 1151 1152 /** 1153 * Returns a raw version of the template as a string. This renders the template in its internal, 1154 * normalized form. 1155 */ 1156 public String toRawString() { 1157 return toSyntax(segments, false); 1158 } 1159 1160 private static String toSyntax(List<Segment> segments, boolean pretty) { 1161 StringBuilder result = new StringBuilder(); 1162 boolean continueLast = true; // if true, no slash is appended. 1163 ListIterator<Segment> iterator = segments.listIterator(); 1164 while (iterator.hasNext()) { 1165 Segment seg = iterator.next(); 1166 if (!continueLast) { 1167 result.append(seg.separator()); 1168 } 1169 continueLast = false; 1170 switch (seg.kind()) { 1171 case BINDING: 1172 if (pretty && seg.value().startsWith("$")) { 1173 // Remove the internal binding. 1174 seg = iterator.next(); // Consume wildcard 1175 result.append(seg.value()); 1176 iterator.next(); // Consume END_BINDING 1177 continue; 1178 } 1179 result.append('{'); 1180 result.append(seg.value()); 1181 if (pretty && peek(iterator, SegmentKind.WILDCARD, SegmentKind.END_BINDING)) { 1182 // Reduce {name=*} to {name}. 1183 result.append('}'); 1184 continue; 1185 } 1186 result.append('='); 1187 continueLast = true; 1188 continue; 1189 case END_BINDING: 1190 result.append('}'); 1191 continue; 1192 default: 1193 result.append(seg.value()); 1194 continue; 1195 } 1196 } 1197 return result.toString(); 1198 } 1199 1200 @Override 1201 public boolean equals(Object obj) { 1202 if (!(obj instanceof PathTemplate)) { 1203 return false; 1204 } 1205 PathTemplate other = (PathTemplate) obj; 1206 return Objects.equals(segments, other.segments); 1207 } 1208 1209 @Override 1210 public int hashCode() { 1211 return segments.hashCode(); 1212 } 1213 } 1214