io.micronaut.http.uri.UriMatchTemplate Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2017-2020 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.http.uri;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.util.CollectionUtils;
import io.micronaut.core.util.ObjectUtils;
import io.micronaut.core.util.StringUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static io.micronaut.core.util.ArrayUtils.EMPTY_OBJECT_ARRAY;
/**
* Extends {@link UriTemplate} and adds the ability to match a URI to a given template using the
* {@link #match(java.net.URI)} method.
*
* @author Graeme Rocher
* @since 1.0
*/
public class UriMatchTemplate extends UriTemplate implements UriMatcher {
protected static final String VARIABLE_MATCH_PATTERN = "([^/?#(?!{)&;+]";
protected StringBuilder pattern;
protected List variables;
private final Pattern matchPattern;
private final boolean isRoot;
private final boolean exactMatch;
// Matches cache
private UriMatchInfo rootMatchInfo;
private UriMatchInfo exactMatchInfo;
/**
* Construct a new URI template for the given template.
*
* @param templateString The template string
*/
public UriMatchTemplate(CharSequence templateString) {
this(templateString, EMPTY_OBJECT_ARRAY);
}
/**
* Construct a new URI template for the given template.
*
* @param templateString The template string
* @param parserArguments The parsed arguments
*/
protected UriMatchTemplate(CharSequence templateString, Object... parserArguments) {
super(templateString, parserArguments);
if (variables.isEmpty() && Pattern.quote(templateString.toString()).contentEquals(pattern)) {
// if there are no variables and a match pattern matches template we can assume it matches exactly
this.matchPattern = null;
this.exactMatch = true;
} else {
this.matchPattern = Pattern.compile(pattern.toString());
this.exactMatch = false;
}
this.isRoot = isRoot();
// cleanup / reduce memory consumption
this.pattern = null;
}
/**
* @param templateString The template
* @param segments The list of segments
* @param matchPattern The match pattern
* @param variables The variables
*/
protected UriMatchTemplate(CharSequence templateString, List segments, Pattern matchPattern, List variables) {
super(templateString.toString(), segments);
this.variables = variables;
this.isRoot = isRoot();
if (variables.isEmpty() && matchPattern.matcher(templateString).matches()) {
// if there are no variables and match pattern matches template we can assume it matches exactly
this.matchPattern = null;
this.exactMatch = true;
} else {
this.matchPattern = matchPattern;
this.exactMatch = false;
}
}
/**
* @param uriTemplate The template
* @param newSegments The list of new segments
* @param newPattern The list of new patters
* @param variables The variables
* @return An instance of {@link UriMatchTemplate}
*/
protected UriMatchTemplate newUriMatchTemplate(CharSequence uriTemplate, List newSegments, Pattern newPattern, List variables) {
return new UriMatchTemplate(uriTemplate, newSegments, newPattern, variables);
}
/**
* @return The variables this template expects
*/
public List getVariableNames() {
return variables.stream().map(UriMatchVariable::getName).collect(Collectors.toList());
}
/**
* @return The variables this template expects
*/
public List getVariables() {
return Collections.unmodifiableList(variables);
}
/**
* Returns the path string excluding any query variables.
*
* @return The path string
*/
public String toPathString() {
return toString(pathSegment -> {
final Optional var = pathSegment.getVariable();
if (var.isPresent()) {
final Optional umv = variables.stream()
.filter(v -> v.getName().equals(var.get()))
.findFirst();
if (umv.isPresent()) {
return !umv.get().isQuery();
}
}
return true;
});
}
/**
* Match the given URI string.
*
* @param uri The uRI
* @return an optional match
*/
@Override
public Optional match(String uri) {
return Optional.ofNullable(tryMatch(uri));
}
/**
* Match the given URI string.
*
* @param uri The uRI
* @return a match or null
*/
@Nullable
public UriMatchInfo tryMatch(@NonNull String uri) {
if (uri == null) {
throw new IllegalArgumentException("Argument 'uri' cannot be null");
}
int length = uri.length();
if (length > 1 && uri.charAt(length - 1) == '/') {
uri = uri.substring(0, length - 1);
}
if (isRoot && (length == 0 || (length == 1 && uri.charAt(0) == '/'))) {
if (rootMatchInfo == null) {
rootMatchInfo = new DefaultUriMatchInfo(uri, Collections.emptyMap(), variables);
}
return rootMatchInfo;
}
//Remove any url parameters before matching
int parameterIndex = uri.indexOf('?');
if (parameterIndex > -1) {
uri = uri.substring(0, parameterIndex);
}
if (uri.endsWith("/")) {
uri = uri.substring(0, uri.length() - 1);
}
if (exactMatch) {
if (uri.equals(templateString)) {
if (exactMatchInfo == null) {
exactMatchInfo = new DefaultUriMatchInfo(uri, Collections.emptyMap(), variables);
}
return exactMatchInfo;
}
return null;
}
Matcher matcher = matchPattern.matcher(uri);
if (matcher.matches()) {
if (variables.isEmpty()) {
return new DefaultUriMatchInfo(uri, Collections.emptyMap(), variables);
} else {
int count = matcher.groupCount();
Map variableMap = CollectionUtils.newLinkedHashMap(count);
for (int j = 0; j < variables.size(); j++) {
int index = (j * 2) + 2;
if (index > count) {
break;
}
UriMatchVariable variable = variables.get(j);
String value = matcher.group(index);
variableMap.put(variable.getName(), value);
}
return new DefaultUriMatchInfo(uri, variableMap, variables);
}
}
return null;
}
@Override
public UriMatchTemplate nest(CharSequence uriTemplate) {
return (UriMatchTemplate) super.nest(uriTemplate);
}
/**
* Create a new {@link UriTemplate} for the given URI.
*
* @param uri The URI
* @return The template
*/
public static UriMatchTemplate of(String uri) {
return new UriMatchTemplate(uri);
}
@Override
protected UriTemplate newUriTemplate(CharSequence uriTemplate, List newSegments) {
Pattern newPattern = Pattern.compile(exactMatch ? Pattern.quote(templateString) + pattern.toString() : matchPattern.pattern() + pattern.toString());
pattern = null;
return newUriMatchTemplate(normalizeNested(toString(), uriTemplate), newSegments, newPattern, new ArrayList<>(variables));
}
@Override
protected UriTemplateParser createParser(String templateString, Object... parserArguments) {
if (Objects.isNull(this.pattern)) {
this.pattern = new StringBuilder();
}
if (this.variables == null) {
this.variables = new ArrayList<>();
}
return new UriMatchTemplateParser(templateString, this);
}
private boolean isRoot() {
CharSequence rawSegment = null;
for (PathSegment segment : segments) {
if (segment.isVariable()) {
if (!segment.isQuerySegment()) {
return false;
}
} else {
if (rawSegment == null) {
rawSegment = segment;
} else {
return false;
}
}
}
if (rawSegment == null) {
return true;
} else {
int len = rawSegment.length();
return len == 0 || (len == 1 && rawSegment.charAt(0) == '/');
}
}
/**
* The default {@link UriMatchInfo} implementation.
*/
protected static class DefaultUriMatchInfo implements UriMatchInfo {
private final String uri;
private final Map variableValues;
private final List variables;
private final Map variableMap;
/**
* @param uri The URI
* @param variableValues The map of variable names with values
* @param variables The variables
*/
protected DefaultUriMatchInfo(String uri, Map variableValues, List variables) {
this.uri = uri;
this.variableValues = variableValues;
this.variables = variables;
LinkedHashMap vm = CollectionUtils.newLinkedHashMap(variables.size());
for (UriMatchVariable variable : variables) {
vm.put(variable.getName(), variable);
}
this.variableMap = Collections.unmodifiableMap(vm);
}
@Override
public String getUri() {
return uri;
}
@Override
public Map getVariableValues() {
return variableValues;
}
@Override
public List getVariables() {
return Collections.unmodifiableList(variables);
}
@Override
public Map getVariableMap() {
return variableMap;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
DefaultUriMatchInfo that = (DefaultUriMatchInfo) o;
return uri.equals(that.uri) && variables.equals(that.variables);
}
@Override
public String toString() {
return getUri();
}
@Override
public int hashCode() {
return ObjectUtils.hash(uri, variableValues);
}
}
/**
* Extended version of {@link UriTemplate.UriTemplateParser} that builds a regular expression to match a path.
* Note that fragments (#) and queries (?) are ignored for the purposes of matching.
*/
protected static class UriMatchTemplateParser extends UriTemplateParser {
final UriMatchTemplate matchTemplate;
/**
* @param templateText The template
* @param matchTemplate The Uri match template
*/
protected UriMatchTemplateParser(String templateText, UriMatchTemplate matchTemplate) {
super(templateText);
this.matchTemplate = matchTemplate;
}
/**
* @return The URI match template
*/
public UriMatchTemplate getMatchTemplate() {
return matchTemplate;
}
@Override
protected void addRawContentSegment(List segments, String value, boolean isQuerySegment) {
matchTemplate.pattern.append(Pattern.quote(value));
super.addRawContentSegment(segments, value, isQuerySegment);
}
@Override
protected void addVariableSegment(List segments,
String variable,
String prefix,
String delimiter,
boolean encode,
boolean repeatPrefix,
String modifierStr,
char modifierChar,
char operator,
String previousDelimiter, boolean isQuerySegment) {
matchTemplate.variables.add(new UriMatchVariable(variable, modifierChar, operator));
int modLen = modifierStr.length();
boolean hasModifier = modifierChar == ':' && modLen > 0;
String operatorPrefix = StringUtils.EMPTY_STRING;
String operatorQuantifier = StringUtils.EMPTY_STRING;
String variableQuantifier = "+?)";
String variablePattern = null;
if (hasModifier) {
char firstChar = modifierStr.charAt(0);
if (firstChar == '?') {
operatorQuantifier = StringUtils.EMPTY_STRING;
} else if (modifierStr.chars().allMatch(Character::isDigit)) {
variableQuantifier = "{1," + modifierStr + "})";
} else {
char lastChar = modifierStr.charAt(modLen - 1);
if (lastChar == '*' ||
(modLen > 1 && lastChar == '?' && (modifierStr.charAt(modLen - 2) == '*' || modifierStr.charAt(modLen - 2) == '+'))) {
operatorQuantifier = "?";
}
String s = firstChar == '^' ? modifierStr.substring(1) : modifierStr;
if (operator == '/' || operator == '.') {
variablePattern = "(" + s + ')';
} else {
operatorPrefix = "(";
variablePattern = s + ')';
}
variableQuantifier = StringUtils.EMPTY_STRING;
}
}
boolean operatorAppended = false;
StringBuilder pattern = matchTemplate.pattern;
switch (operator) {
case '.':
case '/':
pattern.append('(')
.append(operatorPrefix)
.append('\\')
.append(operator)
.append(operatorQuantifier);
operatorAppended = true;
// fall through
case '+':
case '0': // no active operator
if (!operatorAppended) {
pattern.append('(').append(operatorPrefix);
}
if (variablePattern == null) {
variablePattern = getVariablePattern(variable, operator);
}
pattern.append(variablePattern)
.append(variableQuantifier)
.append(')');
break;
default:
// no-op
}
if (operator == '/' || modifierStr.equals("?")) {
pattern.append('?');
}
super.addVariableSegment(segments, variable, prefix, delimiter, encode, repeatPrefix, modifierStr, modifierChar, operator, previousDelimiter, isQuerySegment);
}
/**
* @param variable The variable
* @param operator The operator
* @return The variable match pattern
*/
protected String getVariablePattern(String variable, char operator) {
if (operator == '+') {
// Allow reserved characters. See https://tools.ietf.org/html/rfc6570#section-3.2.3
return "([\\S]";
} else {
return VARIABLE_MATCH_PATTERN;
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy