Improved PathSpec handling for servletName & pathInfo (#7947)

* Introduce MatchedResource
* Introduce MatchedPath
* Improved group matching with optimized Tries
* Deprecate old APIs
* Introduced final preMatchedPath when possible

Signed-off-by: Joakim Erdfelt <joakim.erdfelt@gmail.com>
Co-authored-by: Greg Wilkins <gregw@webtide.com>
This commit is contained in:
Joakim Erdfelt 2022-06-07 14:24:56 -05:00 committed by GitHub
parent 208b150148
commit 5b4d1dd1c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1037 additions and 351 deletions

View File

@ -36,7 +36,12 @@ public abstract class AbstractPathSpec implements PathSpec
return diff; return diff;
// Path Spec Name (alphabetical) // Path Spec Name (alphabetical)
return getDeclaration().compareTo(other.getDeclaration()); diff = getDeclaration().compareTo(other.getDeclaration());
if (diff != 0)
return diff;
// Path Implementation
return getClass().getName().compareTo(other.getClass().getName());
} }
@Override @Override
@ -55,7 +60,7 @@ public abstract class AbstractPathSpec implements PathSpec
@Override @Override
public final int hashCode() public final int hashCode()
{ {
return Objects.hash(getDeclaration()); return Objects.hash(getGroup().ordinal(), getSpecLength(), getDeclaration(), getClass().getName());
} }
@Override @Override

View File

@ -0,0 +1,81 @@
//
// ========================================================================
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.http.pathmap;
public interface MatchedPath
{
MatchedPath EMPTY = new MatchedPath()
{
@Override
public String getPathMatch()
{
return null;
}
@Override
public String getPathInfo()
{
return null;
}
@Override
public String toString()
{
return MatchedPath.class.getSimpleName() + ".EMPTY";
}
};
static MatchedPath from(String pathMatch, String pathInfo)
{
return new MatchedPath()
{
@Override
public String getPathMatch()
{
return pathMatch;
}
@Override
public String getPathInfo()
{
return pathInfo;
}
@Override
public String toString()
{
return MatchedPath.class.getSimpleName() + "[pathMatch=" + pathMatch + ", pathInfo=" + pathInfo + "]";
}
};
}
/**
* Return the portion of the path that matches a path spec.
*
* @return the path name portion of the match.
*/
String getPathMatch();
/**
* Return the portion of the path that is after the path spec.
*
* @return the path info portion of the match, or null if there is no portion after the {@link #getPathMatch()}
*/
String getPathInfo();
}

View File

@ -0,0 +1,76 @@
//
// ========================================================================
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.http.pathmap;
import java.util.Map;
/**
* The match details when using {@link PathMappings#getMatched(String)}, used to minimize return to the PathSpec or PathMappings for subsequent details
* that are now provided by the {@link MatchedPath} instance.
*
* @param <E> the type of resource (IncludeExclude uses boolean, WebSocket uses endpoint/endpoint config, servlet uses ServletHolder, etc)
*/
public class MatchedResource<E>
{
private final E resource;
private final PathSpec pathSpec;
private final MatchedPath matchedPath;
public MatchedResource(E resource, PathSpec pathSpec, MatchedPath matchedPath)
{
this.resource = resource;
this.pathSpec = pathSpec;
this.matchedPath = matchedPath;
}
public static <E> MatchedResource<E> of(Map.Entry<PathSpec, E> mapping, MatchedPath matchedPath)
{
return new MatchedResource<>(mapping.getValue(), mapping.getKey(), matchedPath);
}
public PathSpec getPathSpec()
{
return this.pathSpec;
}
public E getResource()
{
return this.resource;
}
/**
* Return the portion of the path that matches a path spec.
*
* @return the path name portion of the match.
*/
public String getPathMatch()
{
return matchedPath.getPathMatch();
}
/**
* Return the portion of the path that is after the path spec.
*
* @return the path info portion of the match, or null if there is no portion after the {@link #getPathMatch()}
*/
public String getPathInfo()
{
return matchedPath.getPathInfo();
}
}

View File

@ -23,7 +23,6 @@ import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.function.Predicate; import java.util.function.Predicate;
@ -49,8 +48,11 @@ public class PathMappings<E> implements Iterable<MappedResource<E>>, Dumpable
private static final Logger LOG = Log.getLogger(PathMappings.class); private static final Logger LOG = Log.getLogger(PathMappings.class);
private final Set<MappedResource<E>> _mappings = new TreeSet<>(Comparator.comparing(MappedResource::getPathSpec)); private final Set<MappedResource<E>> _mappings = new TreeSet<>(Comparator.comparing(MappedResource::getPathSpec));
private boolean _optimizedExact = true;
private Trie<MappedResource<E>> _exactMap = new ArrayTernaryTrie<>(false); private Trie<MappedResource<E>> _exactMap = new ArrayTernaryTrie<>(false);
private boolean _optimizedPrefix = true;
private Trie<MappedResource<E>> _prefixMap = new ArrayTernaryTrie<>(false); private Trie<MappedResource<E>> _prefixMap = new ArrayTernaryTrie<>(false);
private boolean _optimizedSuffix = true;
private Trie<MappedResource<E>> _suffixMap = new ArrayTernaryTrie<>(false); private Trie<MappedResource<E>> _suffixMap = new ArrayTernaryTrie<>(false);
@Override @Override
@ -88,6 +90,26 @@ public class PathMappings<E> implements Iterable<MappedResource<E>>, Dumpable
_mappings.removeIf(predicate); _mappings.removeIf(predicate);
} }
/**
* Return a list of MatchedResource matches for the specified path.
*
* @param path the path to return matches on
* @return the list of mapped resource the path matches on
*/
public List<MatchedResource<E>> getMatchedList(String path)
{
List<MatchedResource<E>> ret = new ArrayList<>();
for (MappedResource<E> mr : _mappings)
{
MatchedPath matchedPath = mr.getPathSpec().matched(path);
if (matchedPath != null)
{
ret.add(new MatchedResource<>(mr.getResource(), mr.getPathSpec(), matchedPath));
}
}
return ret;
}
/** /**
* Return a list of MappedResource matches for the specified path. * Return a list of MappedResource matches for the specified path.
* *
@ -108,11 +130,11 @@ public class PathMappings<E> implements Iterable<MappedResource<E>>, Dumpable
ret.add(mr); ret.add(mr);
break; break;
case DEFAULT: case DEFAULT:
if (isRootPath || mr.getPathSpec().matches(path)) if (isRootPath || mr.getPathSpec().matched(path) != null)
ret.add(mr); ret.add(mr);
break; break;
default: default:
if (mr.getPathSpec().matches(path)) if (mr.getPathSpec().matched(path) != null)
ret.add(mr); ret.add(mr);
break; break;
} }
@ -120,36 +142,59 @@ public class PathMappings<E> implements Iterable<MappedResource<E>>, Dumpable
return ret; return ret;
} }
public MappedResource<E> getMatch(String path) public MatchedResource<E> getMatched(String path)
{ {
MatchedPath matchedPath;
PathSpecGroup lastGroup = null; PathSpecGroup lastGroup = null;
boolean skipRestOfGroup = false;
// Search all the mappings // Search all the mappings
for (MappedResource<E> mr : _mappings) for (MappedResource<E> mr : _mappings)
{ {
PathSpecGroup group = mr.getPathSpec().getGroup(); PathSpecGroup group = mr.getPathSpec().getGroup();
if (group == lastGroup && skipRestOfGroup)
{
continue; // skip
}
// Run servlet spec optimizations on first hit of specific groups
if (group != lastGroup) if (group != lastGroup)
{ {
// New group, reset skip logic
skipRestOfGroup = false;
// New group in list, so let's look for an optimization // New group in list, so let's look for an optimization
switch (group) switch (group)
{ {
case EXACT: case EXACT:
{
if (_optimizedExact)
{ {
int i = path.length(); int i = path.length();
final Trie<MappedResource<E>> exact_map = _exactMap; final Trie<MappedResource<E>> exact_map = _exactMap;
while (i >= 0) while (i >= 0)
{ {
MappedResource<E> candidate = exact_map.getBest(path, 0, i); MappedResource<E> candidate = exact_map.getBest(path, 0, i);
if (candidate == null) if (candidate == null)
break; break;
if (candidate.getPathSpec().matches(path))
return candidate; matchedPath = candidate.getPathSpec().matched(path);
i = candidate.getPathSpec().getPrefix().length() - 1; if (matchedPath != null)
{
return new MatchedResource<>(candidate.getResource(), candidate.getPathSpec(), matchedPath);
}
i--;
}
// If we reached here, there's NO optimized EXACT Match possible, skip simple match below
skipRestOfGroup = true;
} }
break; break;
} }
case PREFIX_GLOB: case PREFIX_GLOB:
{
if (_optimizedPrefix)
{ {
int i = path.length(); int i = path.length();
final Trie<MappedResource<E>> prefix_map = _prefixMap; final Trie<MappedResource<E>> prefix_map = _prefixMap;
@ -158,22 +203,36 @@ public class PathMappings<E> implements Iterable<MappedResource<E>>, Dumpable
MappedResource<E> candidate = prefix_map.getBest(path, 0, i); MappedResource<E> candidate = prefix_map.getBest(path, 0, i);
if (candidate == null) if (candidate == null)
break; break;
if (candidate.getPathSpec().matches(path))
return candidate; matchedPath = candidate.getPathSpec().matched(path);
i = candidate.getPathSpec().getPrefix().length() - 1; if (matchedPath != null)
return new MatchedResource<>(candidate.getResource(), candidate.getPathSpec(), matchedPath);
i--;
}
// If we reached here, there's NO optimized PREFIX Match possible, skip simple match below
skipRestOfGroup = true;
} }
break; break;
} }
case SUFFIX_GLOB: case SUFFIX_GLOB:
{
if (_optimizedSuffix)
{ {
int i = 0; int i = 0;
final Trie<MappedResource<E>> suffix_map = _suffixMap; final Trie<MappedResource<E>> suffix_map = _suffixMap;
while ((i = path.indexOf('.', i + 1)) > 0) while ((i = path.indexOf('.', i + 1)) > 0)
{ {
MappedResource<E> candidate = suffix_map.get(path, i + 1, path.length() - i - 1); MappedResource<E> candidate = suffix_map.get(path, i + 1, path.length() - i - 1);
if (candidate != null && candidate.getPathSpec().matches(path)) if (candidate == null)
return candidate; break;
matchedPath = candidate.getPathSpec().matched(path);
if (matchedPath != null)
return new MatchedResource<>(candidate.getResource(), candidate.getPathSpec(), matchedPath);
}
// If we reached here, there's NO optimized SUFFIX Match possible, skip simple match below
skipRestOfGroup = true;
} }
break; break;
} }
@ -182,8 +241,9 @@ public class PathMappings<E> implements Iterable<MappedResource<E>>, Dumpable
} }
} }
if (mr.getPathSpec().matches(path)) matchedPath = mr.getPathSpec().matched(path);
return mr; if (matchedPath != null)
return new MatchedResource<>(mr.getResource(), mr.getPathSpec(), matchedPath);
lastGroup = group; lastGroup = group;
} }
@ -191,96 +251,122 @@ public class PathMappings<E> implements Iterable<MappedResource<E>>, Dumpable
return null; return null;
} }
/**
* @deprecated use {@link #getMatched(String)} instead
*/
@Deprecated
public MappedResource<E> getMatch(String path)
{
throw new UnsupportedOperationException("Use .getMatched(String) instead");
}
@Override @Override
public Iterator<MappedResource<E>> iterator() public Iterator<MappedResource<E>> iterator()
{ {
return _mappings.iterator(); return _mappings.iterator();
} }
/**
* @deprecated use {@link PathSpec#from(String)} instead
*/
@Deprecated
public static PathSpec asPathSpec(String pathSpecString) public static PathSpec asPathSpec(String pathSpecString)
{ {
if (pathSpecString == null) return PathSpec.from(pathSpecString);
throw new RuntimeException("Path Spec String must start with '^', '/', or '*.': got [" + pathSpecString + "]");
if (pathSpecString.length() == 0)
return new ServletPathSpec("");
return pathSpecString.charAt(0) == '^' ? new RegexPathSpec(pathSpecString) : new ServletPathSpec(pathSpecString);
} }
public E get(PathSpec spec) public E get(PathSpec spec)
{ {
Optional<E> optionalResource = _mappings.stream() return _mappings.stream()
.filter(mappedResource -> mappedResource.getPathSpec().equals(spec)) .filter(mappedResource -> mappedResource.getPathSpec().equals(spec))
.map(mappedResource -> mappedResource.getResource()) .map(MappedResource::getResource)
.findFirst(); .findFirst()
if (!optionalResource.isPresent()) .orElse(null);
return null;
return optionalResource.get();
} }
public boolean put(String pathSpecString, E resource) public boolean put(String pathSpecString, E resource)
{ {
return put(asPathSpec(pathSpecString), resource); return put(PathSpec.from(pathSpecString), resource);
} }
public boolean put(PathSpec pathSpec, E resource) public boolean put(PathSpec pathSpec, E resource)
{ {
MappedResource<E> entry = new MappedResource<>(pathSpec, resource); MappedResource<E> entry = new MappedResource<>(pathSpec, resource);
boolean added = _mappings.add(entry);
if (LOG.isDebugEnabled())
LOG.debug("{} {} to {}", added ? "Added" : "Ignored", entry, this);
if (added)
{
switch (pathSpec.getGroup()) switch (pathSpec.getGroup())
{ {
case EXACT: case EXACT:
String exact = pathSpec.getPrefix(); if (pathSpec instanceof ServletPathSpec)
{
String exact = pathSpec.getDeclaration();
while (exact != null && !_exactMap.put(exact, entry)) while (exact != null && !_exactMap.put(exact, entry))
{ {
// grow the capacity of the Trie
_exactMap = new ArrayTernaryTrie<>((ArrayTernaryTrie<MappedResource<E>>)_exactMap, 1.5); _exactMap = new ArrayTernaryTrie<>((ArrayTernaryTrie<MappedResource<E>>)_exactMap, 1.5);
} }
}
else
{
// This is not a Servlet mapping, turn off optimization on Exact
// TODO: see if we can optimize all Regex / UriTemplate versions here too.
// Note: Example exact in Regex that can cause problems `^/a\Q/b\E/` (which is only ever matching `/a/b/`)
// Note: UriTemplate can handle exact easily enough
_optimizedExact = false;
}
break; break;
case PREFIX_GLOB: case PREFIX_GLOB:
if (pathSpec instanceof ServletPathSpec)
{
String prefix = pathSpec.getPrefix(); String prefix = pathSpec.getPrefix();
while (prefix != null && !_prefixMap.put(prefix, entry)) while (prefix != null && !_prefixMap.put(prefix, entry))
{ {
// grow the capacity of the Trie
_prefixMap = new ArrayTernaryTrie<>((ArrayTernaryTrie<MappedResource<E>>)_prefixMap, 1.5); _prefixMap = new ArrayTernaryTrie<>((ArrayTernaryTrie<MappedResource<E>>)_prefixMap, 1.5);
} }
}
else
{
// This is not a Servlet mapping, turn off optimization on Prefix
// TODO: see if we can optimize all Regex / UriTemplate versions here too.
// Note: Example Prefix in Regex that can cause problems `^/a/b+` or `^/a/bb*` ('b' one or more times)
// Note: Example Prefix in UriTemplate that might cause problems `/a/{b}/{c}`
_optimizedPrefix = false;
}
break; break;
case SUFFIX_GLOB: case SUFFIX_GLOB:
if (pathSpec instanceof ServletPathSpec)
{
String suffix = pathSpec.getSuffix(); String suffix = pathSpec.getSuffix();
while (suffix != null && !_suffixMap.put(suffix, entry)) while (suffix != null && !_suffixMap.put(suffix, entry))
{ {
// grow the capacity of the Trie
_suffixMap = new ArrayTernaryTrie<>((ArrayTernaryTrie<MappedResource<E>>)_prefixMap, 1.5); _suffixMap = new ArrayTernaryTrie<>((ArrayTernaryTrie<MappedResource<E>>)_prefixMap, 1.5);
} }
}
else
{
// This is not a Servlet mapping, turn off optimization on Suffix
// TODO: see if we can optimize all Regex / UriTemplate versions here too.
// Note: Example suffix in Regex that can cause problems `^.*/path/name.ext` or `^/a/.*(ending)`
// Note: Example suffix in UriTemplate that can cause problems `/{a}/name.ext`
_optimizedSuffix = false;
}
break; break;
default: default:
} }
}
boolean added = _mappings.add(entry);
if (LOG.isDebugEnabled())
LOG.debug("{} {} to {}", added ? "Added" : "Ignored", entry, this);
return added; return added;
} }
@SuppressWarnings("incomplete-switch") @SuppressWarnings("incomplete-switch")
public boolean remove(PathSpec pathSpec) public boolean remove(PathSpec pathSpec)
{ {
String prefix = pathSpec.getPrefix();
String suffix = pathSpec.getSuffix();
switch (pathSpec.getGroup())
{
case EXACT:
if (prefix != null)
_exactMap.remove(prefix);
break;
case PREFIX_GLOB:
if (prefix != null)
_prefixMap.remove(prefix);
break;
case SUFFIX_GLOB:
if (suffix != null)
_suffixMap.remove(suffix);
break;
}
Iterator<MappedResource<E>> iter = _mappings.iterator(); Iterator<MappedResource<E>> iter = _mappings.iterator();
boolean removed = false; boolean removed = false;
while (iter.hasNext()) while (iter.hasNext())
@ -292,11 +378,54 @@ public class PathMappings<E> implements Iterable<MappedResource<E>>, Dumpable
break; break;
} }
} }
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("{} {} to {}", removed ? "Removed" : "Ignored", pathSpec, this); LOG.debug("{} {} to {}", removed ? "Removed" : "Ignored", pathSpec, this);
if (removed)
{
switch (pathSpec.getGroup())
{
case EXACT:
String exact = pathSpec.getDeclaration();
if (exact != null)
{
_exactMap.remove(exact);
// Recalculate _optimizeExact
_optimizedExact = canBeOptimized(PathSpecGroup.EXACT);
}
break;
case PREFIX_GLOB:
String prefix = pathSpec.getPrefix();
if (prefix != null)
{
_prefixMap.remove(prefix);
// Recalculate _optimizePrefix
_optimizedPrefix = canBeOptimized(PathSpecGroup.PREFIX_GLOB);
}
break;
case SUFFIX_GLOB:
String suffix = pathSpec.getSuffix();
if (suffix != null)
{
_suffixMap.remove(suffix);
// Recalculate _optimizeSuffix
_optimizedSuffix = canBeOptimized(PathSpecGroup.SUFFIX_GLOB);
}
break;
}
}
return removed; return removed;
} }
private boolean canBeOptimized(PathSpecGroup suffixGlob)
{
return _mappings.stream()
.filter((mapping) -> mapping.getPathSpec().getGroup() == suffixGlob)
.allMatch((mapping) -> mapping.getPathSpec() instanceof ServletPathSpec);
}
@Override @Override
public String toString() public String toString()
{ {

View File

@ -18,6 +18,8 @@
package org.eclipse.jetty.http.pathmap; package org.eclipse.jetty.http.pathmap;
import java.util.Objects;
/** /**
* A path specification is a URI path template that can be matched against. * A path specification is a URI path template that can be matched against.
* <p> * <p>
@ -25,6 +27,16 @@ package org.eclipse.jetty.http.pathmap;
*/ */
public interface PathSpec extends Comparable<PathSpec> public interface PathSpec extends Comparable<PathSpec>
{ {
static PathSpec from(String pathSpecString)
{
Objects.requireNonNull(pathSpecString, "null PathSpec not supported");
if (pathSpecString.length() == 0)
return new ServletPathSpec("");
return pathSpecString.charAt(0) == '^' ? new RegexPathSpec(pathSpecString) : new ServletPathSpec(pathSpecString);
}
/** /**
* The length of the spec. * The length of the spec.
* *
@ -53,7 +65,9 @@ public interface PathSpec extends Comparable<PathSpec>
* *
* @param path the path to match against * @param path the path to match against
* @return the path info portion of the string * @return the path info portion of the string
* @deprecated use {@link #matched(String)} instead
*/ */
@Deprecated
String getPathInfo(String path); String getPathInfo(String path);
/** /**
@ -61,7 +75,9 @@ public interface PathSpec extends Comparable<PathSpec>
* *
* @param path the path to match against * @param path the path to match against
* @return the match, or null if no match at all * @return the match, or null if no match at all
* @deprecated use {@link #matched(String)} instead
*/ */
@Deprecated
String getPathMatch(String path); String getPathMatch(String path);
/** /**
@ -90,6 +106,16 @@ public interface PathSpec extends Comparable<PathSpec>
* *
* @param path the path to test * @param path the path to test
* @return true if the path matches this path spec, false otherwise * @return true if the path matches this path spec, false otherwise
* @deprecated use {@link #matched(String)} instead
*/ */
@Deprecated
boolean matches(String path); boolean matches(String path);
/**
* Get the complete matched details of the provided path.
*
* @param path the path to test
* @return the matched details, if a match was possible, or null if not able to be matched.
*/
MatchedPath matched(String path);
} }

View File

@ -20,6 +20,7 @@ package org.eclipse.jetty.http.pathmap;
import java.util.AbstractSet; import java.util.AbstractSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.Objects;
import java.util.function.Predicate; import java.util.function.Predicate;
/** /**
@ -34,7 +35,7 @@ public class PathSpecSet extends AbstractSet<String> implements Predicate<String
@Override @Override
public boolean test(String s) public boolean test(String s)
{ {
return specs.getMatch(s) != null; return specs.getMatched(s) != null;
} }
@Override @Override
@ -53,17 +54,14 @@ public class PathSpecSet extends AbstractSet<String> implements Predicate<String
{ {
return (PathSpec)o; return (PathSpec)o;
} }
if (o instanceof String)
{ return PathSpec.from(Objects.toString(o));
return PathMappings.asPathSpec((String)o);
}
return PathMappings.asPathSpec(o.toString());
} }
@Override @Override
public boolean add(String s) public boolean add(String s)
{ {
return specs.put(PathMappings.asPathSpec(s), Boolean.TRUE); return specs.put(PathSpec.from(s), Boolean.TRUE);
} }
@Override @Override

View File

@ -18,11 +18,32 @@
package org.eclipse.jetty.http.pathmap; package org.eclipse.jetty.http.pathmap;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
public class RegexPathSpec extends AbstractPathSpec public class RegexPathSpec extends AbstractPathSpec
{ {
private static final Logger LOG = Log.getLogger(UriTemplatePathSpec.class);
private static final Map<Character, String> FORBIDDEN_ESCAPED = new HashMap<>();
static
{
FORBIDDEN_ESCAPED.put('s', "any whitespace");
FORBIDDEN_ESCAPED.put('n', "newline");
FORBIDDEN_ESCAPED.put('r', "carriage return");
FORBIDDEN_ESCAPED.put('t', "tab");
FORBIDDEN_ESCAPED.put('f', "form-feed");
FORBIDDEN_ESCAPED.put('b', "bell");
FORBIDDEN_ESCAPED.put('e', "escape");
FORBIDDEN_ESCAPED.put('c', "control char");
}
private final String _declaration; private final String _declaration;
private final PathSpecGroup _group; private final PathSpecGroup _group;
private final int _pathDepth; private final int _pathDepth;
@ -38,35 +59,81 @@ public class RegexPathSpec extends AbstractPathSpec
declaration = regex; declaration = regex;
int specLength = declaration.length(); int specLength = declaration.length();
// build up a simple signature we can use to identify the grouping // build up a simple signature we can use to identify the grouping
boolean inGrouping = false; boolean inTextList = false;
boolean inQuantifier = false;
StringBuilder signature = new StringBuilder(); StringBuilder signature = new StringBuilder();
int pathDepth = 0; int pathDepth = 0;
char last = 0;
for (int i = 0; i < declaration.length(); i++) for (int i = 0; i < declaration.length(); i++)
{ {
char c = declaration.charAt(i); char c = declaration.charAt(i);
switch (c) switch (c)
{ {
case '[': case '^': // ignore anchors
inGrouping = true; case '$': // ignore anchors
case '\'': // ignore escaping
case '(': // ignore grouping
case ')': // ignore grouping
break; break;
case ']': case '+': // single char quantifier
inGrouping = false; case '?': // single char quantifier
case '*': // single char quantifier
case '|': // alternate match token
case '.': // any char token
signature.append('g'); // glob signature.append('g'); // glob
break; break;
case '*': case '{':
inQuantifier = true;
break;
case '}':
inQuantifier = false;
break;
case '[':
inTextList = true;
break;
case ']':
inTextList = false;
signature.append('g'); // glob signature.append('g'); // glob
break; break;
case '/': case '/':
if (!inGrouping) if (!inTextList && !inQuantifier)
pathDepth++; pathDepth++;
break; break;
default: default:
if (!inGrouping && Character.isLetterOrDigit(c)) if (!inTextList && !inQuantifier && Character.isLetterOrDigit(c))
{
if (last == '\\') // escaped
{
String forbiddenReason = FORBIDDEN_ESCAPED.get(c);
if (forbiddenReason != null)
{
throw new IllegalArgumentException(String.format("%s does not support \\%c (%s) for \"%s\"",
this.getClass().getSimpleName(), c, forbiddenReason, declaration));
}
switch (c)
{
case 'S': // any non-whitespace
case 'd': // any digits
case 'D': // any non-digits
case 'w': // any word
case 'W': // any non-word
signature.append('g'); // glob
break;
default:
signature.append('l'); // literal (exact) signature.append('l'); // literal (exact)
break; break;
} }
} }
else // not escaped
{
signature.append('l'); // literal (exact)
}
}
break;
}
last = c;
}
Pattern pattern = Pattern.compile(declaration); Pattern pattern = Pattern.compile(declaration);
// Figure out the grouping based on the signature // Figure out the grouping based on the signature
@ -77,7 +144,7 @@ public class RegexPathSpec extends AbstractPathSpec
group = PathSpecGroup.EXACT; group = PathSpecGroup.EXACT;
else if (Pattern.matches("^l*g+", sig)) else if (Pattern.matches("^l*g+", sig))
group = PathSpecGroup.PREFIX_GLOB; group = PathSpecGroup.PREFIX_GLOB;
else if (Pattern.matches("^g+l+$", sig)) else if (Pattern.matches("^g+l+.*", sig))
group = PathSpecGroup.SUFFIX_GLOB; group = PathSpecGroup.SUFFIX_GLOB;
else else
group = PathSpecGroup.MIDDLE_GLOB; group = PathSpecGroup.MIDDLE_GLOB;
@ -87,12 +154,28 @@ public class RegexPathSpec extends AbstractPathSpec
_pathDepth = pathDepth; _pathDepth = pathDepth;
_specLength = specLength; _specLength = specLength;
_pattern = pattern; _pattern = pattern;
if (LOG.isDebugEnabled())
{
LOG.debug("Creating RegexPathSpec[{}] (signature: [{}], group: {})",
_declaration, sig, _group);
}
} }
protected Matcher getMatcher(String path) protected Matcher getMatcher(String path)
{ {
int idx = path.indexOf('?');
if (idx >= 0)
{
// match only non-query part
return _pattern.matcher(path.substring(0, idx));
}
else
{
// match entire path
return _pattern.matcher(path); return _pattern.matcher(path);
} }
}
@Override @Override
public int getSpecLength() public int getSpecLength()
@ -181,16 +264,88 @@ public class RegexPathSpec extends AbstractPathSpec
@Override @Override
public boolean matches(final String path) public boolean matches(final String path)
{ {
int idx = path.indexOf('?');
if (idx >= 0)
{
// match only non-query part
return getMatcher(path.substring(0, idx)).matches();
}
else
{
// match entire path
return getMatcher(path).matches(); return getMatcher(path).matches();
} }
@Override
public MatchedPath matched(String path)
{
Matcher matcher = getMatcher(path);
if (matcher.matches())
{
return new RegexMatchedPath(this, path, matcher);
}
return null;
}
private class RegexMatchedPath implements MatchedPath
{
private final RegexPathSpec pathSpec;
private final String path;
private final Matcher matcher;
public RegexMatchedPath(RegexPathSpec regexPathSpec, String path, Matcher matcher)
{
this.pathSpec = regexPathSpec;
this.path = path;
this.matcher = matcher;
}
@Override
public String getPathMatch()
{
String p = matcher.group("name");
if (p != null)
{
return p;
}
if (pathSpec.getGroup() == PathSpecGroup.PREFIX_GLOB && matcher.groupCount() >= 1)
{
int idx = matcher.start(1);
if (idx > 0)
{
if (this.path.charAt(idx - 1) == '/')
idx--;
return this.path.substring(0, idx);
}
}
// default is the full path
return this.path;
}
@Override
public String getPathInfo()
{
String p = matcher.group("info");
if (p != null)
{
return p;
}
// Path Info only valid for PREFIX_GLOB
if (pathSpec.getGroup() == PathSpecGroup.PREFIX_GLOB && matcher.groupCount() >= 1)
{
String pathInfo = matcher.group(1);
if ("".equals(pathInfo))
return "/";
else
return pathInfo;
}
// default is null
return null;
}
@Override
public String toString()
{
return getClass().getSimpleName() + "[" +
"pathSpec=" + pathSpec +
", path=\"" + path + "\"" +
", matcher=" + matcher +
']';
}
} }
} }

View File

@ -30,8 +30,10 @@ public class ServletPathSpec extends AbstractPathSpec
private final PathSpecGroup _group; private final PathSpecGroup _group;
private final int _pathDepth; private final int _pathDepth;
private final int _specLength; private final int _specLength;
private final int _matchLength;
private final String _prefix; private final String _prefix;
private final String _suffix; private final String _suffix;
private final MatchedPath _preMatchedPath;
/** /**
* If a servlet or filter path mapping isn't a suffix mapping, ensure * If a servlet or filter path mapping isn't a suffix mapping, ensure
@ -62,8 +64,10 @@ public class ServletPathSpec extends AbstractPathSpec
_group = PathSpecGroup.ROOT; _group = PathSpecGroup.ROOT;
_pathDepth = -1; // Set pathDepth to -1 to force this to be at the end of the sort order. _pathDepth = -1; // Set pathDepth to -1 to force this to be at the end of the sort order.
_specLength = 1; _specLength = 1;
_matchLength = 0;
_prefix = null; _prefix = null;
_suffix = null; _suffix = null;
_preMatchedPath = MatchedPath.from("", "/");
return; return;
} }
@ -74,8 +78,10 @@ public class ServletPathSpec extends AbstractPathSpec
_group = PathSpecGroup.DEFAULT; _group = PathSpecGroup.DEFAULT;
_pathDepth = -1; // Set pathDepth to -1 to force this to be at the end of the sort order. _pathDepth = -1; // Set pathDepth to -1 to force this to be at the end of the sort order.
_specLength = 1; _specLength = 1;
_matchLength = 0;
_prefix = null; _prefix = null;
_suffix = null; _suffix = null;
_preMatchedPath = null;
return; return;
} }
@ -83,6 +89,7 @@ public class ServletPathSpec extends AbstractPathSpec
PathSpecGroup group; PathSpecGroup group;
String prefix; String prefix;
String suffix; String suffix;
MatchedPath preMatchedPath;
// prefix based // prefix based
if (servletPathSpec.charAt(0) == '/' && servletPathSpec.endsWith("/*")) if (servletPathSpec.charAt(0) == '/' && servletPathSpec.endsWith("/*"))
@ -90,6 +97,7 @@ public class ServletPathSpec extends AbstractPathSpec
group = PathSpecGroup.PREFIX_GLOB; group = PathSpecGroup.PREFIX_GLOB;
prefix = servletPathSpec.substring(0, specLength - 2); prefix = servletPathSpec.substring(0, specLength - 2);
suffix = null; suffix = null;
preMatchedPath = MatchedPath.from(prefix, null);
} }
// suffix based // suffix based
else if (servletPathSpec.charAt(0) == '*' && servletPathSpec.length() > 1) else if (servletPathSpec.charAt(0) == '*' && servletPathSpec.length() > 1)
@ -97,6 +105,7 @@ public class ServletPathSpec extends AbstractPathSpec
group = PathSpecGroup.SUFFIX_GLOB; group = PathSpecGroup.SUFFIX_GLOB;
prefix = null; prefix = null;
suffix = servletPathSpec.substring(2, specLength); suffix = servletPathSpec.substring(2, specLength);
preMatchedPath = null;
} }
else else
{ {
@ -108,15 +117,15 @@ public class ServletPathSpec extends AbstractPathSpec
LOG.warn("Suspicious URL pattern: '{}'; see sections 12.1 and 12.2 of the Servlet specification", LOG.warn("Suspicious URL pattern: '{}'; see sections 12.1 and 12.2 of the Servlet specification",
servletPathSpec); servletPathSpec);
} }
preMatchedPath = MatchedPath.from(servletPathSpec, null);
} }
int pathDepth = 0; int pathDepth = 0;
for (int i = 0; i < specLength; i++) for (int i = 0; i < specLength; i++)
{ {
int cp = servletPathSpec.codePointAt(i); char c = servletPathSpec.charAt(i);
if (cp < 128) if (c < 128)
{ {
char c = (char)cp;
if (c == '/') if (c == '/')
pathDepth++; pathDepth++;
} }
@ -126,8 +135,17 @@ public class ServletPathSpec extends AbstractPathSpec
_group = group; _group = group;
_pathDepth = pathDepth; _pathDepth = pathDepth;
_specLength = specLength; _specLength = specLength;
_matchLength = prefix == null ? 0 : prefix.length();
_prefix = prefix; _prefix = prefix;
_suffix = suffix; _suffix = suffix;
_preMatchedPath = preMatchedPath;
if (LOG.isDebugEnabled())
{
LOG.debug("Creating {}[{}] (group: {}, prefix: \"{}\", suffix: \"{}\")",
getClass().getSimpleName(),
_declaration, _group, _prefix, _suffix);
}
} }
private static void assertValidServletPathSpec(String servletPathSpec) private static void assertValidServletPathSpec(String servletPathSpec)
@ -194,6 +212,10 @@ public class ServletPathSpec extends AbstractPathSpec
return _pathDepth; return _pathDepth;
} }
/**
* @deprecated use {@link #matched(String)}#{@link MatchedPath#getPathInfo()} instead.
*/
@Deprecated
@Override @Override
public String getPathInfo(String path) public String getPathInfo(String path)
{ {
@ -203,15 +225,19 @@ public class ServletPathSpec extends AbstractPathSpec
return path; return path;
case PREFIX_GLOB: case PREFIX_GLOB:
if (path.length() == (_specLength - 2)) if (path.length() == _matchLength)
return null; return null;
return path.substring(_specLength - 2); return path.substring(_matchLength);
default: default:
return null; return null;
} }
} }
/**
* @deprecated use {@link #matched(String)}#{@link MatchedPath#getPathMatch()} instead.
*/
@Deprecated
@Override @Override
public String getPathMatch(String path) public String getPathMatch(String path)
{ {
@ -227,7 +253,7 @@ public class ServletPathSpec extends AbstractPathSpec
case PREFIX_GLOB: case PREFIX_GLOB:
if (isWildcardMatch(path)) if (isWildcardMatch(path))
return path.substring(0, _specLength - 2); return path.substring(0, _matchLength);
return null; return null;
case SUFFIX_GLOB: case SUFFIX_GLOB:
@ -264,12 +290,44 @@ public class ServletPathSpec extends AbstractPathSpec
private boolean isWildcardMatch(String path) private boolean isWildcardMatch(String path)
{ {
// For a spec of "/foo/*" match "/foo" , "/foo/..." but not "/foobar" // For a spec of "/foo/*" match "/foo" , "/foo/..." but not "/foobar"
int cpl = _specLength - 2; if (_group == PathSpecGroup.PREFIX_GLOB && path.length() >= _matchLength && path.regionMatches(0, _declaration, 0, _matchLength))
if ((_group == PathSpecGroup.PREFIX_GLOB) && (path.regionMatches(0, _declaration, 0, cpl))) return path.length() == _matchLength || path.charAt(_matchLength) == '/';
return (path.length() == cpl) || ('/' == path.charAt(cpl));
return false; return false;
} }
@Override
public MatchedPath matched(String path)
{
switch (_group)
{
case EXACT:
if (_declaration.equals(path))
return _preMatchedPath;
break;
case PREFIX_GLOB:
if (isWildcardMatch(path))
{
if (path.length() == _matchLength)
return _preMatchedPath;
return MatchedPath.from(path.substring(0, _matchLength), path.substring(_matchLength));
}
break;
case SUFFIX_GLOB:
if (path.regionMatches((path.length() - _specLength) + 1, _declaration, 1, _specLength - 1))
return MatchedPath.from(path, null);
break;
case ROOT:
// Only "/" matches
if ("/".equals(path))
return _preMatchedPath;
break;
case DEFAULT:
// If we reached this point, then everything matches
return MatchedPath.from(path, null);
}
return null;
}
@Override @Override
public boolean matches(String path) public boolean matches(String path)
{ {

View File

@ -202,6 +202,12 @@ public class UriTemplatePathSpec extends AbstractPathSpec
_pattern = pattern; _pattern = pattern;
_variables = variables; _variables = variables;
_logicalDeclaration = logicalSignature.toString(); _logicalDeclaration = logicalSignature.toString();
if (LOG.isDebugEnabled())
{
LOG.debug("Creating UriTemplatePathSpec[{}] (regex: \"{}\", signature: [{}], group: {}, variables: [{}])",
_declaration, regex, sig, _group, String.join(", ", _variables));
}
} }
/** /**
@ -301,7 +307,9 @@ public class UriTemplatePathSpec extends AbstractPathSpec
Map<String, String> ret = new HashMap<>(); Map<String, String> ret = new HashMap<>();
int groupCount = matcher.groupCount(); int groupCount = matcher.groupCount();
for (int i = 1; i <= groupCount; i++) for (int i = 1; i <= groupCount; i++)
{
ret.put(_variables[i - 1], matcher.group(i)); ret.put(_variables[i - 1], matcher.group(i));
}
return ret; return ret;
} }
return null; return null;
@ -309,8 +317,18 @@ public class UriTemplatePathSpec extends AbstractPathSpec
protected Matcher getMatcher(String path) protected Matcher getMatcher(String path)
{ {
int idx = path.indexOf('?');
if (idx >= 0)
{
// match only non-query part
return _pattern.matcher(path.substring(0, idx));
}
else
{
// match entire path
return _pattern.matcher(path); return _pattern.matcher(path);
} }
}
@Override @Override
public int getSpecLength() public int getSpecLength()
@ -399,17 +417,18 @@ public class UriTemplatePathSpec extends AbstractPathSpec
@Override @Override
public boolean matches(final String path) public boolean matches(final String path)
{ {
int idx = path.indexOf('?');
if (idx >= 0)
{
// match only non-query part
return getMatcher(path.substring(0, idx)).matches();
}
else
{
// match entire path
return getMatcher(path).matches(); return getMatcher(path).matches();
} }
@Override
public MatchedPath matched(String path)
{
Matcher matcher = getMatcher(path);
if (matcher.matches())
{
return new UriTemplatePathSpec.UriTemplateMatchedPath(this, path, matcher);
}
return null;
} }
public int getVariableCount() public int getVariableCount()
@ -421,4 +440,62 @@ public class UriTemplatePathSpec extends AbstractPathSpec
{ {
return _variables; return _variables;
} }
private static class UriTemplateMatchedPath implements MatchedPath
{
private final UriTemplatePathSpec pathSpec;
private final String path;
private final Matcher matcher;
public UriTemplateMatchedPath(UriTemplatePathSpec uriTemplatePathSpec, String path, Matcher matcher)
{
this.pathSpec = uriTemplatePathSpec;
this.path = path;
this.matcher = matcher;
}
@Override
public String getPathMatch()
{
// TODO: UriTemplatePathSpec has no concept of prefix/suffix, this should be simplified
// TODO: Treat all UriTemplatePathSpec matches as exact when it comes to pathMatch/pathInfo
if (pathSpec.getGroup() == PathSpecGroup.PREFIX_GLOB && matcher.groupCount() >= 1)
{
int idx = matcher.start(1);
if (idx > 0)
{
if (path.charAt(idx - 1) == '/')
idx--;
return path.substring(0, idx);
}
}
return path;
}
@Override
public String getPathInfo()
{
// TODO: UriTemplatePathSpec has no concept of prefix/suffix, this should be simplified
// TODO: Treat all UriTemplatePathSpec matches as exact when it comes to pathMatch/pathInfo
if (pathSpec.getGroup() == PathSpecGroup.PREFIX_GLOB && matcher.groupCount() >= 1)
{
String pathInfo = matcher.group(1);
if ("".equals(pathInfo))
return "/";
else
return pathInfo;
}
return null;
}
@Override
public String toString()
{
return getClass().getSimpleName() + "[" +
"pathSpec=" + pathSpec +
", path=\"" + path + "\"" +
", matcher=" + matcher +
']';
}
}
} }

View File

@ -19,8 +19,6 @@
package org.eclipse.jetty.http.pathmap; package org.eclipse.jetty.http.pathmap;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.is;
@ -29,32 +27,23 @@ import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.nullValue;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
// @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck // @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
public class PathMappingsTest public class PathMappingsTest
{ {
private void assertMatch(PathMappings<String> pathmap, String path, String expectedValue) private void assertMatch(PathMappings<String> pathmap, String path, String expectedValue)
{ {
String msg = String.format(".getMatch(\"%s\")", path); String msg = String.format(".getMatched(\"%s\")", path);
MappedResource<String> match = pathmap.getMatch(path); MatchedResource<String> matched = pathmap.getMatched(path);
assertThat(msg, match, notNullValue()); assertThat(msg, matched, notNullValue());
String actualMatch = match.getResource(); String actualMatch = matched.getResource();
assertEquals(expectedValue, actualMatch, msg); assertEquals(expectedValue, actualMatch, msg);
} }
public void dumpMappings(PathMappings<String> p)
{
for (MappedResource<String> res : p)
{
System.out.printf(" %s%n", res);
}
}
/** /**
* Test the match order rules with a mixed Servlet and regex path specs * Test the match order rules with a mixed Servlet and regex path specs
* <p> *
* <ul> * <ul>
* <li>Exact match</li> * <li>Exact match</li>
* <li>Longest prefix match</li> * <li>Longest prefix match</li>
@ -75,8 +64,6 @@ public class PathMappingsTest
p.put(new RegexPathSpec("^/animal/.*/cam$"), "animalCam"); p.put(new RegexPathSpec("^/animal/.*/cam$"), "animalCam");
p.put(new RegexPathSpec("^/entrance/cam$"), "entranceCam"); p.put(new RegexPathSpec("^/entrance/cam$"), "entranceCam");
// dumpMappings(p);
assertMatch(p, "/animal/bird/eagle", "birds"); assertMatch(p, "/animal/bird/eagle", "birds");
assertMatch(p, "/animal/fish/bass/sea", "fishes"); assertMatch(p, "/animal/fish/bass/sea", "fishes");
assertMatch(p, "/animal/peccary/javalina/evolution", "animals"); assertMatch(p, "/animal/peccary/javalina/evolution", "animals");
@ -107,7 +94,7 @@ public class PathMappingsTest
/** /**
* Test the match order rules with a mixed Servlet and URI Template path specs * Test the match order rules with a mixed Servlet and URI Template path specs
* <p> *
* <ul> * <ul>
* <li>Exact match</li> * <li>Exact match</li>
* <li>Longest prefix match</li> * <li>Longest prefix match</li>
@ -127,8 +114,6 @@ public class PathMappingsTest
p.put(new UriTemplatePathSpec("/animal/{type}/{name}/cam"), "animalCam"); p.put(new UriTemplatePathSpec("/animal/{type}/{name}/cam"), "animalCam");
p.put(new UriTemplatePathSpec("/entrance/cam"), "entranceCam"); p.put(new UriTemplatePathSpec("/entrance/cam"), "entranceCam");
// dumpMappings(p);
assertMatch(p, "/animal/bird/eagle", "birds"); assertMatch(p, "/animal/bird/eagle", "birds");
assertMatch(p, "/animal/fish/bass/sea", "fishes"); assertMatch(p, "/animal/fish/bass/sea", "fishes");
assertMatch(p, "/animal/peccary/javalina/evolution", "animals"); assertMatch(p, "/animal/peccary/javalina/evolution", "animals");
@ -141,7 +126,7 @@ public class PathMappingsTest
/** /**
* Test the match order rules for URI Template based specs * Test the match order rules for URI Template based specs
* <p> *
* <ul> * <ul>
* <li>Exact match</li> * <li>Exact match</li>
* <li>Longest prefix match</li> * <li>Longest prefix match</li>
@ -159,8 +144,6 @@ public class PathMappingsTest
p.put(new UriTemplatePathSpec("/{var1}/d"), "endpointD"); p.put(new UriTemplatePathSpec("/{var1}/d"), "endpointD");
p.put(new UriTemplatePathSpec("/b/{var2}"), "endpointE"); p.put(new UriTemplatePathSpec("/b/{var2}"), "endpointE");
// dumpMappings(p);
assertMatch(p, "/a/b/c", "endpointB"); assertMatch(p, "/a/b/c", "endpointB");
assertMatch(p, "/a/d/c", "endpointA"); assertMatch(p, "/a/d/c", "endpointA");
assertMatch(p, "/a/x/y", "endpointC"); assertMatch(p, "/a/x/y", "endpointC");
@ -168,8 +151,28 @@ public class PathMappingsTest
assertMatch(p, "/b/d", "endpointE"); assertMatch(p, "/b/d", "endpointE");
} }
/**
* Test the match order rules for mixed Servlet and Regex path specs
*/
@Test @Test
public void testPathMap() throws Exception public void testServletAndRegexMatchOrder()
{
PathMappings<String> p = new PathMappings<>();
p.put(new ServletPathSpec("/a/*"), "endpointA");
p.put(new RegexPathSpec("^.*/middle/.*$"), "middle");
p.put(new ServletPathSpec("*.do"), "endpointDo");
p.put(new ServletPathSpec("/"), "default");
assertMatch(p, "/a/b/c", "endpointA");
assertMatch(p, "/a/middle/c", "endpointA");
assertMatch(p, "/b/middle/c", "middle");
assertMatch(p, "/x/y.do", "endpointDo");
assertMatch(p, "/b/d", "default");
}
@Test
public void testPathMap()
{ {
PathMappings<String> p = new PathMappings<>(); PathMappings<String> p = new PathMappings<>();
@ -183,76 +186,40 @@ public class PathMappingsTest
p.put(new ServletPathSpec("/"), "8"); p.put(new ServletPathSpec("/"), "8");
// p.put(new ServletPathSpec("/XXX:/YYY"), "9"); // special syntax from Jetty 3.1.x // p.put(new ServletPathSpec("/XXX:/YYY"), "9"); // special syntax from Jetty 3.1.x
p.put(new ServletPathSpec(""), "10"); p.put(new ServletPathSpec(""), "10");
// @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
p.put(new ServletPathSpec("/\u20ACuro/*"), "11"); p.put(new ServletPathSpec("/\u20ACuro/*"), "11");
// @checkstyle-enable-check : AvoidEscapedUnicodeCharactersCheck
assertEquals("/Foo/bar", new ServletPathSpec("/Foo/bar").getPathMatch("/Foo/bar"), "pathMatch exact");
assertEquals("/Foo", new ServletPathSpec("/Foo/*").getPathMatch("/Foo/bar"), "pathMatch prefix");
assertEquals("/Foo", new ServletPathSpec("/Foo/*").getPathMatch("/Foo/"), "pathMatch prefix");
assertEquals("/Foo", new ServletPathSpec("/Foo/*").getPathMatch("/Foo"), "pathMatch prefix");
assertEquals("/Foo/bar.ext", new ServletPathSpec("*.ext").getPathMatch("/Foo/bar.ext"), "pathMatch suffix");
assertEquals("/Foo/bar.ext", new ServletPathSpec("/").getPathMatch("/Foo/bar.ext"), "pathMatch default");
assertEquals(null, new ServletPathSpec("/Foo/bar").getPathInfo("/Foo/bar"), "pathInfo exact");
assertEquals("/bar", new ServletPathSpec("/Foo/*").getPathInfo("/Foo/bar"), "pathInfo prefix");
assertEquals("/*", new ServletPathSpec("/Foo/*").getPathInfo("/Foo/*"), "pathInfo prefix");
assertEquals("/", new ServletPathSpec("/Foo/*").getPathInfo("/Foo/"), "pathInfo prefix");
assertEquals(null, new ServletPathSpec("/Foo/*").getPathInfo("/Foo"), "pathInfo prefix");
assertEquals(null, new ServletPathSpec("*.ext").getPathInfo("/Foo/bar.ext"), "pathInfo suffix");
assertEquals(null, new ServletPathSpec("/").getPathInfo("/Foo/bar.ext"), "pathInfo default");
p.put(new ServletPathSpec("/*"), "0"); p.put(new ServletPathSpec("/*"), "0");
// assertEquals("1", p.get("/abs/path"), "Get absolute path"); assertEquals("/abs/path", p.getMatched("/abs/path").getPathSpec().getDeclaration(), "Match absolute path");
assertEquals("/abs/path", p.getMatch("/abs/path").getPathSpec().getDeclaration(), "Match absolute path"); assertEquals("1", p.getMatched("/abs/path").getResource(), "Match absolute path");
assertEquals("1", p.getMatch("/abs/path").getResource(), "Match absolute path"); assertEquals("0", p.getMatched("/abs/path/xxx").getResource(), "Mismatch absolute path");
assertEquals("0", p.getMatch("/abs/path/xxx").getResource(), "Mismatch absolute path"); assertEquals("0", p.getMatched("/abs/pith").getResource(), "Mismatch absolute path");
assertEquals("0", p.getMatch("/abs/pith").getResource(), "Mismatch absolute path"); assertEquals("2", p.getMatched("/abs/path/longer").getResource(), "Match longer absolute path");
assertEquals("2", p.getMatch("/abs/path/longer").getResource(), "Match longer absolute path"); assertEquals("0", p.getMatched("/abs/path/").getResource(), "Not exact absolute path");
assertEquals("0", p.getMatch("/abs/path/").getResource(), "Not exact absolute path"); assertEquals("0", p.getMatched("/abs/path/xxx").getResource(), "Not exact absolute path");
assertEquals("0", p.getMatch("/abs/path/xxx").getResource(), "Not exact absolute path");
assertEquals("3", p.getMatch("/animal/bird/eagle/bald").getResource(), "Match longest prefix"); assertEquals("3", p.getMatched("/animal/bird/eagle/bald").getResource(), "Match longest prefix");
assertEquals("4", p.getMatch("/animal/fish/shark/grey").getResource(), "Match longest prefix"); assertEquals("4", p.getMatched("/animal/fish/shark/grey").getResource(), "Match longest prefix");
assertEquals("5", p.getMatch("/animal/insect/bug").getResource(), "Match longest prefix"); assertEquals("5", p.getMatched("/animal/insect/bug").getResource(), "Match longest prefix");
assertEquals("5", p.getMatch("/animal").getResource(), "mismatch exact prefix"); assertEquals("5", p.getMatched("/animal").getResource(), "mismatch exact prefix");
assertEquals("5", p.getMatch("/animal/").getResource(), "mismatch exact prefix"); assertEquals("5", p.getMatched("/animal/").getResource(), "mismatch exact prefix");
assertEquals("0", p.getMatch("/suffix/path.tar.gz").getResource(), "Match longest suffix"); assertEquals("0", p.getMatched("/suffix/path.tar.gz").getResource(), "Match longest suffix");
assertEquals("0", p.getMatch("/suffix/path.gz").getResource(), "Match longest suffix"); assertEquals("0", p.getMatched("/suffix/path.gz").getResource(), "Match longest suffix");
assertEquals("5", p.getMatch("/animal/path.gz").getResource(), "prefix rather than suffix"); assertEquals("5", p.getMatched("/animal/path.gz").getResource(), "prefix rather than suffix");
assertEquals("0", p.getMatch("/Other/path").getResource(), "default"); assertEquals("0", p.getMatched("/Other/path").getResource(), "default");
assertEquals("", new ServletPathSpec("/*").getPathMatch("/xxx/zzz"), "pathMatch /*"); assertEquals("10", p.getMatched("/").getResource(), "match / with ''");
assertEquals("/xxx/zzz", new ServletPathSpec("/*").getPathInfo("/xxx/zzz"), "pathInfo /*");
assertTrue(new ServletPathSpec("/").matches("/anything"), "match /");
assertTrue(new ServletPathSpec("/*").matches("/anything"), "match /*");
assertTrue(new ServletPathSpec("/foo").matches("/foo"), "match /foo");
assertTrue(!new ServletPathSpec("/foo").matches("/bar"), "!match /foo");
assertTrue(new ServletPathSpec("/foo/*").matches("/foo"), "match /foo/*");
assertTrue(new ServletPathSpec("/foo/*").matches("/foo/"), "match /foo/*");
assertTrue(new ServletPathSpec("/foo/*").matches("/foo/anything"), "match /foo/*");
assertTrue(!new ServletPathSpec("/foo/*").matches("/bar"), "!match /foo/*");
assertTrue(!new ServletPathSpec("/foo/*").matches("/bar/"), "!match /foo/*");
assertTrue(!new ServletPathSpec("/foo/*").matches("/bar/anything"), "!match /foo/*");
assertTrue(new ServletPathSpec("*.foo").matches("anything.foo"), "match *.foo");
assertTrue(!new ServletPathSpec("*.foo").matches("anything.bar"), "!match *.foo");
assertTrue(new ServletPathSpec("/On*").matches("/On*"), "match /On*");
assertTrue(!new ServletPathSpec("/On*").matches("/One"), "!match /One");
assertEquals("10", p.getMatch("/").getResource(), "match / with ''");
assertTrue(new ServletPathSpec("").matches("/"), "match \"\"");
} }
/** /**
* See JIRA issue: JETTY-88. * See JIRA issue: JETTY-88.
*
* @throws Exception failed test
*/ */
@Test @Test
public void testPathMappingsOnlyMatchOnDirectoryNames() throws Exception public void testPathMappingsOnlyMatchOnDirectoryNames()
{ {
ServletPathSpec spec = new ServletPathSpec("/xyz/*"); ServletPathSpec spec = new ServletPathSpec("/xyz/*");
@ -271,40 +238,25 @@ public class PathMappingsTest
} }
@Test @Test
public void testPrecidenceVsOrdering() throws Exception public void testPrecedenceVsOrdering()
{ {
PathMappings<String> p = new PathMappings<>(); PathMappings<String> p = new PathMappings<>();
p.put(new ServletPathSpec("/dump/gzip/*"), "prefix"); p.put(new ServletPathSpec("/dump/gzip/*"), "prefix");
p.put(new ServletPathSpec("*.txt"), "suffix"); p.put(new ServletPathSpec("*.txt"), "suffix");
assertEquals(null, p.getMatch("/foo/bar")); assertNull(p.getMatched("/foo/bar"));
assertEquals("prefix", p.getMatch("/dump/gzip/something").getResource()); assertEquals("prefix", p.getMatched("/dump/gzip/something").getResource());
assertEquals("suffix", p.getMatch("/foo/something.txt").getResource()); assertEquals("suffix", p.getMatched("/foo/something.txt").getResource());
assertEquals("prefix", p.getMatch("/dump/gzip/something.txt").getResource()); assertEquals("prefix", p.getMatched("/dump/gzip/something.txt").getResource());
p = new PathMappings<>(); p = new PathMappings<>();
p.put(new ServletPathSpec("*.txt"), "suffix"); p.put(new ServletPathSpec("*.txt"), "suffix");
p.put(new ServletPathSpec("/dump/gzip/*"), "prefix"); p.put(new ServletPathSpec("/dump/gzip/*"), "prefix");
assertEquals(null, p.getMatch("/foo/bar")); assertNull(p.getMatched("/foo/bar"));
assertEquals("prefix", p.getMatch("/dump/gzip/something").getResource()); assertEquals("prefix", p.getMatched("/dump/gzip/something").getResource());
assertEquals("suffix", p.getMatch("/foo/something.txt").getResource()); assertEquals("suffix", p.getMatched("/foo/something.txt").getResource());
assertEquals("prefix", p.getMatch("/dump/gzip/something.txt").getResource()); assertEquals("prefix", p.getMatched("/dump/gzip/something.txt").getResource());
}
@ParameterizedTest
@ValueSource(strings = {
"*",
"/foo/*/bar",
"*/foo",
"*.foo/*"
})
public void testBadPathSpecs(String str)
{
assertThrows(IllegalArgumentException.class, () ->
{
new ServletPathSpec(str);
});
} }
@Test @Test
@ -315,8 +267,8 @@ public class PathMappingsTest
assertThat(p.put(new UriTemplatePathSpec("/a/{var2}/c"), "resourceAA"), is(false)); assertThat(p.put(new UriTemplatePathSpec("/a/{var2}/c"), "resourceAA"), is(false));
assertThat(p.put(new UriTemplatePathSpec("/a/b/c"), "resourceB"), is(true)); assertThat(p.put(new UriTemplatePathSpec("/a/b/c"), "resourceB"), is(true));
assertThat(p.put(new UriTemplatePathSpec("/a/b/c"), "resourceBB"), is(false)); assertThat(p.put(new UriTemplatePathSpec("/a/b/c"), "resourceBB"), is(false));
assertThat(p.put(new ServletPathSpec("/a/b/c"), "resourceBB"), is(false)); assertThat(p.put(new ServletPathSpec("/a/b/c"), "resourceBB"), is(true));
assertThat(p.put(new RegexPathSpec("/a/b/c"), "resourceBB"), is(false)); assertThat(p.put(new RegexPathSpec("/a/b/c"), "resourceBB"), is(true));
assertThat(p.put(new ServletPathSpec("/*"), "resourceC"), is(true)); assertThat(p.put(new ServletPathSpec("/*"), "resourceC"), is(true));
assertThat(p.put(new RegexPathSpec("/(.*)"), "resourceCC"), is(true)); assertThat(p.put(new RegexPathSpec("/(.*)"), "resourceCC"), is(true));
@ -466,14 +418,14 @@ public class PathMappingsTest
@Test @Test
public void testAsPathSpec() public void testAsPathSpec()
{ {
assertThat(PathMappings.asPathSpec(""), instanceOf(ServletPathSpec.class)); assertThat(PathSpec.from(""), instanceOf(ServletPathSpec.class));
assertThat(PathMappings.asPathSpec("/"), instanceOf(ServletPathSpec.class)); assertThat(PathSpec.from("/"), instanceOf(ServletPathSpec.class));
assertThat(PathMappings.asPathSpec("/*"), instanceOf(ServletPathSpec.class)); assertThat(PathSpec.from("/*"), instanceOf(ServletPathSpec.class));
assertThat(PathMappings.asPathSpec("/foo/*"), instanceOf(ServletPathSpec.class)); assertThat(PathSpec.from("/foo/*"), instanceOf(ServletPathSpec.class));
assertThat(PathMappings.asPathSpec("*.jsp"), instanceOf(ServletPathSpec.class)); assertThat(PathSpec.from("*.jsp"), instanceOf(ServletPathSpec.class));
assertThat(PathMappings.asPathSpec("^$"), instanceOf(RegexPathSpec.class)); assertThat(PathSpec.from("^$"), instanceOf(RegexPathSpec.class));
assertThat(PathMappings.asPathSpec("^.*"), instanceOf(RegexPathSpec.class)); assertThat(PathSpec.from("^.*"), instanceOf(RegexPathSpec.class));
assertThat(PathMappings.asPathSpec("^/"), instanceOf(RegexPathSpec.class)); assertThat(PathSpec.from("^/"), instanceOf(RegexPathSpec.class));
} }
} }

View File

@ -26,6 +26,8 @@ import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.nullValue;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
public class RegexPathSpecTest public class RegexPathSpecTest
@ -33,13 +35,13 @@ public class RegexPathSpecTest
public static void assertMatches(PathSpec spec, String path) public static void assertMatches(PathSpec spec, String path)
{ {
String msg = String.format("Spec(\"%s\").matches(\"%s\")", spec.getDeclaration(), path); String msg = String.format("Spec(\"%s\").matches(\"%s\")", spec.getDeclaration(), path);
assertThat(msg, spec.matches(path), is(true)); assertNotNull(spec.matched(path), msg);
} }
public static void assertNotMatches(PathSpec spec, String path) public static void assertNotMatches(PathSpec spec, String path)
{ {
String msg = String.format("!Spec(\"%s\").matches(\"%s\")", spec.getDeclaration(), path); String msg = String.format("!Spec(\"%s\").matches(\"%s\")", spec.getDeclaration(), path);
assertThat(msg, spec.matches(path), is(false)); assertNull(spec.matched(path), msg);
} }
@Test @Test
@ -151,6 +153,64 @@ public class RegexPathSpecTest
assertNotMatches(spec, "/aa/bb.do/more"); assertNotMatches(spec, "/aa/bb.do/more");
} }
@Test
public void testSuffixSpecMiddle()
{
RegexPathSpec spec = new RegexPathSpec("^.*/middle/.*$");
assertEquals("^.*/middle/.*$", spec.getDeclaration(), "Spec.pathSpec");
assertEquals("^.*/middle/.*$", spec.getPattern().pattern(), "Spec.pattern");
assertEquals(2, spec.getPathDepth(), "Spec.pathDepth");
assertEquals(PathSpecGroup.SUFFIX_GLOB, spec.getGroup(), "Spec.group");
assertMatches(spec, "/a/middle/c.do");
assertMatches(spec, "/a/b/c/d/middle/e/f");
assertMatches(spec, "/middle/");
assertNotMatches(spec, "/a.do");
assertNotMatches(spec, "/a/middle");
assertNotMatches(spec, "/middle");
}
@Test
public void testSuffixSpecMiddleWithGroupings()
{
RegexPathSpec spec = new RegexPathSpec("^(.*)/middle/(.*)$");
assertEquals("^(.*)/middle/(.*)$", spec.getDeclaration(), "Spec.pathSpec");
assertEquals("^(.*)/middle/(.*)$", spec.getPattern().pattern(), "Spec.pattern");
assertEquals(2, spec.getPathDepth(), "Spec.pathDepth");
assertEquals(PathSpecGroup.SUFFIX_GLOB, spec.getGroup(), "Spec.group");
assertMatches(spec, "/a/middle/c.do");
assertMatches(spec, "/a/b/c/d/middle/e/f");
assertMatches(spec, "/middle/");
assertNotMatches(spec, "/a.do");
assertNotMatches(spec, "/a/middle");
assertNotMatches(spec, "/middle");
}
@Test
public void testNamedRegexGroup()
{
RegexPathSpec spec = new RegexPathSpec("^(?<name>(.*)/middle/)(?<info>.*)$");
assertEquals("^(?<name>(.*)/middle/)(?<info>.*)$", spec.getDeclaration(), "Spec.pathSpec");
assertEquals("^(?<name>(.*)/middle/)(?<info>.*)$", spec.getPattern().pattern(), "Spec.pattern");
assertEquals(2, spec.getPathDepth(), "Spec.pathDepth");
assertEquals(PathSpecGroup.SUFFIX_GLOB, spec.getGroup(), "Spec.group");
assertMatches(spec, "/a/middle/c.do");
assertMatches(spec, "/a/b/c/d/middle/e/f");
assertMatches(spec, "/middle/");
assertNotMatches(spec, "/a.do");
assertNotMatches(spec, "/a/middle");
assertNotMatches(spec, "/middle");
MatchedPath matchedPath = spec.matched("/a/middle/c.do");
assertThat(matchedPath.getPathMatch(), is("/a/middle/"));
assertThat(matchedPath.getPathInfo(), is("c.do"));
}
@Test @Test
public void testEquals() public void testEquals()
{ {

View File

@ -29,7 +29,7 @@ import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
/** /**
* Tests of {@link PathMappings#getMatch(String)}, with a focus on correct mapping selection order * Tests of {@link PathMappings#getMatched(String)}, with a focus on correct mapping selection order
*/ */
@SuppressWarnings("Duplicates") @SuppressWarnings("Duplicates")
public class ServletPathSpecOrderTest public class ServletPathSpecOrderTest
@ -58,6 +58,7 @@ public class ServletPathSpecOrderTest
data.add(Arguments.of("/Other/path", "default")); data.add(Arguments.of("/Other/path", "default"));
// @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck // @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
data.add(Arguments.of("/\u20ACuro/path", "money")); data.add(Arguments.of("/\u20ACuro/path", "money"));
// @checkstyle-enable-check : AvoidEscapedUnicodeCharactersCheck
data.add(Arguments.of("/", "root")); data.add(Arguments.of("/", "root"));
// Extra tests // Extra tests
@ -92,6 +93,6 @@ public class ServletPathSpecOrderTest
@MethodSource("data") @MethodSource("data")
public void testMatch(String inputPath, String expectedResource) public void testMatch(String inputPath, String expectedResource)
{ {
assertThat("Match on [" + inputPath + "]", mappings.getMatch(inputPath).getResource(), is(expectedResource)); assertThat("Match on [" + inputPath + "]", mappings.getMatched(inputPath).getResource(), is(expectedResource));
} }
} }

View File

@ -19,70 +19,48 @@
package org.eclipse.jetty.http.pathmap; package org.eclipse.jetty.http.pathmap;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class ServletPathSpecTest public class ServletPathSpecTest
{ {
private void assertBadServletPathSpec(String pathSpec)
{
try
{
new ServletPathSpec(pathSpec);
fail("Expected IllegalArgumentException for a bad servlet pathspec on: " + pathSpec);
}
catch (IllegalArgumentException e)
{
// expected path
System.out.println(e);
}
}
private void assertMatches(ServletPathSpec spec, String path) private void assertMatches(ServletPathSpec spec, String path)
{ {
String msg = String.format("Spec(\"%s\").matches(\"%s\")", spec.getDeclaration(), path); String msg = String.format("Spec(\"%s\").matches(\"%s\")", spec.getDeclaration(), path);
assertThat(msg, spec.matches(path), is(true)); assertThat(msg, spec.matched(path), not(nullValue()));
} }
private void assertNotMatches(ServletPathSpec spec, String path) private void assertNotMatches(ServletPathSpec spec, String path)
{ {
String msg = String.format("!Spec(\"%s\").matches(\"%s\")", spec.getDeclaration(), path); String msg = String.format("!Spec(\"%s\").matches(\"%s\")", spec.getDeclaration(), path);
assertThat(msg, spec.matches(path), is(false)); assertThat(msg, spec.matched(path), is(nullValue()));
} }
@Test @ParameterizedTest
public void testBadServletPathSpecA() @ValueSource(strings = {
"foo",
"/foo/*.do",
"foo/*.do",
"foo/*.*do",
"*",
"*do",
"/foo/*/bar",
"*/foo",
"*.foo/*"
})
public void testBadPathSpecs(String str)
{ {
assertBadServletPathSpec("foo"); assertThrows(IllegalArgumentException.class, () -> new ServletPathSpec(str));
}
@Test
public void testBadServletPathSpecB()
{
assertBadServletPathSpec("/foo/*.do");
}
@Test
public void testBadServletPathSpecC()
{
assertBadServletPathSpec("foo/*.do");
}
@Test
public void testBadServletPathSpecD()
{
assertBadServletPathSpec("foo/*.*do");
}
@Test
public void testBadServletPathSpecE()
{
assertBadServletPathSpec("*do");
} }
@Test @Test
@ -112,16 +90,27 @@ public class ServletPathSpecTest
@Test @Test
public void testGetPathInfo() public void testGetPathInfo()
{ {
assertEquals(null, new ServletPathSpec("/Foo/bar").getPathInfo("/Foo/bar"), "pathInfo exact"); ServletPathSpec spec = new ServletPathSpec("/Foo/bar");
assertEquals("/bar", new ServletPathSpec("/Foo/*").getPathInfo("/Foo/bar"), "pathInfo prefix"); assertThat("PathInfo exact", spec.matched("/Foo/bar").getPathInfo(), is(nullValue()));
assertEquals("/*", new ServletPathSpec("/Foo/*").getPathInfo("/Foo/*"), "pathInfo prefix");
assertEquals("/", new ServletPathSpec("/Foo/*").getPathInfo("/Foo/"), "pathInfo prefix"); spec = new ServletPathSpec("/Foo/*");
assertEquals(null, new ServletPathSpec("/Foo/*").getPathInfo("/Foo"), "pathInfo prefix"); assertThat("PathInfo prefix", spec.matched("/Foo/bar").getPathInfo(), is("/bar"));
assertEquals(null, new ServletPathSpec("*.ext").getPathInfo("/Foo/bar.ext"), "pathInfo suffix"); assertThat("PathInfo prefix", spec.matched("/Foo/*").getPathInfo(), is("/*"));
assertEquals(null, new ServletPathSpec("/").getPathInfo("/Foo/bar.ext"), "pathInfo default"); assertThat("PathInfo prefix", spec.matched("/Foo/").getPathInfo(), is("/"));
assertEquals("/", new ServletPathSpec("").getPathInfo("/"), "pathInfo root"); assertThat("PathInfo prefix", spec.matched("/Foo").getPathInfo(), is(nullValue()));
assertEquals("", new ServletPathSpec("").getPathInfo(""), "pathInfo root");
assertEquals("/xxx/zzz", new ServletPathSpec("/*").getPathInfo("/xxx/zzz"), "pathInfo default"); spec = new ServletPathSpec("*.ext");
assertThat("PathInfo suffix", spec.matched("/Foo/bar.ext").getPathInfo(), is(nullValue()));
spec = new ServletPathSpec("/");
assertThat("PathInfo default", spec.matched("/Foo/bar.ext").getPathInfo(), is(nullValue()));
spec = new ServletPathSpec("");
assertThat("PathInfo root", spec.matched("/").getPathInfo(), is("/"));
assertThat("PathInfo root", spec.matched(""), is(nullValue())); // does not match // TODO: verify with greg
spec = new ServletPathSpec("/*");
assertThat("PathInfo default", spec.matched("/xxx/zzz").getPathInfo(), is("/xxx/zzz"));
} }
@Test @Test
@ -143,15 +132,26 @@ public class ServletPathSpecTest
@Test @Test
public void testPathMatch() public void testPathMatch()
{ {
assertEquals("/Foo/bar", new ServletPathSpec("/Foo/bar").getPathMatch("/Foo/bar"), "pathMatch exact"); ServletPathSpec spec = new ServletPathSpec("/Foo/bar");
assertEquals("/Foo", new ServletPathSpec("/Foo/*").getPathMatch("/Foo/bar"), "pathMatch prefix"); assertThat("PathMatch exact", spec.matched("/Foo/bar").getPathMatch(), is("/Foo/bar"));
assertEquals("/Foo", new ServletPathSpec("/Foo/*").getPathMatch("/Foo/"), "pathMatch prefix");
assertEquals("/Foo", new ServletPathSpec("/Foo/*").getPathMatch("/Foo"), "pathMatch prefix"); spec = new ServletPathSpec("/Foo/*");
assertEquals("/Foo/bar.ext", new ServletPathSpec("*.ext").getPathMatch("/Foo/bar.ext"), "pathMatch suffix"); assertThat("PathMatch prefix", spec.matched("/Foo/bar").getPathMatch(), is("/Foo"));
assertEquals("/Foo/bar.ext", new ServletPathSpec("/").getPathMatch("/Foo/bar.ext"), "pathMatch default"); assertThat("PathMatch prefix", spec.matched("/Foo/").getPathMatch(), is("/Foo"));
assertEquals("", new ServletPathSpec("").getPathMatch("/"), "pathInfo root"); assertThat("PathMatch prefix", spec.matched("/Foo").getPathMatch(), is("/Foo"));
assertEquals("", new ServletPathSpec("").getPathMatch(""), "pathInfo root");
assertEquals("", new ServletPathSpec("/*").getPathMatch("/xxx/zzz"), "pathMatch default"); spec = new ServletPathSpec("*.ext");
assertThat("PathMatch suffix", spec.matched("/Foo/bar.ext").getPathMatch(), is("/Foo/bar.ext"));
spec = new ServletPathSpec("/");
assertThat("PathMatch default", spec.matched("/Foo/bar.ext").getPathMatch(), is("/Foo/bar.ext"));
spec = new ServletPathSpec("");
assertThat("PathMatch root", spec.matched("/").getPathMatch(), is(""));
assertThat("PathMatch root", spec.matched(""), is(nullValue())); // does not match // TODO: verify with greg
spec = new ServletPathSpec("/*");
assertThat("PathMatch default", spec.matched("/xxx/zzz").getPathMatch(), is(""));
} }
@Test @Test
@ -168,9 +168,39 @@ public class ServletPathSpecTest
assertMatches(spec, "/downloads"); assertMatches(spec, "/downloads");
assertEquals("/", spec.getPathInfo("/downloads/"), "Spec.pathInfo"); MatchedPath matched = spec.matched("/downloads/");
assertEquals("/distribution.zip", spec.getPathInfo("/downloads/distribution.zip"), "Spec.pathInfo"); assertThat("matched.pathMatch", matched.getPathMatch(), is("/downloads"));
assertEquals("/dist/9.0/distribution.tar.gz", spec.getPathInfo("/downloads/dist/9.0/distribution.tar.gz"), "Spec.pathInfo"); assertThat("matched.pathInfo", matched.getPathInfo(), is("/"));
matched = spec.matched("/downloads/distribution.zip");
assertThat("matched.pathMatch", matched.getPathMatch(), is("/downloads"));
assertThat("matched.pathInfo", matched.getPathInfo(), is("/distribution.zip"));
matched = spec.matched("/downloads/dist/9.0/distribution.tar.gz");
assertThat("matched.pathMatch", matched.getPathMatch(), is("/downloads"));
assertThat("matched.pathInfo", matched.getPathInfo(), is("/dist/9.0/distribution.tar.gz"));
}
@Test
public void testMatches()
{
assertTrue(new ServletPathSpec("/").matches("/anything"), "match /");
assertTrue(new ServletPathSpec("/*").matches("/anything"), "match /*");
assertTrue(new ServletPathSpec("/foo").matches("/foo"), "match /foo");
assertFalse(new ServletPathSpec("/foo").matches("/bar"), "!match /foo");
assertTrue(new ServletPathSpec("/foo/*").matches("/foo"), "match /foo/*");
assertTrue(new ServletPathSpec("/foo/*").matches("/foo/"), "match /foo/*");
assertTrue(new ServletPathSpec("/foo/*").matches("/foo/anything"), "match /foo/*");
assertFalse(new ServletPathSpec("/foo/*").matches("/bar"), "!match /foo/*");
assertFalse(new ServletPathSpec("/foo/*").matches("/bar/"), "!match /foo/*");
assertFalse(new ServletPathSpec("/foo/*").matches("/bar/anything"), "!match /foo/*");
assertTrue(new ServletPathSpec("*.foo").matches("anything.foo"), "match *.foo");
assertFalse(new ServletPathSpec("*.foo").matches("anything.bar"), "!match *.foo");
assertTrue(new ServletPathSpec("/On*").matches("/On*"), "match /On*");
assertFalse(new ServletPathSpec("/On*").matches("/One"), "!match /One");
assertTrue(new ServletPathSpec("").matches("/"), "match \"\"");
} }
@Test @Test
@ -187,7 +217,9 @@ public class ServletPathSpecTest
assertNotMatches(spec, "/downloads/distribution.tgz"); assertNotMatches(spec, "/downloads/distribution.tgz");
assertNotMatches(spec, "/abs/path"); assertNotMatches(spec, "/abs/path");
assertEquals(null, spec.getPathInfo("/downloads/distribution.tar.gz"), "Spec.pathInfo"); MatchedPath matched = spec.matched("/downloads/distribution.tar.gz");
assertThat("Suffix.pathMatch", matched.getPathMatch(), is("/downloads/distribution.tar.gz"));
assertThat("Suffix.pathInfo", matched.getPathInfo(), is(nullValue()));
} }
@Test @Test
@ -201,5 +233,4 @@ public class ServletPathSpecTest
assertThat(new ServletPathSpec("/bar/foo"), not(equalTo(new ServletPathSpec("/foo/bar")))); assertThat(new ServletPathSpec("/bar/foo"), not(equalTo(new ServletPathSpec("/foo/bar"))));
assertThat(new ServletPathSpec("/foo"), not(equalTo(new RegexPathSpec("/foo")))); assertThat(new ServletPathSpec("/foo"), not(equalTo(new RegexPathSpec("/foo"))));
} }
} }

View File

@ -2,3 +2,4 @@ org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog
#org.eclipse.jetty.LEVEL=DEBUG #org.eclipse.jetty.LEVEL=DEBUG
#org.eclipse.jetty.server.LEVEL=DEBUG #org.eclipse.jetty.server.LEVEL=DEBUG
#org.eclipse.jetty.http.LEVEL=DEBUG #org.eclipse.jetty.http.LEVEL=DEBUG
#org.eclipse.jetty.http.pathmap.LEVEL=DEBUG

View File

@ -105,7 +105,7 @@ public class AbstractNCSARequestLog extends ContainerLifeCycle implements Reques
{ {
try try
{ {
if (_ignorePathMap != null && _ignorePathMap.getMatch(request.getRequestURI()) != null) if (_ignorePathMap != null && _ignorePathMap.getMatched(request.getRequestURI()) != null)
return; return;
if (!isEnabled()) if (!isEnabled())

View File

@ -327,7 +327,7 @@ public class CustomRequestLog extends ContainerLifeCycle implements RequestLog
{ {
try try
{ {
if (_ignorePathMap != null && _ignorePathMap.getMatch(request.getRequestURI()) != null) if (_ignorePathMap != null && _ignorePathMap.getMatched(request.getRequestURI()) != null)
return; return;
StringBuilder sb = _buffers.get(); StringBuilder sb = _buffers.get();

View File

@ -31,7 +31,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.pathmap.MappedResource; import org.eclipse.jetty.http.pathmap.MatchedResource;
import org.eclipse.jetty.server.Dispatcher; import org.eclipse.jetty.server.Dispatcher;
import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Request;
@ -71,7 +71,7 @@ public class Invoker extends HttpServlet
private ContextHandler _contextHandler; private ContextHandler _contextHandler;
private ServletHandler _servletHandler; private ServletHandler _servletHandler;
private MappedResource<ServletHolder> _invokerEntry; private MatchedResource<ServletHolder> _invokerEntry;
private Map<String, String> _parameters; private Map<String, String> _parameters;
private boolean _nonContextServlets; private boolean _nonContextServlets;
private boolean _verbose; private boolean _verbose;
@ -167,13 +167,13 @@ public class Invoker extends HttpServlet
synchronized (_servletHandler) synchronized (_servletHandler)
{ {
// find the entry for the invoker (me) // find the entry for the invoker (me)
_invokerEntry = _servletHandler.getMappedServlet(servletPath); _invokerEntry = _servletHandler.getMatchedServlet(servletPath);
// Check for existing mapping (avoid threaded race). // Check for existing mapping (avoid threaded race).
String path = URIUtil.addPaths(servletPath, servlet); String path = URIUtil.addPaths(servletPath, servlet);
MappedResource<ServletHolder> entry = _servletHandler.getMappedServlet(path); MatchedResource<ServletHolder> entry = _servletHandler.getMatchedServlet(path);
if (entry != null && !entry.equals(_invokerEntry)) if (entry != null && !entry.getResource().equals(_invokerEntry.getResource()))
{ {
// Use the holder // Use the holder
holder = entry.getResource(); holder = entry.getResource();

View File

@ -50,6 +50,8 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.pathmap.MappedResource; import org.eclipse.jetty.http.pathmap.MappedResource;
import org.eclipse.jetty.http.pathmap.MatchedPath;
import org.eclipse.jetty.http.pathmap.MatchedResource;
import org.eclipse.jetty.http.pathmap.PathMappings; import org.eclipse.jetty.http.pathmap.PathMappings;
import org.eclipse.jetty.http.pathmap.PathSpec; import org.eclipse.jetty.http.pathmap.PathSpec;
import org.eclipse.jetty.http.pathmap.ServletPathSpec; import org.eclipse.jetty.http.pathmap.ServletPathSpec;
@ -369,13 +371,16 @@ public class ServletHandler extends ScopedHandler
* *
* @param target Path within _context or servlet name * @param target Path within _context or servlet name
* @return PathMap Entries pathspec to ServletHolder * @return PathMap Entries pathspec to ServletHolder
* @deprecated Use {@link #getMappedServlet(String)} * @deprecated Use {@link #getMatchedServlet(String)} instead
*/ */
@Deprecated @Deprecated
public MappedResource<ServletHolder> getHolderEntry(String target) public MappedResource<ServletHolder> getHolderEntry(String target)
{ {
if (target.startsWith("/")) if (target.startsWith("/"))
return getMappedServlet(target); {
MatchedResource<ServletHolder> matchedResource = getMatchedServlet(target);
return new MappedResource<>(matchedResource.getPathSpec(), matchedResource.getResource());
}
return null; return null;
} }
@ -465,16 +470,15 @@ public class ServletHandler extends ScopedHandler
ServletHolder servletHolder = null; ServletHolder servletHolder = null;
UserIdentity.Scope oldScope = null; UserIdentity.Scope oldScope = null;
MappedResource<ServletHolder> mapping = getMappedServlet(target); MatchedResource<ServletHolder> matched = getMatchedServlet(target);
if (mapping != null) if (matched != null)
{ {
servletHolder = mapping.getResource(); servletHolder = matched.getResource();
if (mapping.getPathSpec() != null) if (matched.getPathSpec() != null)
{ {
PathSpec pathSpec = mapping.getPathSpec(); String servletPath = matched.getPathMatch();
String servletPath = pathSpec.getPathMatch(target); String pathInfo = matched.getPathInfo();
String pathInfo = pathSpec.getPathInfo(target);
if (DispatcherType.INCLUDE.equals(type)) if (DispatcherType.INCLUDE.equals(type))
{ {
@ -558,24 +562,39 @@ public class ServletHandler extends ScopedHandler
} }
/** /**
* ServletHolder matching path. * ServletHolder matching target path.
* *
* @param target Path within _context or servlet name * @param target Path within _context or servlet name
* @return MappedResource to the ServletHolder. Named servlets have a null PathSpec * @return MatchedResource, pointing to the {@link MappedResource} for the {@link ServletHolder}, and also the pathspec specific name/info sections for the match.
* Named servlets have a null PathSpec and {@link MatchedResource}.
*/ */
public MappedResource<ServletHolder> getMappedServlet(String target) public MatchedResource<ServletHolder> getMatchedServlet(String target)
{ {
if (target.startsWith("/")) if (target.startsWith("/"))
{ {
if (_servletPathMap == null) if (_servletPathMap == null)
return null; return null;
return _servletPathMap.getMatch(target); return _servletPathMap.getMatched(target);
} }
ServletHolder holder = _servletNameMap.get(target); ServletHolder holder = _servletNameMap.get(target);
if (holder == null) if (holder == null)
return null; return null;
return new MappedResource<>(null, holder); return new MatchedResource<>(holder, null, MatchedPath.EMPTY);
}
/**
* ServletHolder matching path.
*
* @param target Path within _context or servlet name
* @return MappedResource to the ServletHolder. Named servlets have a null PathSpec
* @deprecated use {@link #getMatchedServlet(String)} instead
*/
@Deprecated
public MappedResource<ServletHolder> getMappedServlet(String target)
{
MatchedResource<ServletHolder> matchedResource = getMatchedServlet(target);
return new MappedResource<>(matchedResource.getPathSpec(), matchedResource.getResource());
} }
protected FilterChain getFilterChain(Request baseRequest, String pathInContext, ServletHolder servletHolder) protected FilterChain getFilterChain(Request baseRequest, String pathInContext, ServletHolder servletHolder)

View File

@ -22,6 +22,7 @@ import java.util.Iterator;
import javax.servlet.ServletContext; import javax.servlet.ServletContext;
import org.eclipse.jetty.http.pathmap.MappedResource; import org.eclipse.jetty.http.pathmap.MappedResource;
import org.eclipse.jetty.http.pathmap.MatchedResource;
import org.eclipse.jetty.http.pathmap.PathMappings; import org.eclipse.jetty.http.pathmap.PathMappings;
import org.eclipse.jetty.http.pathmap.PathSpec; import org.eclipse.jetty.http.pathmap.PathSpec;
import org.eclipse.jetty.http.pathmap.RegexPathSpec; import org.eclipse.jetty.http.pathmap.RegexPathSpec;
@ -75,14 +76,30 @@ public class NativeWebSocketConfiguration extends ContainerLifeCycle implements
} }
/** /**
* Get the matching {@link MappedResource} for the provided target. * Get the matching {@link MatchedResource} for the provided target.
* *
* @param target the target path * @param target the target path
* @return the matching resource, or null if no match. * @return the matching resource, or null if no match.
*/ */
public MatchedResource<WebSocketCreator> getMatched(String target)
{
return this.mappings.getMatched(target);
}
/**
* Get the matching {@link MappedResource} for the provided target.
*
* @param target the target path
* @return the matching resource, or null if no match.
* @deprecated use {@link #getMatched(String)} instead.
*/
@Deprecated
public MappedResource<WebSocketCreator> getMatch(String target) public MappedResource<WebSocketCreator> getMatch(String target)
{ {
return this.mappings.getMatch(target); MatchedResource<WebSocketCreator> matched = this.mappings.getMatched(target);
if (matched == null)
return null;
return new MappedResource<>(matched.getPathSpec(), matched.getResource());
} }
/** /**

View File

@ -31,7 +31,7 @@ import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.pathmap.MappedResource; import org.eclipse.jetty.http.pathmap.MatchedResource;
import org.eclipse.jetty.http.pathmap.PathSpec; import org.eclipse.jetty.http.pathmap.PathSpec;
import org.eclipse.jetty.servlet.FilterHolder; import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletContextHandler;
@ -302,7 +302,7 @@ public class WebSocketUpgradeFilter implements Filter, MappedWebSocketCreator, D
target = target + httpreq.getPathInfo(); target = target + httpreq.getPathInfo();
} }
MappedResource<WebSocketCreator> resource = configuration.getMatch(target); MatchedResource<WebSocketCreator> resource = configuration.getMatched(target);
if (resource == null) if (resource == null)
{ {
// no match. // no match.

View File

@ -23,7 +23,7 @@ import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.pathmap.MappedResource; import org.eclipse.jetty.http.pathmap.MatchedResource;
import org.eclipse.jetty.http.pathmap.PathSpec; import org.eclipse.jetty.http.pathmap.PathSpec;
import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.MappedByteBufferPool; import org.eclipse.jetty.io.MappedByteBufferPool;
@ -89,7 +89,7 @@ public class WebSocketUpgradeHandlerWrapper extends HandlerWrapper implements Ma
{ {
if (configuration.getFactory().isUpgradeRequest(request, response)) if (configuration.getFactory().isUpgradeRequest(request, response))
{ {
MappedResource<WebSocketCreator> resource = configuration.getMatch(target); MatchedResource<WebSocketCreator> resource = configuration.getMatched(target);
if (resource == null) if (resource == null)
{ {
// no match. // no match.