io.quarkus.vertx.http.runtime.security.PathMatcher Maven / Gradle / Ivy
package io.quarkus.vertx.http.runtime.security;
import java.util.Comparator;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* Handler that dispatches to a given handler based of a prefix match of the path.
*
* This only matches a single level of a request, e.g if you have a request that takes the form:
*
* /foo/bar
*
*
* @author Stuart Douglas
*/
public class PathMatcher {
private static final String STRING_PATH_SEPARATOR = "/";
private volatile T defaultHandler;
private final SubstringMap paths = new SubstringMap<>();
private final ConcurrentMap exactPathMatches = new ConcurrentHashMap<>();
/**
* lengths of all registered paths
*/
private volatile int[] lengths = {};
public PathMatcher(final T defaultHandler) {
this.defaultHandler = defaultHandler;
}
public PathMatcher() {
}
/**
* Matches a path against the registered handlers.
*
* @param path The relative path to match
* @return The match match. This will never be null, however if none matched its value field will be
*/
public PathMatch match(String path) {
if (!exactPathMatches.isEmpty()) {
T match = getExactPath(path);
if (match != null) {
return new PathMatch<>(path, "", match);
}
}
int length = path.length();
final int[] lengths = this.lengths;
for (int pathLength : lengths) {
if (pathLength == length) {
SubstringMap.SubstringMatch next = paths.get(path, length);
if (next != null) {
return new PathMatch<>(path, "", next.getValue());
}
} else if (pathLength < length) {
char c = path.charAt(pathLength);
if (c == '/') {
//String part = path.substring(0, pathLength);
SubstringMap.SubstringMatch next = paths.get(path, pathLength);
if (next != null) {
return new PathMatch<>(next.getKey(), path.substring(pathLength), next.getValue());
}
}
}
}
return new PathMatch<>("", path, defaultHandler);
}
/**
* Adds a path prefix and a handler for that path. If the path does not start
* with a / then one will be prepended.
*
* The match is done on a prefix bases, so registering /foo will also match /bar. Exact
* path matches are taken into account first.
*
* If / is specified as the path then it will replace the default handler.
*
* @param path The path
* @param handler The handler
*/
public synchronized PathMatcher addPrefixPath(final String path, final T handler) {
if (path.isEmpty()) {
throw new IllegalArgumentException("Path not specified");
}
if (PathMatcher.STRING_PATH_SEPARATOR.equals(path)) {
this.defaultHandler = handler;
return this;
}
paths.put(path, handler);
buildLengths();
return this;
}
public synchronized PathMatcher addExactPath(final String path, final T handler) {
if (path.isEmpty()) {
throw new IllegalArgumentException("Path not specified");
}
exactPathMatches.put(path, handler);
return this;
}
public T getExactPath(final String path) {
return exactPathMatches.get(path);
}
public T getPrefixPath(final String path) {
// enable the prefix path mechanism to return the default handler
SubstringMap.SubstringMatch match = paths.get(path);
if (PathMatcher.STRING_PATH_SEPARATOR.equals(path) && match == null) {
return this.defaultHandler;
}
if (match == null) {
return null;
}
// return the value for the given path
return match.getValue();
}
private void buildLengths() {
final Set lengths = new TreeSet<>(new Comparator() {
@Override
public int compare(Integer o1, Integer o2) {
return -o1.compareTo(o2);
}
});
for (String p : paths.keys()) {
lengths.add(p.length());
}
int[] lengthArray = new int[lengths.size()];
int pos = 0;
for (int i : lengths) {
lengthArray[pos++] = i;
}
this.lengths = lengthArray;
}
@Deprecated
public synchronized PathMatcher removePath(final String path) {
return removePrefixPath(path);
}
public synchronized PathMatcher removePrefixPath(final String path) {
if (path == null || path.isEmpty()) {
throw new IllegalArgumentException("Path not specified");
}
if (PathMatcher.STRING_PATH_SEPARATOR.equals(path)) {
defaultHandler = null;
return this;
}
paths.remove(path);
buildLengths();
return this;
}
public synchronized PathMatcher removeExactPath(final String path) {
if (path == null || path.isEmpty()) {
throw new IllegalArgumentException("Path not specified");
}
exactPathMatches.remove(path);
return this;
}
public synchronized PathMatcher clearPaths() {
paths.clear();
exactPathMatches.clear();
this.lengths = new int[0];
defaultHandler = null;
return this;
}
public Map getPaths() {
return paths.toMap();
}
public static final class PathMatch {
private final String matched;
private final String remaining;
private final T value;
public PathMatch(String matched, String remaining, T value) {
this.matched = matched;
this.remaining = remaining;
this.value = value;
}
public String getRemaining() {
return remaining;
}
public String getMatched() {
return matched;
}
public T getValue() {
return value;
}
}
}