
com.linecorp.armeria.server.DefaultPathMapping Maven / Gradle / Ivy
/*
* Copyright 2017 LINE Corporation
*
* LINE Corporation licenses this file to you 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:
*
* http://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 com.linecorp.armeria.server;
import static java.util.Objects.requireNonNull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.StringJoiner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
/**
* The default {@link PathMapping} implementation. It holds three things:
*
* - The regex-compiled form of the path. It is used for matching and extracting.
* - The skeleton of the path. It is used for duplication detecting.
* - A set of path parameters declared in the path pattern
*
*/
final class DefaultPathMapping extends AbstractPathMapping {
private static final Pattern VALID_PATTERN = Pattern.compile("(/[^/{}:]+|/:[^/{}]+|/\\{[^/{}]+})+/?");
/**
* The original path pattern specified in the constructor.
*/
private final String pathPattern;
/**
* Regex form of given path, which will be used for matching or extracting.
*
* e.g. "/{x}/{y}/{x}" -> "/(?<x>[^/]+)/(?<y>[^/]+)/(\\k<x>)"
*/
private final Pattern pattern;
/**
* Skeletal form of given path, which is used for duplicated routing rule detection.
* For example, "/{a}/{b}" and "/{c}/{d}" has same skeletal form and regarded as duplicated.
*
*
e.g. "/{x}/{y}/{z}" -> "/{}/{}/{}"
*/
private final String skeleton;
/**
* The names of the path parameters in the order of appearance.
*/
private final String[] paramNameArray;
/**
* The names of the path parameters this mapping will extract.
*/
private final Set paramNames;
private final String loggerName;
private final String metricName;
/**
* Create a {@link DefaultPathMapping} instance from given {@code pathPattern}.
*
* @param pathPattern the {@link String} that contains path params.
* e.g. {@code /users/{name}} or {@code /users/:name}
*
* @throws IllegalArgumentException if the {@code pathPattern} is invalid.
*/
DefaultPathMapping(String pathPattern) {
requireNonNull(pathPattern, "pathPattern");
if (!pathPattern.startsWith("/")) {
throw new IllegalArgumentException("pathPattern: " + pathPattern + " (must start with '/')");
}
if (!VALID_PATTERN.matcher(pathPattern).matches()) {
throw new IllegalArgumentException("pathPattern: " + pathPattern + " (invalid pattern)");
}
final StringJoiner patternJoiner = new StringJoiner("/");
final StringJoiner skeletonJoiner = new StringJoiner("/");
final List paramNames = new ArrayList<>();
for (String token : pathPattern.split("/")) {
final String paramName = paramName(token);
if (paramName == null) {
// If the given token is a constant, do not manipulate it.
patternJoiner.add(token);
skeletonJoiner.add(token);
continue;
}
final int paramNameIdx = paramNames.indexOf(paramName);
if (paramNameIdx < 0) {
// If the given token appeared first time, add it to the set and
// replace it with a capturing group expression in regex.
paramNames.add(paramName);
patternJoiner.add("([^/]+)");
} else {
// If the given token appeared before, replace it with a back-reference expression
// in regex.
patternJoiner.add("\\" + (paramNameIdx + 1));
}
skeletonJoiner.add("{}");
}
this.pathPattern = pathPattern;
pattern = Pattern.compile(patternJoiner.toString());
skeleton = skeletonJoiner.toString();
paramNameArray = paramNames.toArray(new String[paramNames.size()]);
this.paramNames = ImmutableSet.copyOf(paramNames);
loggerName = loggerName(pathPattern);
metricName = pathPattern;
}
/**
* Returns the name of the path parameter contained in the path element. If it contains no path parameter,
* {@code null} is returned. e.g.
*
* - {@code "{foo}"} -> {@code "foo"}
* - {@code ":bar"} -> {@code "bar"}
* - {@code "baz"} -> {@code null}
*
*/
private static String paramName(String token) {
if (token.startsWith("{") && token.endsWith("}")) {
return token.substring(1, token.length() - 1);
}
if (token.startsWith(":")) {
return token.substring(1);
}
return null;
}
/**
* Returns the skeleton.
*/
String skeleton() {
return skeleton;
}
@Override
public Set paramNames() {
return paramNames;
}
@Override
public String loggerName() {
return loggerName;
}
@Override
public String metricName() {
return metricName;
}
@Override
protected PathMappingResult doApply(String path, @Nullable String query) {
final Matcher matcher = pattern.matcher(path);
if (!matcher.matches()) {
return PathMappingResult.empty();
}
if (paramNameArray.length == 0) {
return PathMappingResult.of(path, query);
}
final ImmutableMap.Builder pathParams = ImmutableMap.builder();
for (int i = 0; i < paramNameArray.length; i++) {
pathParams.put(paramNameArray[i], matcher.group(i + 1));
}
return PathMappingResult.of(path, query, pathParams.build());
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
DefaultPathMapping that = (DefaultPathMapping) o;
return skeleton.equals(that.skeleton) &&
Arrays.equals(paramNameArray, that.paramNameArray);
}
@Override
public int hashCode() {
return skeleton.hashCode() * 31 + Arrays.hashCode(paramNameArray);
}
@Override
public String toString() {
return pathPattern;
}
}