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

com.google.api.pathtemplate.PathTemplate Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2016, Google Inc.
 *
 * 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 com.google.api.pathtemplate;

import com.google.auto.value.AutoValue;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.ArrayList;
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 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: * *

{@code
 * 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: * *
{@code
 * 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: * *
{@code
 * 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: * *
{@code
 * 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. */ public class PathTemplate { /** * A constant identifying the special variable used for endpoint bindings in the result of {@link * #matchFromFullName(String)}. It may also contain protocol string, if its provided in the input. */ 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 regex to match a hostname with or without protocol. private static final Pattern HOSTNAME_PATTERN = Pattern.compile("^(\\w+:)?//"); // A splitter on slash. private static final Splitter SLASH_SPLITTER = Splitter.on('/').trimResults(); // A regex to match the valid complex resource ID delimiters. private static final Pattern COMPLEX_DELIMITER_PATTERN = Pattern.compile("[_\\-\\.~]"); // A regex to match multiple complex resource ID delimiters. private static final Pattern MULTIPLE_COMPLEX_DELIMITER_PATTERN = Pattern.compile("\\}[_\\-\\.~]{2,}\\{"); // A regex to match a missing complex resource ID delimiter. private static final Pattern MISSING_COMPLEX_DELIMITER_PATTERN = Pattern.compile("\\}\\{"); // A regex to match invalid complex resource ID delimiters. private static final Pattern INVALID_COMPLEX_DELIMITER_PATTERN = Pattern.compile("\\}[^_\\-\\.~]\\{"); // A regex to match a closing segment (end brace) followed by one complex resource ID delimiter. private static final Pattern END_SEGMENT_COMPLEX_DELIMITER_PATTERN = Pattern.compile("\\}[_\\-\\.~]{1}"); // 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, ""); } /** Creates a segment of given kind, value, and complex separator. */ private static Segment create(SegmentKind kind, String value, String complexSeparator) { return new AutoValue_PathTemplate_Segment(kind, value, complexSeparator); } private static Segment wildcardCreate(String complexSeparator) { return new AutoValue_PathTemplate_Segment( SegmentKind.WILDCARD, "*", !complexSeparator.isEmpty() && COMPLEX_DELIMITER_PATTERN.matcher(complexSeparator).find() ? complexSeparator : ""); } /** 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(); abstract String complexSeparator(); /** 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: * *
{@code
   * 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: * *

{@code
   * 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: * *

{@code
   * 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: * *
{@code
   * 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)); } Matcher matcher = HOSTNAME_PATTERN.matcher(path); boolean withHostName = matcher.find(); if (withHostName) { path = matcher.replaceFirst(""); } 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 = matcher.group(0) + hostName; } values.put(HOSTNAME_VAR, hostName); } if (withHostName) { inPos = alignInputToAlignableSegment(input, inPos, segments.get(0)); } if (!match(input, inPos, segments, 0, values)) { return null; } return ImmutableMap.copyOf(values); } // Aligns input to start of literal value of literal or binding segment if input contains // hostname. private int alignInputToAlignableSegment(List input, int inPos, Segment segment) { switch (segment.kind()) { case BINDING: inPos = alignInputPositionToLiteral(input, inPos, segment.value() + "s"); return inPos + 1; case LITERAL: return alignInputPositionToLiteral(input, inPos, segment.value()); } return inPos; } // Aligns input to start of literal value if input contains hostname. private int alignInputPositionToLiteral( List input, int inPos, String literalSegmentValue) { for (; inPos < input.size(); inPos++) { if (literalSegmentValue.equals(input.get(inPos))) { return inPos; } } return inPos; } // 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; List modifiableInput = new ArrayList<>(input); 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; case LITERAL: case WILDCARD: if (inPos >= modifiableInput.size()) { // End of input return false; } // Check literal match. String next = decodeUrl(modifiableInput.get(inPos++)); if (seg.kind() == SegmentKind.LITERAL) { if (!seg.value().equals(next)) { // Literal does not match. return false; } } if (seg.kind() == SegmentKind.WILDCARD && !seg.complexSeparator().isEmpty()) { // Parse the complex resource separators one by one. int complexSeparatorIndex = next.indexOf(seg.complexSeparator()); if (complexSeparatorIndex >= 0) { modifiableInput.add(inPos, next.substring(complexSeparatorIndex + 1)); next = next.substring(0, complexSeparatorIndex); modifiableInput.set(inPos - 1, next); } else { // No complex resource ID separator found in the literal when we expected one. return false; } } if (currentVar != null) { // Create or extend current match values.put(currentVar, concatCaptures(values.get(currentVar), next)); } break; case 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: case CUSTOM_VERB: // These segments do not actually consume any input. continue; default: segsToMatch++; } } int available = (modifiableInput.size() - inPos) - segsToMatch; // If this segment is empty, make sure it is still captured. if (available == 0 && !values.containsKey(currentVar)) { values.put(currentVar, ""); } while (available-- > 0) { values.put( currentVar, concatCaptures(values.get(currentVar), decodeUrl(modifiableInput.get(inPos++)))); } } } return inPos == modifiableInput.size(); } private static String concatCaptures(@Nullable String cur, String next) { return cur == null ? next : cur + "/" + next; } // 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: * *

{@code
   * 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(); String prevSeparator = ""; while (iterator.hasNext()) { Segment seg = iterator.next(); if (!skip && !continueLast) { String separator = prevSeparator.isEmpty() || !iterator.hasNext() ? seg.separator() : prevSeparator; result.append(separator); prevSeparator = seg.complexSeparator().isEmpty() ? seg.separator() : seg.complexSeparator(); } 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)) { // Handle _deleted-topic_ for PubSub. if (seg.equals("_deleted-topic_")) { builder.add(Segment.create(SegmentKind.LITERAL, seg)); continue; } boolean isLastSegment = (template.indexOf(seg) + seg.length()) == template.length(); boolean isCollectionWildcard = !isLastSegment && (seg.equals("-") || seg.equals("-}")); if (!isCollectionWildcard && isSegmentBeginOrEndInvalid(seg)) { throw new ValidationException("parse error: invalid begin or end character in '%s'", seg); } // Disallow zero or multiple delimiters between variable names. if (MULTIPLE_COMPLEX_DELIMITER_PATTERN.matcher(seg).find() || MISSING_COMPLEX_DELIMITER_PATTERN.matcher(seg).find()) { throw new ValidationException( "parse error: missing or 2+ consecutive delimiter characters in '%s'", seg); } // If segment starts with '{', a binding group starts. boolean bindingStarts = seg.startsWith("{"); boolean implicitWildcard = false; boolean complexDelimiterFound = false; if (bindingStarts) { if (varName != null) { throw new ValidationException("parse error: nested binding in '%s'", template); } seg = seg.substring(1); // Check for invalid complex resource ID delimiters. if (INVALID_COMPLEX_DELIMITER_PATTERN.matcher(seg).find()) { throw new ValidationException( "parse error: invalid complex resource ID delimiter character in '%s'", seg); } Matcher complexPatternDelimiterMatcher = END_SEGMENT_COMPLEX_DELIMITER_PATTERN.matcher(seg); complexDelimiterFound = !isCollectionWildcard && complexPatternDelimiterMatcher.find(); // Look for complex resource names. // Need to handle something like "{user_a}~{user_b}". if (complexDelimiterFound) { builder.addAll(parseComplexResourceId(seg)); } else { 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 if (seg.indexOf('-') <= 0 && isCollectionWildcard) { implicitWildcard = true; } 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 (!complexDelimiterFound) { // 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; case "-": builder.add(Segment.WILDCARD); implicitWildcard = false; break; default: builder.add(Segment.create(SegmentKind.LITERAL, seg)); } // End a binding. if (bindingEnds && !complexDelimiterFound) { // 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(); } private static boolean isSegmentBeginOrEndInvalid(String seg) { // A segment is invalid if it contains only one character and the character is a delimiter if (seg.length() == 1 && COMPLEX_DELIMITER_PATTERN.matcher(seg).find()) { return true; } // A segment can start with a delimiter, as long as there is no { right after it. // A segment can end with a delimiter, as long as there is no } right before it. // e.g. Invalid: .{well}-{known}, {well}-{known}- // Valid: .well-known, .well-{known}, .-~{well-known}, these segments are all considered as // literals for matching return (COMPLEX_DELIMITER_PATTERN.matcher(seg.substring(0, 1)).find() && seg.charAt(1) == '{') || (COMPLEX_DELIMITER_PATTERN.matcher(seg.substring(seg.length() - 1)).find() && seg.charAt(seg.length() - 2) == '}'); } private static List parseComplexResourceId(String seg) { List segments = new ArrayList<>(); List separatorIndices = new ArrayList<>(); Matcher complexPatternDelimiterMatcher = END_SEGMENT_COMPLEX_DELIMITER_PATTERN.matcher(seg); boolean delimiterFound = complexPatternDelimiterMatcher.find(); while (delimiterFound) { int delimiterIndex = complexPatternDelimiterMatcher.start(); if (seg.substring(delimiterIndex).startsWith("}")) { delimiterIndex += 1; } String currDelimiter = seg.substring(delimiterIndex, delimiterIndex + 1); if (!COMPLEX_DELIMITER_PATTERN.matcher(currDelimiter).find()) { throw new ValidationException( "parse error: invalid complex ID delimiter '%s' in '%s'", currDelimiter, seg); } separatorIndices.add(currDelimiter); delimiterFound = complexPatternDelimiterMatcher.find(delimiterIndex + 1); } // The last entry does not have a delimiter. separatorIndices.add(""); String subVarName = null; Iterable complexSubsegments = Splitter.onPattern("\\}[_\\-\\.~]").trimResults().split(seg); boolean complexSegImplicitWildcard = false; int currIteratorIndex = 0; for (String complexSeg : complexSubsegments) { boolean subsegmentBindingStarts = complexSeg.startsWith("{"); if (subsegmentBindingStarts) { if (subVarName != null) { throw new ValidationException("parse error: nested binding in '%s'", complexSeg); } complexSeg = complexSeg.substring(1); } subVarName = complexSeg.trim(); boolean subBindingEnds = complexSeg.endsWith("}"); int i = complexSeg.indexOf('='); if (i <= 0) { // Possibly looking at something like "{name}" with implicit wildcard. if (subBindingEnds) { // Remember to add an implicit wildcard later. complexSegImplicitWildcard = true; subVarName = complexSeg.substring(0, complexSeg.length() - 1).trim(); complexSeg = complexSeg.substring(complexSeg.length() - 1).trim(); } } else { // Looking at something like "{name=wildcard}". subVarName = complexSeg.substring(0, i).trim(); complexSeg = complexSeg.substring(i + 1).trim(); if (complexSeg.equals("**")) { throw new ValidationException( "parse error: wildcard path not allowed in complex ID resource '%s'", subVarName); } } String complexDelimiter = currIteratorIndex < separatorIndices.size() ? separatorIndices.get(currIteratorIndex) : ""; segments.add(Segment.create(SegmentKind.BINDING, subVarName, complexDelimiter)); segments.add(Segment.wildcardCreate(complexDelimiter)); segments.add(Segment.END_BINDING); subVarName = null; currIteratorIndex++; } return segments; } // 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