stringMatcherCache = new ConcurrentHashMap<>(256);
+
+
+ /**
+ * Create a new instance with the {@link #DEFAULT_PATH_SEPARATOR}.
+ */
+ public AntPathMatcher() {
+ this.pathSeparator = DEFAULT_PATH_SEPARATOR;
+ this.pathSeparatorPatternCache = new PathSeparatorPatternCache(DEFAULT_PATH_SEPARATOR);
+ }
+
+ /**
+ * A convenient, alternative constructor to use with a custom path separator.
+ * @param pathSeparator the path separator to use, must not be {@code null}.
+ * @since 4.1
+ */
+ public AntPathMatcher(String pathSeparator) {
+ Assert.notNull(pathSeparator, "'pathSeparator' is required");
+ this.pathSeparator = pathSeparator;
+ this.pathSeparatorPatternCache = new PathSeparatorPatternCache(pathSeparator);
+ }
+
+
+ /**
+ * Set the path separator to use for pattern parsing.
+ * Default is "/", as in Ant.
+ */
+ public void setPathSeparator(String pathSeparator) {
+ this.pathSeparator = (pathSeparator != null ? pathSeparator : DEFAULT_PATH_SEPARATOR);
+ this.pathSeparatorPatternCache = new PathSeparatorPatternCache(this.pathSeparator);
+ }
+
+ /**
+ * Specify whether to perform pattern matching in a case-sensitive fashion.
+ *
Default is {@code true}. Switch this to {@code false} for case-insensitive matching.
+ * @since 4.2
+ */
+ public void setCaseSensitive(boolean caseSensitive) {
+ this.caseSensitive = caseSensitive;
+ }
+
+ /**
+ * Specify whether to trim tokenized paths and patterns.
+ *
Default is {@code false}.
+ */
+ public void setTrimTokens(boolean trimTokens) {
+ this.trimTokens = trimTokens;
+ }
+
+ /**
+ * Specify whether to cache parsed pattern metadata for patterns passed
+ * into this matcher's {@link #match} method. A value of {@code true}
+ * activates an unlimited pattern cache; a value of {@code false} turns
+ * the pattern cache off completely.
+ *
Default is for the cache to be on, but with the variant to automatically
+ * turn it off when encountering too many patterns to cache at runtime
+ * (the threshold is 65536), assuming that arbitrary permutations of patterns
+ * are coming in, with little chance for encountering a recurring pattern.
+ * @since 4.0.1
+ * @see #getStringMatcher(String)
+ */
+ public void setCachePatterns(boolean cachePatterns) {
+ this.cachePatterns = cachePatterns;
+ }
+
+ private void deactivatePatternCache() {
+ this.cachePatterns = false;
+ this.tokenizedPatternCache.clear();
+ this.stringMatcherCache.clear();
+ }
+
+
+ public boolean isPattern(String path) {
+ return (path.indexOf('*') != -1 || path.indexOf('?') != -1);
+ }
+
+ public boolean match(String pattern, String path) {
+ return doMatch(pattern, path, true, null);
+ }
+
+ public boolean matchStart(String pattern, String path) {
+ return doMatch(pattern, path, false, null);
+ }
+
+ /**
+ * Actually match the given {@code path} against the given {@code pattern}.
+ * @param pattern the pattern to match against
+ * @param path the path String to test
+ * @param fullMatch whether a full pattern match is required (else a pattern match
+ * as far as the given base path goes is sufficient)
+ * @return {@code true} if the supplied {@code path} matched, {@code false} if it didn't
+ */
+ protected boolean doMatch(String pattern, String path, boolean fullMatch,
+ Map uriTemplateVariables) {
+
+ if (path.startsWith(this.pathSeparator) != pattern.startsWith(this.pathSeparator)) {
+ return false;
+ }
+
+ String[] pattDirs = tokenizePattern(pattern);
+ if (fullMatch && this.caseSensitive && !isPotentialMatch(path, pattDirs)) {
+ return false;
+ }
+
+ String[] pathDirs = tokenizePath(path);
+
+ int pattIdxStart = 0;
+ int pattIdxEnd = pattDirs.length - 1;
+ int pathIdxStart = 0;
+ int pathIdxEnd = pathDirs.length - 1;
+
+ // Match all elements up to the first **
+ while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
+ String pattDir = pattDirs[pattIdxStart];
+ if ("**".equals(pattDir)) {
+ break;
+ }
+ if (!matchStrings(pattDir, pathDirs[pathIdxStart], uriTemplateVariables)) {
+ return false;
+ }
+ pattIdxStart++;
+ pathIdxStart++;
+ }
+
+ if (pathIdxStart > pathIdxEnd) {
+ // Path is exhausted, only match if rest of pattern is * or **'s
+ if (pattIdxStart > pattIdxEnd) {
+ return (pattern.endsWith(this.pathSeparator) == path.endsWith(this.pathSeparator));
+ }
+ if (!fullMatch) {
+ return true;
+ }
+ if (pattIdxStart == pattIdxEnd && pattDirs[pattIdxStart].equals("*") && path.endsWith(this.pathSeparator)) {
+ return true;
+ }
+ for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
+ if (!pattDirs[i].equals("**")) {
+ return false;
+ }
+ }
+ return true;
+ }
+ else if (pattIdxStart > pattIdxEnd) {
+ // String not exhausted, but pattern is. Failure.
+ return false;
+ }
+ else if (!fullMatch && "**".equals(pattDirs[pattIdxStart])) {
+ // Path start definitely matches due to "**" part in pattern.
+ return true;
+ }
+
+ // up to last '**'
+ while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
+ String pattDir = pattDirs[pattIdxEnd];
+ if (pattDir.equals("**")) {
+ break;
+ }
+ if (!matchStrings(pattDir, pathDirs[pathIdxEnd], uriTemplateVariables)) {
+ return false;
+ }
+ pattIdxEnd--;
+ pathIdxEnd--;
+ }
+ if (pathIdxStart > pathIdxEnd) {
+ // String is exhausted
+ for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
+ if (!pattDirs[i].equals("**")) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ while (pattIdxStart != pattIdxEnd && pathIdxStart <= pathIdxEnd) {
+ int patIdxTmp = -1;
+ for (int i = pattIdxStart + 1; i <= pattIdxEnd; i++) {
+ if (pattDirs[i].equals("**")) {
+ patIdxTmp = i;
+ break;
+ }
+ }
+ if (patIdxTmp == pattIdxStart + 1) {
+ // '**/**' situation, so skip one
+ pattIdxStart++;
+ continue;
+ }
+ // Find the pattern between padIdxStart & padIdxTmp in str between
+ // strIdxStart & strIdxEnd
+ int patLength = (patIdxTmp - pattIdxStart - 1);
+ int strLength = (pathIdxEnd - pathIdxStart + 1);
+ int foundIdx = -1;
+
+ strLoop:
+ for (int i = 0; i <= strLength - patLength; i++) {
+ for (int j = 0; j < patLength; j++) {
+ String subPat = pattDirs[pattIdxStart + j + 1];
+ String subStr = pathDirs[pathIdxStart + i + j];
+ if (!matchStrings(subPat, subStr, uriTemplateVariables)) {
+ continue strLoop;
+ }
+ }
+ foundIdx = pathIdxStart + i;
+ break;
+ }
+
+ if (foundIdx == -1) {
+ return false;
+ }
+
+ pattIdxStart = patIdxTmp;
+ pathIdxStart = foundIdx + patLength;
+ }
+
+ for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
+ if (!pattDirs[i].equals("**")) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private boolean isPotentialMatch(String path, String[] pattDirs) {
+ if (!this.trimTokens) {
+ int pos = 0;
+ for (String pattDir : pattDirs) {
+ int skipped = skipSeparator(path, pos, this.pathSeparator);
+ pos += skipped;
+ skipped = skipSegment(path, pos, pattDir);
+ if (skipped < pattDir.length()) {
+ return (skipped > 0 || (pattDir.length() > 0 && isWildcardChar(pattDir.charAt(0))));
+ }
+ pos += skipped;
+ }
+ }
+ return true;
+ }
+
+ private int skipSegment(String path, int pos, String prefix) {
+ int skipped = 0;
+ for (int i = 0; i < prefix.length(); i++) {
+ char c = prefix.charAt(i);
+ if (isWildcardChar(c)) {
+ return skipped;
+ }
+ int currPos = pos + skipped;
+ if (currPos >= path.length()) {
+ return 0;
+ }
+ if (c == path.charAt(currPos)) {
+ skipped++;
+ }
+ }
+ return skipped;
+ }
+
+ private int skipSeparator(String path, int pos, String separator) {
+ int skipped = 0;
+ while (path.startsWith(separator, pos + skipped)) {
+ skipped += separator.length();
+ }
+ return skipped;
+ }
+
+ private boolean isWildcardChar(char c) {
+ for (char candidate : WILDCARD_CHARS) {
+ if (c == candidate) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Tokenize the given path pattern into parts, based on this matcher's settings.
+ * Performs caching based on {@link #setCachePatterns}, delegating to
+ * {@link #tokenizePath(String)} for the actual tokenization algorithm.
+ * @param pattern the pattern to tokenize
+ * @return the tokenized pattern parts
+ */
+ protected String[] tokenizePattern(String pattern) {
+ String[] tokenized = null;
+ Boolean cachePatterns = this.cachePatterns;
+ if (cachePatterns == null || cachePatterns.booleanValue()) {
+ tokenized = this.tokenizedPatternCache.get(pattern);
+ }
+ if (tokenized == null) {
+ tokenized = tokenizePath(pattern);
+ if (cachePatterns == null && this.tokenizedPatternCache.size() >= CACHE_TURNOFF_THRESHOLD) {
+ // Try to adapt to the runtime situation that we're encountering:
+ // There are obviously too many different patterns coming in here...
+ // So let's turn off the cache since the patterns are unlikely to be reoccurring.
+ deactivatePatternCache();
+ return tokenized;
+ }
+ if (cachePatterns == null || cachePatterns.booleanValue()) {
+ this.tokenizedPatternCache.put(pattern, tokenized);
+ }
+ }
+ return tokenized;
+ }
+
+ /**
+ * Tokenize the given path String into parts, based on this matcher's settings.
+ * @param path the path to tokenize
+ * @return the tokenized path parts
+ */
+ protected String[] tokenizePath(String path) {
+ return StringUtils.tokenizeToStringArray(path, this.pathSeparator, this.trimTokens, true);
+ }
+
+ /**
+ * Test whether or not a string matches against a pattern.
+ * @param pattern the pattern to match against (never {@code null})
+ * @param str the String which must be matched against the pattern (never {@code null})
+ * @return {@code true} if the string matches against the pattern, or {@code false} otherwise
+ */
+ private boolean matchStrings(String pattern, String str,
+ Map uriTemplateVariables) {
+
+ return getStringMatcher(pattern).matchStrings(str, uriTemplateVariables);
+ }
+
+ /**
+ * Build or retrieve an {@link AntPathStringMatcher} for the given pattern.
+ * The default implementation checks this AntPathMatcher's internal cache
+ * (see {@link #setCachePatterns}), creating a new AntPathStringMatcher instance
+ * if no cached copy is found.
+ *
When encountering too many patterns to cache at runtime (the threshold is 65536),
+ * it turns the default cache off, assuming that arbitrary permutations of patterns
+ * are coming in, with little chance for encountering a recurring pattern.
+ *
This method may be overridden to implement a custom cache strategy.
+ * @param pattern the pattern to match against (never {@code null})
+ * @return a corresponding AntPathStringMatcher (never {@code null})
+ * @see #setCachePatterns
+ */
+ protected AntPathStringMatcher getStringMatcher(String pattern) {
+ AntPathStringMatcher matcher = null;
+ Boolean cachePatterns = this.cachePatterns;
+ if (cachePatterns == null || cachePatterns.booleanValue()) {
+ matcher = this.stringMatcherCache.get(pattern);
+ }
+ if (matcher == null) {
+ matcher = new AntPathStringMatcher(pattern, this.caseSensitive);
+ if (cachePatterns == null && this.stringMatcherCache.size() >= CACHE_TURNOFF_THRESHOLD) {
+ // Try to adapt to the runtime situation that we're encountering:
+ // There are obviously too many different patterns coming in here...
+ // So let's turn off the cache since the patterns are unlikely to be reoccurring.
+ deactivatePatternCache();
+ return matcher;
+ }
+ if (cachePatterns == null || cachePatterns.booleanValue()) {
+ this.stringMatcherCache.put(pattern, matcher);
+ }
+ }
+ return matcher;
+ }
+
+ /**
+ * Given a pattern and a full path, determine the pattern-mapped part.
For example:
+ * - '{@code /docs/cvs/commit.html}' and '{@code /docs/cvs/commit.html} -> ''
+ * - '{@code /docs/*}' and '{@code /docs/cvs/commit} -> '{@code cvs/commit}'
+ * - '{@code /docs/cvs/*.html}' and '{@code /docs/cvs/commit.html} -> '{@code commit.html}'
+ * - '{@code /docs/**}' and '{@code /docs/cvs/commit} -> '{@code cvs/commit}'
+ * - '{@code /docs/**\/*.html}' and '{@code /docs/cvs/commit.html} -> '{@code cvs/commit.html}'
+ * - '{@code /*.html}' and '{@code /docs/cvs/commit.html} -> '{@code docs/cvs/commit.html}'
+ * - '{@code *.html}' and '{@code /docs/cvs/commit.html} -> '{@code /docs/cvs/commit.html}'
+ * - '{@code *}' and '{@code /docs/cvs/commit.html} -> '{@code /docs/cvs/commit.html}'
+ * Assumes that {@link #match} returns {@code true} for '{@code pattern}' and '{@code path}', but
+ * does not enforce this.
+ */
+ public String extractPathWithinPattern(String pattern, String path) {
+ String[] patternParts = StringUtils.tokenizeToStringArray(pattern, this.pathSeparator, this.trimTokens, true);
+ String[] pathParts = StringUtils.tokenizeToStringArray(path, this.pathSeparator, this.trimTokens, true);
+ StringBuilder builder = new StringBuilder();
+ boolean pathStarted = false;
+
+ for (int segment = 0; segment < patternParts.length; segment++) {
+ String patternPart = patternParts[segment];
+ if (patternPart.indexOf('*') > -1 || patternPart.indexOf('?') > -1) {
+ for (; segment < pathParts.length; segment++) {
+ if (pathStarted || (segment == 0 && !pattern.startsWith(this.pathSeparator))) {
+ builder.append(this.pathSeparator);
+ }
+ builder.append(pathParts[segment]);
+ pathStarted = true;
+ }
+ }
+ }
+
+ return builder.toString();
+ }
+
+ public Map extractUriTemplateVariables(String pattern, String path) {
+ Map variables = new LinkedHashMap<>();
+ boolean result = doMatch(pattern, path, true, variables);
+ if (!result) {
+ throw new IllegalStateException("Pattern \"" + pattern + "\" is not a match for \"" + path + "\"");
+ }
+ return variables;
+ }
+
+ /**
+ * Combine two patterns into a new pattern.
+ * This implementation simply concatenates the two patterns, unless
+ * the first pattern contains a file extension match (e.g., {@code *.html}).
+ * In that case, the second pattern will be merged into the first. Otherwise,
+ * an {@code IllegalArgumentException} will be thrown.
+ *
Examples
+ *
+ * Pattern 1 | Pattern 2 | Result |
+ * {@code null} | {@code null} | |
+ * /hotels | {@code null} | /hotels |
+ * {@code null} | /hotels | /hotels |
+ * /hotels | /bookings | /hotels/bookings |
+ * /hotels | bookings | /hotels/bookings |
+ * /hotels/* | /bookings | /hotels/bookings |
+ * /hotels/** | /bookings | /hotels/**/bookings |
+ * /hotels | {hotel} | /hotels/{hotel} |
+ * /hotels/* | {hotel} | /hotels/{hotel} |
+ * /hotels/** | {hotel} | /hotels/**/{hotel} |
+ * /*.html | /hotels.html | /hotels.html |
+ * /*.html | /hotels | /hotels.html |
+ * /*.html | /*.txt | {@code IllegalArgumentException} |
+ *
+ * @param pattern1 the first pattern
+ * @param pattern2 the second pattern
+ * @return the combination of the two patterns
+ * @throws IllegalArgumentException if the two patterns cannot be combined
+ */
+ public String combine(String pattern1, String pattern2) {
+ if (!StringUtils.hasText(pattern1) && !StringUtils.hasText(pattern2)) {
+ return "";
+ }
+ if (!StringUtils.hasText(pattern1)) {
+ return pattern2;
+ }
+ if (!StringUtils.hasText(pattern2)) {
+ return pattern1;
+ }
+
+ boolean pattern1ContainsUriVar = (pattern1.indexOf('{') != -1);
+ if (!pattern1.equals(pattern2) && !pattern1ContainsUriVar && match(pattern1, pattern2)) {
+ // /* + /hotel -> /hotel ; "/*.*" + "/*.html" -> /*.html
+ // However /user + /user -> /usr/user ; /{foo} + /bar -> /{foo}/bar
+ return pattern2;
+ }
+
+ // /hotels/* + /booking -> /hotels/booking
+ // /hotels/* + booking -> /hotels/booking
+ if (pattern1.endsWith(this.pathSeparatorPatternCache.getEndsOnWildCard())) {
+ return concat(pattern1.substring(0, pattern1.length() - 2), pattern2);
+ }
+
+ // /hotels/** + /booking -> /hotels/**/booking
+ // /hotels/** + booking -> /hotels/**/booking
+ if (pattern1.endsWith(this.pathSeparatorPatternCache.getEndsOnDoubleWildCard())) {
+ return concat(pattern1, pattern2);
+ }
+
+ int starDotPos1 = pattern1.indexOf("*.");
+ if (pattern1ContainsUriVar || starDotPos1 == -1 || this.pathSeparator.equals(".")) {
+ // simply concatenate the two patterns
+ return concat(pattern1, pattern2);
+ }
+
+ String ext1 = pattern1.substring(starDotPos1 + 1);
+ int dotPos2 = pattern2.indexOf('.');
+ String file2 = (dotPos2 == -1 ? pattern2 : pattern2.substring(0, dotPos2));
+ String ext2 = (dotPos2 == -1 ? "" : pattern2.substring(dotPos2));
+ boolean ext1All = (ext1.equals(".*") || ext1.equals(""));
+ boolean ext2All = (ext2.equals(".*") || ext2.equals(""));
+ if (!ext1All && !ext2All) {
+ throw new IllegalArgumentException("Cannot combine patterns: " + pattern1 + " vs " + pattern2);
+ }
+ String ext = (ext1All ? ext2 : ext1);
+ return file2 + ext;
+ }
+
+ private String concat(String path1, String path2) {
+ boolean path1EndsWithSeparator = path1.endsWith(this.pathSeparator);
+ boolean path2StartsWithSeparator = path2.startsWith(this.pathSeparator);
+
+ if (path1EndsWithSeparator && path2StartsWithSeparator) {
+ return path1 + path2.substring(1);
+ }
+ else if (path1EndsWithSeparator || path2StartsWithSeparator) {
+ return path1 + path2;
+ }
+ else {
+ return path1 + this.pathSeparator + path2;
+ }
+ }
+
+ /**
+ * Given a full path, returns a {@link Comparator} suitable for sorting patterns in order of
+ * explicitness.
+ * This{@code Comparator} will {@linkplain java.util.List#sort(Comparator) sort}
+ * a list so that more specific patterns (without uri templates or wild cards) come before
+ * generic patterns. So given a list with the following patterns:
+ *
+ * - {@code /hotels/new}
+ * - {@code /hotels/{hotel}}
- {@code /hotels/*}
+ *
+ * the returned comparator will sort this list so that the order will be as indicated.
+ * The full path given as parameter is used to test for exact matches. So when the given path
+ * is {@code /hotels/2}, the pattern {@code /hotels/2} will be sorted before {@code /hotels/1}.
+ * @param path the full path to use for comparison
+ * @return a comparator capable of sorting patterns in order of explicitness
+ */
+ public Comparator getPatternComparator(String path) {
+ return new AntPatternComparator(path);
+ }
+
+
+ /**
+ * Tests whether or not a string matches against a pattern via a {@link Pattern}.
+ * The pattern may contain special characters: '*' means zero or more characters; '?' means one and
+ * only one character; '{' and '}' indicate a URI template pattern. For example /users/{user}.
+ */
+ protected static class AntPathStringMatcher {
+
+ private static final Pattern GLOB_PATTERN = Pattern.compile("\\?|\\*|\\{((?:\\{[^/]+?\\}|[^/{}]|\\\\[{}])+?)\\}");
+
+ private static final String DEFAULT_VARIABLE_PATTERN = "(.*)";
+
+ private final Pattern pattern;
+
+ private final List variableNames = new LinkedList<>();
+
+ public AntPathStringMatcher(String pattern) {
+ this(pattern, true);
+ }
+
+ public AntPathStringMatcher(String pattern, boolean caseSensitive) {
+ StringBuilder patternBuilder = new StringBuilder();
+ Matcher matcher = GLOB_PATTERN.matcher(pattern);
+ int end = 0;
+ while (matcher.find()) {
+ patternBuilder.append(quote(pattern, end, matcher.start()));
+ String match = matcher.group();
+ if ("?".equals(match)) {
+ patternBuilder.append('.');
+ }
+ else if ("*".equals(match)) {
+ patternBuilder.append(".*");
+ }
+ else if (match.startsWith("{") && match.endsWith("}")) {
+ int colonIdx = match.indexOf(':');
+ if (colonIdx == -1) {
+ patternBuilder.append(DEFAULT_VARIABLE_PATTERN);
+ this.variableNames.add(matcher.group(1));
+ }
+ else {
+ String variablePattern = match.substring(colonIdx + 1, match.length() - 1);
+ patternBuilder.append('(');
+ patternBuilder.append(variablePattern);
+ patternBuilder.append(')');
+ String variableName = match.substring(1, colonIdx);
+ this.variableNames.add(variableName);
+ }
+ }
+ end = matcher.end();
+ }
+ patternBuilder.append(quote(pattern, end, pattern.length()));
+ this.pattern = (caseSensitive ? Pattern.compile(patternBuilder.toString()) :
+ Pattern.compile(patternBuilder.toString(), Pattern.CASE_INSENSITIVE));
+ }
+
+ private String quote(String s, int start, int end) {
+ if (start == end) {
+ return "";
+ }
+ return Pattern.quote(s.substring(start, end));
+ }
+
+ /**
+ * Main entry point.
+ * @return {@code true} if the string matches against the pattern, or {@code false} otherwise.
+ */
+ public boolean matchStrings(String str, Map uriTemplateVariables) {
+ Matcher matcher = this.pattern.matcher(str);
+ if (matcher.matches()) {
+ if (uriTemplateVariables != null) {
+ // SPR-8455
+ if (this.variableNames.size() != matcher.groupCount()) {
+ throw new IllegalArgumentException("The number of capturing groups in the pattern segment " +
+ this.pattern + " does not match the number of URI template variables it defines, " +
+ "which can occur if capturing groups are used in a URI template regex. " +
+ "Use non-capturing groups instead.");
+ }
+ for (int i = 1; i <= matcher.groupCount(); i++) {
+ String name = this.variableNames.get(i - 1);
+ String value = matcher.group(i);
+ uriTemplateVariables.put(name, value);
+ }
+ }
+ return true;
+ }
+ else {
+ return false;
+ }
+ }
+ }
+
+
+ /**
+ * The default {@link Comparator} implementation returned by
+ * {@link #getPatternComparator(String)}.
+ * In order, the most "generic" pattern is determined by the following:
+ *
+ * - if it's null or a capture all pattern (i.e. it is equal to "/**")
+ * - if the other pattern is an actual match
+ * - if it's a catch-all pattern (i.e. it ends with "**"
+ * - if it's got more "*" than the other pattern
+ * - if it's got more "{foo}" than the other pattern
+ * - if it's shorter than the other pattern
+ *
+ */
+ protected static class AntPatternComparator implements Comparator {
+
+ private final String path;
+
+ public AntPatternComparator(String path) {
+ this.path = path;
+ }
+
+ /**
+ * Compare two patterns to determine which should match first, i.e. which
+ * is the most specific regarding the current path.
+ * @return a negative integer, zero, or a positive integer as pattern1 is
+ * more specific, equally specific, or less specific than pattern2.
+ */
+ @Override
+ public int compare(String pattern1, String pattern2) {
+ PatternInfo info1 = new PatternInfo(pattern1);
+ PatternInfo info2 = new PatternInfo(pattern2);
+
+ if (info1.isLeastSpecific() && info2.isLeastSpecific()) {
+ return 0;
+ }
+ else if (info1.isLeastSpecific()) {
+ return 1;
+ }
+ else if (info2.isLeastSpecific()) {
+ return -1;
+ }
+
+ boolean pattern1EqualsPath = pattern1.equals(path);
+ boolean pattern2EqualsPath = pattern2.equals(path);
+ if (pattern1EqualsPath && pattern2EqualsPath) {
+ return 0;
+ }
+ else if (pattern1EqualsPath) {
+ return -1;
+ }
+ else if (pattern2EqualsPath) {
+ return 1;
+ }
+
+ if (info1.isPrefixPattern() && info2.getDoubleWildcards() == 0) {
+ return 1;
+ }
+ else if (info2.isPrefixPattern() && info1.getDoubleWildcards() == 0) {
+ return -1;
+ }
+
+ if (info1.getTotalCount() != info2.getTotalCount()) {
+ return info1.getTotalCount() - info2.getTotalCount();
+ }
+
+ if (info1.getLength() != info2.getLength()) {
+ return info2.getLength() - info1.getLength();
+ }
+
+ if (info1.getSingleWildcards() < info2.getSingleWildcards()) {
+ return -1;
+ }
+ else if (info2.getSingleWildcards() < info1.getSingleWildcards()) {
+ return 1;
+ }
+
+ if (info1.getUriVars() < info2.getUriVars()) {
+ return -1;
+ }
+ else if (info2.getUriVars() < info1.getUriVars()) {
+ return 1;
+ }
+
+ return 0;
+ }
+
+
+ /**
+ * Value class that holds information about the pattern, e.g. number of
+ * occurrences of "*", "**", and "{" pattern elements.
+ */
+ private static class PatternInfo {
+
+ private final String pattern;
+
+ private int uriVars;
+
+ private int singleWildcards;
+
+ private int doubleWildcards;
+
+ private boolean catchAllPattern;
+
+ private boolean prefixPattern;
+
+ private Integer length;
+
+ public PatternInfo(String pattern) {
+ this.pattern = pattern;
+ if (this.pattern != null) {
+ initCounters();
+ this.catchAllPattern = this.pattern.equals("/**");
+ this.prefixPattern = !this.catchAllPattern && this.pattern.endsWith("/**");
+ }
+ if (this.uriVars == 0) {
+ this.length = (this.pattern != null ? this.pattern.length() : 0);
+ }
+ }
+
+ protected void initCounters() {
+ int pos = 0;
+ if (this.pattern != null) {
+ while (pos < this.pattern.length()) {
+ if (this.pattern.charAt(pos) == '{') {
+ this.uriVars++;
+ pos++;
+ }
+ else if (this.pattern.charAt(pos) == '*') {
+ if (pos + 1 < this.pattern.length() && this.pattern.charAt(pos + 1) == '*') {
+ this.doubleWildcards++;
+ pos += 2;
+ }
+ else if (pos > 0 && !this.pattern.substring(pos - 1).equals(".*")) {
+ this.singleWildcards++;
+ pos++;
+ }
+ else {
+ pos++;
+ }
+ }
+ else {
+ pos++;
+ }
+ }
+ }
+ }
+
+ public int getUriVars() {
+ return this.uriVars;
+ }
+
+ public int getSingleWildcards() {
+ return this.singleWildcards;
+ }
+
+ public int getDoubleWildcards() {
+ return this.doubleWildcards;
+ }
+
+ public boolean isLeastSpecific() {
+ return (this.pattern == null || this.catchAllPattern);
+ }
+
+ public boolean isPrefixPattern() {
+ return this.prefixPattern;
+ }
+
+ public int getTotalCount() {
+ return this.uriVars + this.singleWildcards + (2 * this.doubleWildcards);
+ }
+
+ /**
+ * Returns the length of the given pattern, where template variables are considered to be 1 long.
+ */
+ public int getLength() {
+ if (this.length == null) {
+ this.length = (this.pattern != null ?
+ VARIABLE_PATTERN.matcher(this.pattern).replaceAll("#").length() : 0);
+ }
+ return this.length;
+ }
+ }
+ }
+
+
+ /**
+ * A simple cache for patterns that depend on the configured path separator.
+ */
+ private static class PathSeparatorPatternCache {
+
+ private final String endsOnWildCard;
+
+ private final String endsOnDoubleWildCard;
+
+ public PathSeparatorPatternCache(String pathSeparator) {
+ this.endsOnWildCard = pathSeparator + "*";
+ this.endsOnDoubleWildCard = pathSeparator + "**";
+ }
+
+ public String getEndsOnWildCard() {
+ return this.endsOnWildCard;
+ }
+
+ public String getEndsOnDoubleWildCard() {
+ return this.endsOnDoubleWildCard;
+ }
+ }
+
+}
+
diff --git a/src/main/java/com/rocket/pig/utils/Assert.java b/src/main/java/com/rocket/pig/utils/Assert.java
new file mode 100644
index 0000000000000000000000000000000000000000..577f114827e5cca0d5a0ccb129fe40f76a784648
--- /dev/null
+++ b/src/main/java/com/rocket/pig/utils/Assert.java
@@ -0,0 +1,28 @@
+package com.rocket.pig.utils;
+
+public abstract class Assert {
+
+ public static void notNull(Object object, String message) {
+ if (object == null) {
+ throw new IllegalArgumentException(message);
+ }
+ }
+
+ public static void isTrue(boolean expression, String message) {
+ if (!expression) {
+ throw new IllegalArgumentException(message);
+ }
+ }
+
+ public static void isNull(Object object, String message) {
+ if (object != null) {
+ throw new IllegalArgumentException(message);
+ }
+ }
+
+ public static void hasText(String text, String message) {
+ if (!StringUtils.hasText(text)) {
+ throw new IllegalArgumentException(message);
+ }
+ }
+}
diff --git a/src/main/java/com/rocket/pig/utils/StringUtils.java b/src/main/java/com/rocket/pig/utils/StringUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..5f9610182471c1670175c3223658e6f57d716257
--- /dev/null
+++ b/src/main/java/com/rocket/pig/utils/StringUtils.java
@@ -0,0 +1,48 @@
+package com.rocket.pig.utils;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.StringTokenizer;
+
+public abstract class StringUtils {
+
+ public static boolean hasText(String str) {
+ return (str != null && !str.isEmpty() && containsText(str));
+ }
+
+ private static boolean containsText(CharSequence str) {
+ int strLen = str.length();
+ for (int i = 0; i < strLen; i++) {
+ if (!Character.isWhitespace(str.charAt(i))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static String[] tokenizeToStringArray(
+ String str, String delimiters, boolean trimTokens, boolean ignoreEmptyTokens) {
+
+ if (str == null) {
+ return new String[0];
+ }
+
+ StringTokenizer st = new StringTokenizer(str, delimiters);
+ List tokens = new ArrayList<>();
+ while (st.hasMoreTokens()) {
+ String token = st.nextToken();
+ if (trimTokens) {
+ token = token.trim();
+ }
+ if (!ignoreEmptyTokens || token.length() > 0) {
+ tokens.add(token);
+ }
+ }
+ return toStringArray(tokens);
+ }
+
+ public static String[] toStringArray(Collection collection) {
+ return collection.toArray(new String[0]);
+ }
+}
diff --git a/src/main/resources/logging.properties b/src/main/resources/logging.properties
index 51ba4e343e5dd24c96f84facfcf464f40d2520dc..ea576eff51754dee087cc256feeea654a190d67a 100644
--- a/src/main/resources/logging.properties
+++ b/src/main/resources/logging.properties
@@ -15,7 +15,7 @@
# Note that these classes must be on the system classpath.
# By default we only configure a ConsoleHandler, which will only
# show messages at the INFO and above levels.
-handlers= java.util.logging.ConsoleHandler
+handlers= java.util.logging.ConsoleHandler,java.util.logging.FileHandler
# To also add the FileHandler, use the following line instead.
#handlers= java.util.logging.FileHandler, java.util.logging.ConsoleHandler
diff --git a/src/test/java/com/rocket/pig/test/DemoTest.java b/src/test/java/com/rocket/pig/test/DemoTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..ffe3224e328bef9ecf21782a40e1f0aba3ab9fbe
--- /dev/null
+++ b/src/test/java/com/rocket/pig/test/DemoTest.java
@@ -0,0 +1,65 @@
+package com.rocket.pig.test;
+
+
+import jdk.internal.org.objectweb.asm.ClassWriter;
+import jdk.internal.org.objectweb.asm.MethodVisitor;
+import jdk.internal.org.objectweb.asm.Opcodes;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+public class DemoTest {
+
+ public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
+// 生成二进制字节码
+ byte[] bytes = generate();
+ // 使用自定义的ClassLoader
+ MyClassLoader cl = new MyClassLoader();
+ // 加载我们生成的 HelloWorld 类
+ Class> clazz = cl.defineClass("com.dadiyang.asm.HelloWorld", bytes);
+ // 反射获取 main 方法
+ Method main = clazz.getMethod("main", String[].class);
+ // 调用 main 方法
+ main.invoke(null, new Object[]{new String[]{}});
+
+ }
+
+ private static byte[] generate() {
+ ClassWriter cw = new ClassWriter(0);
+ // 定义对象头:版本号、修饰符、全类名、签名、父类、实现的接口
+ cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "com/dadiyang/asm/HelloWorld",
+ null, "java/lang/Object", null);
+ // 添加方法:修饰符、方法名、描述符、签名、抛出的异常
+ MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "main",
+ "([Ljava/lang/String;)V", null, null);
+ // 执行指令:获取静态属性
+ mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
+ // 加载常量 load constant
+ mv.visitLdcInsn("HelloWorld!");
+ // 调用方法
+ mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
+ // 返回
+ mv.visitInsn(Opcodes.RETURN);
+ // 设置栈大小和局部变量表大小
+ mv.visitMaxs(2, 1);
+ // 方法结束
+ mv.visitEnd();
+ // 类完成
+ cw.visitEnd();
+ // 生成字节数组
+ return cw.toByteArray();
+ }
+}
+
+/**
+ * 自定义ClassLoader以支持加载字节数组形式的字节码
+ *
+ * @author dadiyang
+ */
+class MyClassLoader extends ClassLoader {
+ public Class> defineClass(String name, byte[] b) {
+ // ClassLoader是个抽象类,而ClassLoader.defineClass 方法是protected的
+ // 所以我们需要定义一个子类将这个方法暴露出来
+ return super.defineClass(name, b, 0, b.length);
+ }
+}
diff --git a/src/test/java/com/rocket/pig/test/HelloWorldRefection.java b/src/test/java/com/rocket/pig/test/HelloWorldRefection.java
new file mode 100644
index 0000000000000000000000000000000000000000..fc27460aa887e357abb33b6444e8864ba3675510
--- /dev/null
+++ b/src/test/java/com/rocket/pig/test/HelloWorldRefection.java
@@ -0,0 +1,18 @@
+package com.rocket.pig.test;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+class ReflectDemo {
+ public static void main(String[] args) {
+ System.out.println("HelloWorld");
+ }
+}
+
+public class HelloWorldRefection {
+ public static void main(String[] args) throws InstantiationException, IllegalAccessException,
+ SecurityException, NoSuchMethodException, IllegalArgumentException, InvocationTargetException {
+ Method method = ReflectDemo.class.getMethod("main",String[].class);
+ method.invoke(null,(Object)new String[]{});
+ }
+}