All Downloads are FREE. Search and download functionalities are using the official Maven repository.

nl.topicus.jdbc.shaded.com.google.api.pathtemplate.PathTemplate Maven / Gradle / Ivy

There is a newer version: 1.1.6
Show newest version
/*
 * Copyright 2016, Google Inc.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *     * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following disclaimer
 * in the documentation and/or other materials provided with the
 * distribution.
 *     * Neither the name of Google Inc. nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package nl.topicus.jdbc.shaded.com.google.api.pathtemplate;

import nl.topicus.jdbc.shaded.com.google.api.core.BetaApi;
import nl.topicus.jdbc.shaded.com.google.auto.value.AutoValue;
import nl.topicus.jdbc.shaded.com.google.common.base.Splitter;
import nl.topicus.jdbc.shaded.com.google.common.collect.ImmutableList;
import nl.topicus.jdbc.shaded.com.google.common.collect.ImmutableMap;
import nl.topicus.jdbc.shaded.com.google.common.collect.Lists;
import nl.topicus.jdbc.shaded.com.google.common.collect.Maps;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import nl.topicus.jdbc.shaded.javax.annotation.Nullable;

/**
 * Represents a path template.
 *
 * 

* Templates use the syntax of the API platform; see the protobuf of HttpRule for details. A * template consists of a sequence of literals, wildcards, and variable bindings, where each binding * can have a sub-path. A string representation can be parsed into an instance of * {@link PathTemplate}, which can then be used to perform matching and instantiation. * *

* Matching and instantiation deals with unescaping and escaping using URL encoding rules. For * example, if a template variable for a single segment is instantiated with a string like * {@code "a/b"}, the slash will be escaped to {@code "%2f"}. (Note that slash will not be escaped * for a multiple-segment variable, but other characters will). The literals in the template itself * are not escaped automatically, and must be already URL encoded. * *

* Here is an example for a template using simple variables: * *

 *   PathTemplate template = PathTemplate.create("v1/shelves/{shelf}/books/{book}");
 *   assert template.matches("v2/shelves") == false;
 *   Map<String, String> values = template.match("v1/shelves/s1/books/b1");
 *   Map<String, String> expectedValues = new HashMap<>();
 *   expectedValues.put("shelf", "s1");
 *   expectedValues.put("book", "b1");
 *   assert values.equals(expectedValues);
 *   assert template.instantiate(values).equals("v1/shelves/s1/books/b1");
 * 
* * Templates can use variables which match sub-paths. Example: * *
 *   PathTemplate template = PathTemplate.create("v1/{name=shelves/*/books/*}"};
 *   assert template.match("v1/shelves/books/b1") == null;
 *   Map<String, String> expectedValues = new HashMap<>();
 *   expectedValues.put("name", "shelves/s1/books/b1");
 *   assert template.match("v1/shelves/s1/books/b1").equals(expectedValues);
 * 
* * Path templates can also be used with only wildcards. Each wildcard is associated with an implicit * variable {@code $n}, where n is the zero-based position of the wildcard. Example: * *
 *   PathTemplate template = PathTemplate.create("shelves/*/books/*"};
 *   assert template.match("shelves/books/b1") == null;
 *   Map<String, String> values = template.match("v1/shelves/s1/books/b1");
 *   Map<String, String> expectedValues = new HashMap<>();
 *   expectedValues.put("$0", s1");
 *   expectedValues.put("$1", "b1");
 *   assert values.equals(expectedValues);
 * 
* * Paths input to matching can use URL relative syntax to indicate a host name by prefixing the host * name, as in {@code //somewhere.io/some/path}. The host name is matched into the special variable * {@link #HOSTNAME_VAR}. Patterns are agnostic about host names, and the same pattern can be used * for URL relative syntax and simple path syntax: * *
 *   PathTemplate template = PathTemplate.create("shelves/*"};
 *   Map<String, String> expectedValues = new HashMap<>();
 *   expectedValues.put(PathTemplate.HOSTNAME_VAR, "//somewhere.io");
 *   expectedValues.put("$0", s1");
 *   assert template.match("//somewhere.io/shelves/s1").equals(expectedValues);
 *   expectedValues.clear();
 *   expectedValues.put("$0", s1");
 *   assert template.match("shelves/s1").equals(expectedValues);
 * 
* * For the representation of a resource name see {@link TemplatedResourceName}, which is * based on path templates. */ @BetaApi public class PathTemplate { /** * A constant identifying the special variable used for endpoint bindings in the result of * {@link #matchFromFullName(String)}. */ public static final String HOSTNAME_VAR = "$hostname"; // A regexp to match a custom verb at the end of a path. private static final Pattern CUSTOM_VERB_PATTERN = Pattern.compile(":([^/*}{=]+)$"); // A splitter on slash. private static final Splitter SLASH_SPLITTER = Splitter.on('/').trimResults(); // Helper Types // ============ /** * Specifies a path segment kind. */ enum SegmentKind { /** A literal path segment. */ LITERAL, /** A custom verb. Can only appear at the end of path. */ CUSTOM_VERB, /** A simple wildcard ('*'). */ WILDCARD, /** A path wildcard ('**'). */ PATH_WILDCARD, /** A field binding start. */ BINDING, /** A field binding end. */ END_BINDING, } /** * Specifies a path segment. */ @AutoValue abstract static class Segment { /** * A constant for the WILDCARD segment. */ private static final Segment WILDCARD = create(SegmentKind.WILDCARD, "*"); /** * A constant for the PATH_WILDCARD segment. */ private static final Segment PATH_WILDCARD = create(SegmentKind.PATH_WILDCARD, "**"); /** * A constant for the END_BINDING segment. */ private static final Segment END_BINDING = create(SegmentKind.END_BINDING, ""); /** * Creates a segment of given kind and value. */ private static Segment create(SegmentKind kind, String value) { return new AutoValue_PathTemplate_Segment(kind, value); } /** * The path segment kind. */ abstract SegmentKind kind(); /** * The value for the segment. For literals, custom verbs, and wildcards, this reflects the value * as it appears in the template. For bindings, this represents the variable of the binding. */ abstract String value(); /** * Returns true of this segment is one of the wildcards, */ boolean isAnyWildcard() { return kind() == SegmentKind.WILDCARD || kind() == SegmentKind.PATH_WILDCARD; } String separator() { switch (kind()) { case CUSTOM_VERB: return ":"; case END_BINDING: return ""; default: return "/"; } } } /** * Creates a path template from a string. The string must satisfy the syntax of path templates of * the API platform; see HttpRule's proto source. * * @throws ValidationException if there are errors while parsing the template. */ public static PathTemplate create(String template) { return create(template, true); } /** * Creates a path template from a string. The string must satisfy the syntax of path templates of * the API platform; see HttpRule's proto source. Url encoding of template variables is disabled. * * @throws ValidationException if there are errors while parsing the template. */ public static PathTemplate createWithoutUrlEncoding(String template) { return create(template, false); } private static PathTemplate create(String template, boolean urlEncoding) { return new PathTemplate(parseTemplate(template), urlEncoding); } // Instance State and Methods // ========================== // List of segments of this template. private final ImmutableList segments; // Map from variable names to bindings in the template. private final ImmutableMap bindings; // Control use of URL encoding private final boolean urlEncoding; private PathTemplate(Iterable segments, boolean urlEncoding) { this.segments = ImmutableList.copyOf(segments); if (this.segments.isEmpty()) { throw new ValidationException("template cannot be empty."); } Map bindings = Maps.newLinkedHashMap(); for (Segment seg : this.segments) { if (seg.kind() == SegmentKind.BINDING) { if (bindings.containsKey(seg.value())) { throw new ValidationException("Duplicate binding '%s'", seg.value()); } bindings.put(seg.value(), seg); } } this.bindings = ImmutableMap.copyOf(bindings); this.urlEncoding = urlEncoding; } /** * Returns the set of variable names used in the template. */ public Set vars() { return bindings.keySet(); } /** * Returns a template for the parent of this template. * * @throws ValidationException if the template has no parent. */ public PathTemplate parentTemplate() { int i = segments.size(); Segment seg = segments.get(--i); if (seg.kind() == SegmentKind.END_BINDING) { while (i > 0 && segments.get(--i).kind() != SegmentKind.BINDING) {} } if (i == 0) { throw new ValidationException("template does not have a parent"); } return new PathTemplate(segments.subList(0, i), urlEncoding); } /** * Returns a template where all variable bindings have been replaced by wildcards, but which is * equivalent regards matching to this one. */ public PathTemplate withoutVars() { StringBuilder result = new StringBuilder(); ListIterator iterator = segments.listIterator(); boolean start = true; while (iterator.hasNext()) { Segment seg = iterator.next(); switch (seg.kind()) { case BINDING: case END_BINDING: break; default: if (!start) { result.append(seg.separator()); } else { start = false; } result.append(seg.value()); } } return create(result.toString(), urlEncoding); } /** * Returns a path template for the sub-path of the given variable. Example: * *
   *   PathTemplate template = PathTemplate.create("v1/{name=shelves/*/books/*}");
   *   assert template.subTemplate("name").toString().equals("shelves/*/books/*");
   * 
* * The returned template will never have named variables, but only wildcards, which are dealt with * in matching and instantiation using '$n'-variables. See the documentation of * {@link #match(String)} and {@link #instantiate(Map)}, respectively. * *

* For a variable which has no sub-path, this returns a path template with a single wildcard * ('*'). * * @throws ValidationException if the variable does not exist in the template. */ public PathTemplate subTemplate(String varName) { List sub = Lists.newArrayList(); boolean inBinding = false; for (Segment seg : segments) { if (seg.kind() == SegmentKind.BINDING && seg.value().equals(varName)) { inBinding = true; } else if (inBinding) { if (seg.kind() == SegmentKind.END_BINDING) { return PathTemplate.create(toSyntax(sub, true), urlEncoding); } else { sub.add(seg); } } } throw new ValidationException( String.format("Variable '%s' is undefined in template '%s'", varName, this.toRawString())); } /** * Returns true of this template ends with a literal. */ public boolean endsWithLiteral() { return segments.get(segments.size() - 1).kind() == SegmentKind.LITERAL; } /** * Returns true of this template ends with a custom verb. */ public boolean endsWithCustomVerb() { return segments.get(segments.size() - 1).kind() == SegmentKind.CUSTOM_VERB; } /** * Creates a resource name from this template and a path. * * @throws ValidationException if the path does not match the template. */ public TemplatedResourceName parse(String path) { return TemplatedResourceName.create(this, path); } /** * Returns the name of a singleton variable used by this template. If the template does not * contain a single variable, returns null. */ @Nullable public String singleVar() { if (bindings.size() == 1) { return bindings.entrySet().iterator().next().getKey(); } return null; } // Template Matching // ================= /** * Throws a ValidationException if the template doesn't match the path. The exceptionMessagePrefix * parameter will be prepended to the ValidationException message. */ public void validate(String path, String exceptionMessagePrefix) { if (!matches(path)) { throw new ValidationException( String.format( "%s: Parameter \"%s\" must be in the form \"%s\"", exceptionMessagePrefix, path, this.toString())); } } /** * Matches the path, returning a map from variable names to matched values. All matched values * will be properly unescaped using URL encoding rules. If the path does not match the template, * throws a ValidationException. The exceptionMessagePrefix parameter will be prepended to the * ValidationException message. * *

* If the path starts with '//', the first segment will be interpreted as a host name and stored * in the variable {@link #HOSTNAME_VAR}. * *

* See the {@link PathTemplate} class documentation for examples. * *

* For free wildcards in the template, the matching process creates variables named '$n', where * 'n' is the wildcard's position in the template (starting at n=0). For example: * *

   *   PathTemplate template = PathTemplate.create("shelves/*/books/*");
   *   Map<String, String> expectedValues = new HashMap<>();
   *   expectedValues.put("$0", "s1");
   *   expectedValues.put("$1", "b1");
   *   assert template.validatedMatch("shelves/s1/books/b2", "User exception string")
   *              .equals(expectedValues);
   *   expectedValues.clear();
   *   expectedValues.put(HOSTNAME_VAR, "//somewhere.io");
   *   expectedValues.put("$0", "s1");
   *   expectedValues.put("$1", "b1");
   *   assert template.validatedMatch("//somewhere.io/shelves/s1/books/b2", "User exception string")
   *              .equals(expectedValues);
   * 
* * All matched values will be properly unescaped using URL encoding rules (so long as URL encoding * has not been disabled by the {@link #createWithoutUrlEncoding} method). */ public Map validatedMatch(String path, String exceptionMessagePrefix) { Map matchMap = match(path); if (matchMap == null) { throw new ValidationException( String.format( "%s: Parameter \"%s\" must be in the form \"%s\"", exceptionMessagePrefix, path, this.toString())); } return matchMap; } /** * Returns true if the template matches the path. */ public boolean matches(String path) { return match(path) != null; } /** * Matches the path, returning a map from variable names to matched values. All matched values * will be properly unescaped using URL encoding rules. If the path does not match the template, * null is returned. * *

* If the path starts with '//', the first segment will be interpreted as a host name and stored * in the variable {@link #HOSTNAME_VAR}. * *

* See the {@link PathTemplate} class documentation for examples. * *

* For free wildcards in the template, the matching process creates variables named '$n', where * 'n' is the wildcard's position in the template (starting at n=0). For example: * *

   *   PathTemplate template = PathTemplate.create("shelves/*/books/*");
   *   Map<String, String> expectedValues = new HashMap<>();
   *   expectedValues.put("$0", "s1");
   *   expectedValues.put("$1", "b1");
   *   assert template.match("shelves/s1/books/b2").equals(expectedValues);
   *   expectedValues.clear();
   *   expectedValues.put(HOSTNAME_VAR, "//somewhere.io");
   *   expectedValues.put("$0", "s1");
   *   expectedValues.put("$1", "b1");
   *   assert template.match("//somewhere.io/shelves/s1/books/b2").equals(expectedValues);
   * 
* * All matched values will be properly unescaped using URL encoding rules (so long as URL encoding * has not been disabled by the {@link #createWithoutUrlEncoding} method). */ @Nullable public Map match(String path) { return match(path, false); } /** * Matches the path, where the first segment is interpreted as the host name regardless of whether * it starts with '//' or not. Example: * *
   *   Map<String, String> expectedValues = new HashMap<>();
   *   expectedValues.put(HOSTNAME_VAR, "//somewhere.io");
   *   expectedValues.put("name", "shelves/s1");
   *   assert template("{name=shelves/*}").matchFromFullName("somewhere.io/shelves/s1")
   *            .equals(expectedValues);
   * 
*/ @Nullable public Map matchFromFullName(String path) { return match(path, true); } // Matches a path. private Map match(String path, boolean forceHostName) { // Quick check for trailing custom verb. Segment last = segments.get(segments.size() - 1); if (last.kind() == SegmentKind.CUSTOM_VERB) { Matcher matcher = CUSTOM_VERB_PATTERN.matcher(path); if (!matcher.find() || !decodeUrl(matcher.group(1)).equals(last.value())) { return null; } path = path.substring(0, matcher.start(0)); } // Do full match. boolean withHostName = path.startsWith("//"); if (withHostName) { path = path.substring(2); } List input = SLASH_SPLITTER.splitToList(path); int inPos = 0; Map values = Maps.newLinkedHashMap(); if (withHostName || forceHostName) { if (input.isEmpty()) { return null; } String hostName = input.get(inPos++); if (withHostName) { // Put the // back, so we can distinguish this case from forceHostName. hostName = "//" + hostName; } values.put(HOSTNAME_VAR, hostName); } if (!match(input, inPos, segments, 0, values)) { return null; } return ImmutableMap.copyOf(values); } // Tries to match the input based on the segments at given positions. Returns a boolean // indicating whether the match was successful. private boolean match( List input, int inPos, List segments, int segPos, Map values) { String currentVar = null; while (segPos < segments.size()) { Segment seg = segments.get(segPos++); switch (seg.kind()) { case END_BINDING: // End current variable binding scope. currentVar = null; continue; case BINDING: // Start variable binding scope. currentVar = seg.value(); continue; case CUSTOM_VERB: // This is the final segment, and this check should have already been performed by the // caller. The matching value is no longer present in the input. break; default: if (inPos >= input.size()) { // End of input return false; } // Check literal match. String next = decodeUrl(input.get(inPos++)); if (seg.kind() == SegmentKind.LITERAL) { if (!seg.value().equals(next)) { // Literal does not match. return false; } } if (currentVar != null) { // Create or extend current match String current = values.get(currentVar); if (current == null) { values.put(currentVar, next); } else { values.put(currentVar, current + "/" + next); } } if (seg.kind() == SegmentKind.PATH_WILDCARD) { // Compute the number of additional input the ** can consume. This // is possible because we restrict patterns to have only one **. int segsToMatch = 0; for (int i = segPos; i < segments.size(); i++) { switch (segments.get(i).kind()) { case BINDING: case END_BINDING: // skip continue; default: segsToMatch++; } } int available = (input.size() - inPos) - segsToMatch; while (available-- > 0) { values.put(currentVar, values.get(currentVar) + "/" + decodeUrl(input.get(inPos++))); } } } } return inPos == input.size(); } // Template Instantiation // ====================== /** * Instantiate the template based on the given variable assignment. Performs proper URL escaping * of variable assignments. * *

* Note that free wildcards in the template must have bindings of '$n' variables, where 'n' is the * position of the wildcard (starting at 0). See the documentation of {@link #match(String)} for * details. * * @throws ValidationException if a variable occurs in the template without a binding. */ public String instantiate(Map values) { return instantiate(values, false); } /** * Shortcut for {@link #instantiate(Map)} with a vararg parameter for keys and values. */ public String instantiate(String... keysAndValues) { ImmutableMap.Builder builder = ImmutableMap.builder(); for (int i = 0; i < keysAndValues.length; i += 2) { builder.put(keysAndValues[i], keysAndValues[i + 1]); } return instantiate(builder.build()); } /** * Same like {@link #instantiate(Map)} but allows for unbound variables, which are substituted * using their original syntax. Example: * *

   *   PathTemplate template = PathTemplate.create("v1/shelves/{shelf}/books/{book}");
   *   Map<String, String> partialMap = new HashMap<>();
   *   partialMap.put("shelf", "s1");
   *   assert template.instantiatePartial(partialMap).equals("v1/shelves/s1/books/{book}");
   * 
* * The result of this call can be used to create a new template. */ public String instantiatePartial(Map values) { return instantiate(values, true); } private String instantiate(Map values, boolean allowPartial) { StringBuilder result = new StringBuilder(); if (values.containsKey(HOSTNAME_VAR)) { result.append(values.get(HOSTNAME_VAR)); result.append('/'); } boolean continueLast = true; // Whether to not append separator boolean skip = false; // Whether we are substituting a binding and segments shall be skipped. ListIterator iterator = segments.listIterator(); while (iterator.hasNext()) { Segment seg = iterator.next(); if (!skip && !continueLast) { result.append(seg.separator()); } continueLast = false; switch (seg.kind()) { case BINDING: String var = seg.value(); String value = values.get(seg.value()); if (value == null) { if (!allowPartial) { throw new ValidationException( String.format("Unbound variable '%s'. Bindings: %s", var, values)); } // Append pattern to output if (var.startsWith("$")) { // Eliminate positional variable. result.append(iterator.next().value()); iterator.next(); continue; } result.append('{'); result.append(seg.value()); result.append('='); continueLast = true; continue; } Segment next = iterator.next(); Segment nextNext = iterator.next(); boolean pathEscape = next.kind() == SegmentKind.PATH_WILDCARD || nextNext.kind() != SegmentKind.END_BINDING; restore(iterator, iterator.nextIndex() - 2); if (!pathEscape) { result.append(encodeUrl(value)); } else { // For a path wildcard or path of length greater 1, split the value and escape // every sub-segment. boolean first = true; for (String subSeg : SLASH_SPLITTER.split(value)) { if (!first) { result.append('/'); } first = false; result.append(encodeUrl(subSeg)); } } skip = true; continue; case END_BINDING: if (!skip) { result.append('}'); } skip = false; continue; default: if (!skip) { result.append(seg.value()); } } } return result.toString(); } // Positional Matching and Instantiation // ===================================== /** * Instantiates the template from the given positional parameters. The template must not be build * from named bindings, but only contain wildcards. Each parameter position corresponds to a * wildcard of the according position in the template. */ public String encode(String... values) { ImmutableMap.Builder builder = ImmutableMap.builder(); int i = 0; for (String value : values) { builder.put("$" + i++, value); } // We will get an error if there are named bindings which are not reached by values. return instantiate(builder.build()); } /** * Matches the template into a list of positional values. The template must not be build from * named bindings, but only contain wildcards. For each wildcard in the template, a value is * returned at corresponding position in the list. */ public List decode(String path) { Map match = match(path); if (match == null) { throw new IllegalArgumentException( String.format("template '%s' does not match '%s'", this, path)); } List result = Lists.newArrayList(); for (Map.Entry entry : match.entrySet()) { String key = entry.getKey(); if (!key.startsWith("$")) { throw new IllegalArgumentException("template must not contain named bindings"); } int i = Integer.parseInt(key.substring(1)); while (result.size() <= i) { result.add(""); } result.set(i, entry.getValue()); } return ImmutableList.copyOf(result); } // Template Parsing // ================ private static ImmutableList parseTemplate(String template) { // Skip useless leading slash. if (template.startsWith("/")) { template = template.substring(1); } // Extract trailing custom verb. Matcher matcher = CUSTOM_VERB_PATTERN.matcher(template); String customVerb = null; if (matcher.find()) { customVerb = matcher.group(1); template = template.substring(0, matcher.start(0)); } ImmutableList.Builder builder = ImmutableList.builder(); String varName = null; int freeWildcardCounter = 0; int pathWildCardBound = 0; for (String seg : Splitter.on('/').trimResults().split(template)) { // If segment starts with '{', a binding group starts. boolean bindingStarts = seg.startsWith("{"); boolean implicitWildcard = false; if (bindingStarts) { if (varName != null) { throw new ValidationException("parse error: nested binding in '%s'", template); } seg = seg.substring(1); int i = seg.indexOf('='); if (i <= 0) { // Possibly looking at something like "{name}" with implicit wildcard. if (seg.endsWith("}")) { // Remember to add an implicit wildcard later. implicitWildcard = true; varName = seg.substring(0, seg.length() - 1).trim(); seg = seg.substring(seg.length() - 1).trim(); } else { throw new ValidationException("parse error: invalid binding syntax in '%s'", template); } } else { // Looking at something like "{name=wildcard}". varName = seg.substring(0, i).trim(); seg = seg.substring(i + 1).trim(); } builder.add(Segment.create(SegmentKind.BINDING, varName)); } // If segment ends with '}', a binding group ends. Remove the brace and remember. boolean bindingEnds = seg.endsWith("}"); if (bindingEnds) { seg = seg.substring(0, seg.length() - 1).trim(); } // Process the segment, after stripping off "{name=.." and "..}". switch (seg) { case "**": case "*": if ("**".equals(seg)) { pathWildCardBound++; } Segment wildcard = seg.length() == 2 ? Segment.PATH_WILDCARD : Segment.WILDCARD; if (varName == null) { // Not in a binding, turn wildcard into implicit binding. // "*" => "{$n=*}" builder.add(Segment.create(SegmentKind.BINDING, "$" + freeWildcardCounter)); freeWildcardCounter++; builder.add(wildcard); builder.add(Segment.END_BINDING); } else { builder.add(wildcard); } break; case "": if (!bindingEnds) { throw new ValidationException( "parse error: empty segment not allowed in '%s'", template); } // If the wildcard is implicit, seg will be empty. Just continue. break; default: builder.add(Segment.create(SegmentKind.LITERAL, seg)); } // End a binding. if (bindingEnds) { // Reset varName to null for next binding. varName = null; if (implicitWildcard) { // Looking at something like "{var}". Insert an implicit wildcard, as it is the same // as "{var=*}". builder.add(Segment.WILDCARD); } builder.add(Segment.END_BINDING); } if (pathWildCardBound > 1) { // Report restriction on number of '**' in the pattern. There can be only one, which // enables non-backtracking based matching. throw new ValidationException( "parse error: pattern must not contain more than one path wildcard ('**') in '%s'", template); } } if (customVerb != null) { builder.add(Segment.create(SegmentKind.CUSTOM_VERB, customVerb)); } return builder.build(); } // Helpers // ======= private String encodeUrl(String text) { if (urlEncoding) { try { return URLEncoder.encode(text, "UTF-8"); } catch (UnsupportedEncodingException e) { throw new ValidationException("UTF-8 encoding is not supported on this platform"); } } else { // When encoding is disabled, we accept any character except '/' final String INVALID_CHAR = "/"; if (text.contains(INVALID_CHAR)) { throw new ValidationException( "Invalid character \"" + INVALID_CHAR + "\" in path section \"" + text + "\"."); } return text; } } private String decodeUrl(String url) { if (urlEncoding) { try { return URLDecoder.decode(url, "UTF-8"); } catch (UnsupportedEncodingException e) { throw new ValidationException("UTF-8 encoding is not supported on this platform"); } } else { return url; } } // Checks for the given segments kind. On success, consumes them. Otherwise leaves // the list iterator in its state. private static boolean peek(ListIterator segments, SegmentKind... kinds) { int start = segments.nextIndex(); boolean success = false; for (SegmentKind kind : kinds) { if (!segments.hasNext() || segments.next().kind() != kind) { success = false; break; } } if (success) { return true; } restore(segments, start); return false; } // Restores a list iterator back to a given index. private static void restore(ListIterator segments, int index) { while (segments.nextIndex() > index) { segments.previous(); } } // Equality and String Conversion // ============================== /** * Returns a pretty version of the template as a string. */ @Override public String toString() { return toSyntax(segments, true); } /** * Returns a raw version of the template as a string. This renders the template in its internal, * normalized form. */ public String toRawString() { return toSyntax(segments, false); } private static String toSyntax(List segments, boolean pretty) { StringBuilder result = new StringBuilder(); boolean continueLast = true; // if true, no slash is appended. ListIterator iterator = segments.listIterator(); while (iterator.hasNext()) { Segment seg = iterator.next(); if (!continueLast) { result.append(seg.separator()); } continueLast = false; switch (seg.kind()) { case BINDING: if (pretty && seg.value().startsWith("$")) { // Remove the internal binding. seg = iterator.next(); // Consume wildcard result.append(seg.value()); iterator.next(); // Consume END_BINDING continue; } result.append('{'); result.append(seg.value()); if (pretty && peek(iterator, SegmentKind.WILDCARD, SegmentKind.END_BINDING)) { // Reduce {name=*} to {name}. result.append('}'); continue; } result.append('='); continueLast = true; continue; case END_BINDING: result.append('}'); continue; default: result.append(seg.value()); continue; } } return result.toString(); } @Override public boolean equals(Object obj) { if (!(obj instanceof PathTemplate)) { return false; } PathTemplate other = (PathTemplate) obj; return Objects.equals(segments, other.segments); } @Override public int hashCode() { return segments.hashCode(); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy