• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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&lt;String, String&gt; values = template.match("v1/shelves/s1/books/b1");
72  * Map&lt;String, String&gt; expectedValues = new HashMap&lt;&gt;();
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/*&#47;books/*}"};
83  * assert template.match("v1/shelves/books/b1") == null;
84  * Map&lt;String, String&gt; expectedValues = new HashMap&lt;&gt;();
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/*&#47;books/*"};
94  * assert template.match("shelves/books/b1") == null;
95  * Map&lt;String, String&gt; values = template.match("v1/shelves/s1/books/b1");
96  * Map&lt;String, String&gt; expectedValues = new HashMap&lt;&gt;();
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&lt;String, String&gt; expectedValues = new HashMap&lt;&gt;();
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/*&#47;books/*}");
347    * assert template.subTemplate("name").toString().equals("shelves/*&#47;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/*&#47;books/*");
440    * Map&lt;String, String&gt; expectedValues = new HashMap&lt;&gt;();
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/*&#47;books/*");
487    * Map&lt;String, String&gt; expectedValues = new HashMap&lt;&gt;();
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&lt;String, String&gt; expectedValues = new HashMap&lt;&gt;();
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&lt;String, String&gt; partialMap = new HashMap&lt;&gt;();
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