diff --git a/jpress-commons/src/main/java/io/jpress/commons/utils/AntPathMatcher.java b/jpress-commons/src/main/java/io/jpress/commons/utils/AntPathMatcher.java new file mode 100644 index 0000000000000000000000000000000000000000..c52ae8d54f5b74720e9641a3b9edfb04e0e96760 --- /dev/null +++ b/jpress-commons/src/main/java/io/jpress/commons/utils/AntPathMatcher.java @@ -0,0 +1,908 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.jpress.commons.utils; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * {@link PathMatcher} implementation for Ant-style path patterns. + * + *
Part of this mapping code has been kindly borrowed from Apache Ant. + * + *
The mapping matches URLs using the following rules:
+ *
com/**/test.jsp
— matches all {@code test.jsp}
+ * files underneath the {@code com} pathorg/springframework/**/*.jsp
— matches all
+ * {@code .jsp} files underneath the {@code org/springframework} pathorg/**/servlet/bla.jsp
— matches
+ * {@code org/springframework/servlet/bla.jsp} but also
+ * {@code org/springframework/testing/servlet/bla.jsp} and {@code org/servlet/bla.jsp}Note: a pattern and a path must both be absolute or must
+ * both be relative in order for the two to match. Therefore it is recommended
+ * that users of this implementation to sanitize patterns in order to prefix
+ * them with "/" as it makes sense in the context in which they're used.
+ *
+ * @author Alef Arendsen
+ * @author Juergen Hoeller
+ * @author Rob Harrop
+ * @author Arjen Poutsma
+ * @author Rossen Stoyanchev
+ * @author Sam Brannen
+ * @since 16.07.2003
+ */
+public class AntPathMatcher implements PathMatcher {
+
+ /**
+ * Default path separator: "/".
+ */
+ public static final String DEFAULT_PATH_SEPARATOR = "/";
+
+ private static final int CACHE_TURNOFF_THRESHOLD = 65536;
+
+ private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\{[^/]+?\\}");
+
+ private static final char[] WILDCARD_CHARS = {'*', '?', '{'};
+
+
+ private String pathSeparator;
+
+ private PathSeparatorPatternCache pathSeparatorPatternCache;
+
+ private boolean caseSensitive = true;
+
+ private boolean trimTokens = false;
+
+ private volatile Boolean cachePatterns;
+
+ private final Map 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.
+ *
+ * @see #getStringMatcher(String)
+ * @since 4.0.1
+ */
+ public void setCachePatterns(boolean cachePatterns) {
+ this.cachePatterns = cachePatterns;
+ }
+
+ private void deactivatePatternCache() {
+ this.cachePatterns = false;
+ this.tokenizedPatternCache.clear();
+ this.stringMatcherCache.clear();
+ }
+
+
+ @Override
+ public boolean isPattern(String path) {
+ return (path.indexOf('*') != -1 || path.indexOf('?') != -1);
+ }
+
+ @Override
+ public boolean match(String pattern, String path) {
+ return doMatch(pattern, path, true, null);
+ }
+
+ @Override
+ 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 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 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: Assumes that {@link #match} returns {@code true} for '{@code pattern}' and '{@code path}', but
+ * does not enforce this.
+ */
+ @Override
+ 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();
+ }
+
+ @Override
+ public Map 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.
+ * 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:
+ * 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
+ */
+ @Override
+ public Comparator 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 In order, the most "generic" pattern is determined by the following:
+ * The default implementation is {@link AntPathMatcher}, supporting the
+ * Ant-style pattern syntax.
+ *
+ * @author Juergen Hoeller
+ * @see AntPathMatcher
+ * @since 1.2
+ */
+public interface PathMatcher {
+
+ /**
+ * Does the given {@code path} represent a pattern that can be matched
+ * by an implementation of this interface?
+ * If the return value is {@code false}, then the {@link #match}
+ * method does not have to be used because direct equality comparisons
+ * on the static path Strings will lead to the same result.
+ *
+ * @param path the path String to check
+ * @return {@code true} if the given {@code path} represents a pattern
+ */
+ boolean isPattern(String path);
+
+ /**
+ * Match the given {@code path} against the given {@code pattern},
+ * according to this PathMatcher's matching strategy.
+ *
+ * @param pattern the pattern to match against
+ * @param path the path String to test
+ * @return {@code true} if the supplied {@code path} matched,
+ * {@code false} if it didn't
+ */
+ boolean match(String pattern, String path);
+
+ /**
+ * Match the given {@code path} against the corresponding part of the given
+ * {@code pattern}, according to this PathMatcher's matching strategy.
+ * Determines whether the pattern at least matches as far as the given base
+ * path goes, assuming that a full path may then match as well.
+ *
+ * @param pattern the pattern to match against
+ * @param path the path String to test
+ * @return {@code true} if the supplied {@code path} matched,
+ * {@code false} if it didn't
+ */
+ boolean matchStart(String pattern, String path);
+
+ /**
+ * Given a pattern and a full path, determine the pattern-mapped part.
+ * This method is supposed to find out which part of the path is matched
+ * dynamically through an actual pattern, that is, it strips off a statically
+ * defined leading path from the given full path, returning only the actually
+ * pattern-matched part of the path.
+ * For example: For "myroot/*.html" as pattern and "myroot/myfile.html"
+ * as full path, this method should return "myfile.html". The detailed
+ * determination rules are specified to this PathMatcher's matching strategy.
+ * A simple implementation may return the given full path as-is in case
+ * of an actual pattern, and the empty String in case of the pattern not
+ * containing any dynamic parts (i.e. the {@code pattern} parameter being
+ * a static path that wouldn't qualify as an actual {@link #isPattern pattern}).
+ * A sophisticated implementation will differentiate between the static parts
+ * and the dynamic parts of the given path pattern.
+ *
+ * @param pattern the path pattern
+ * @param path the full path to introspect
+ * @return the pattern-mapped part of the given {@code path}
+ * (never {@code null})
+ */
+ String extractPathWithinPattern(String pattern, String path);
+
+ /**
+ * Given a pattern and a full path, extract the URI template variables. URI template
+ * variables are expressed through curly brackets ('{' and '}').
+ * For example: For pattern "/hotels/{hotel}" and path "/hotels/1", this method will
+ * return a map containing "hotel"->"1".
+ *
+ * @param pattern the path pattern, possibly containing URI templates
+ * @param path the full path to extract template variables from
+ * @return a map, containing variable names as keys; variables values as values
+ */
+ Map The full algorithm used depends on the underlying implementation,
+ * but generally, the returned {@code Comparator} will
+ * {@linkplain java.util.List#sort(java.util.Comparator) sort}
+ * a list so that more specific patterns come before generic patterns.
+ *
+ * @param path the full path to use for comparison
+ * @return a comparator capable of sorting patterns in order of explicitness
+ */
+ Comparator The full algorithm used for combining the two pattern depends on the underlying implementation.
+ *
+ * @param pattern1 the first pattern
+ * @param pattern2 the second pattern
+ * @return the combination of the two patterns
+ * @throws IllegalArgumentException when the two patterns cannot be combined
+ */
+ String combine(String pattern1, String pattern2);
+
+}
diff --git a/jpress-commons/src/main/java/io/jpress/commons/utils/StringUtils.java b/jpress-commons/src/main/java/io/jpress/commons/utils/StringUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..f844a4e8a90d26654c66a20509bf52e51e793149
--- /dev/null
+++ b/jpress-commons/src/main/java/io/jpress/commons/utils/StringUtils.java
@@ -0,0 +1,46 @@
+package io.jpress.commons.utils;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.StringTokenizer;
+
+public abstract class StringUtils {
+
+ 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
+ *
+ * Examples
+ *
+ *
+ *
+ * @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
+ */
+ @Override
+ 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.isEmpty());
+ boolean ext2All = (ext2.equals(".*") || ext2.isEmpty());
+ 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.
+ *
+ * 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}
+ *
+ * the returned comparator will sort this list so that the order will be as indicated.
+ *
+ *
+ */
+ protected static class AntPatternComparator implements Comparator