http://git-wip-us.apache.org/repos/asf/incubator-tamaya/blob/95885781/core/src/main/java/org/apache/tamaya/core/internal/resource/AntPathMatcher.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/tamaya/core/internal/resource/AntPathMatcher.java b/core/src/main/java/org/apache/tamaya/core/internal/resource/AntPathMatcher.java index bfb8182..ae44377 100644 --- a/core/src/main/java/org/apache/tamaya/core/internal/resource/AntPathMatcher.java +++ b/core/src/main/java/org/apache/tamaya/core/internal/resource/AntPathMatcher.java @@ -24,12 +24,12 @@ import java.util.regex.Pattern; /** * PathMatcher implementation for Ant-style path patterns. Examples are provided below. - * + * <p> * <p>Part current this annotation code has been kindly borrowed from <a href="http://ant.apache.org">Apache Ant</a>. - * + * <p> * <p>The annotation matches URLs using the following rules:<br> <ul> <li>? matches one character</li> <li>* matches zero * or more characters</li> <li>** matches zero or more 'directories' in a path</li> </ul> - * + * <p> * <p>Some examples:<br> <ul> <li>{@code com/t?st.jsp} - matches {@code com/testdata.jsp} but also * {@code com/tast.jsp} or {@code com/txst.jsp}</li> <li>{@code com/*.jsp} - matches all * {@code .jsp} files in the {@code com} directory</li> <li>{@code com/**/testdata.jsp} - matches all @@ -47,733 +47,729 @@ import java.util.regex.Pattern; */ class AntPathMatcher { - /** 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 String pathSeparator; - - private PathSeparatorPatternCache pathSeparatorPatternCache; - - private boolean trimTokens = true; - - private volatile Boolean cachePatterns; - - private final Map<String, String[]> tokenizedPatternCache = new ConcurrentHashMap<>(256); - - final Map<String, AntPathStringMatcher> 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 convenience 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) { - Objects.requireNonNull(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 trim tokenized paths and patterns. - * Default is {@code true}. - */ - 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 keys current {@code true} - * activates an unlimited pattern cache; a keys current {@code false} turns - * the pattern cache off completely. - * <p>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 current patterns - * are coming in, with little chance for encountering a reoccurring pattern. - * @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 testdata - * @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<String, String> uriTemplateVariables) { - if (path.startsWith(this.pathSeparator) != pattern.startsWith(this.pathSeparator)) { - return false; - } - - String[] pattDirs = tokenizePattern(pattern); - 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 current pattern is * or **'s - if (pattIdxStart > pattIdxEnd) { - return (pattern.endsWith(this.pathSeparator) ? path.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; - } - - /** - * Tokenize the given path pattern into parts, based on this matcher's settings. - * <p>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) { - 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) { - 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); - } - - /** - * Tests 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<String, String> uriTemplateVariables) { - return getStringMatcher(pattern).matchStrings(str, uriTemplateVariables); - } - - /** - * Build or retrieve an {@link AntPathStringMatcher} for the given pattern. - * <p>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 current patterns - * are coming in, with little chance for encountering a reoccurring pattern. - * <p>This method may get 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) { - matcher = this.stringMatcherCache.get(pattern); - } - if (matcher == null) { - matcher = new AntPathStringMatcher(pattern); - 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) { - this.stringMatcherCache.put(pattern, matcher); - } - } - return matcher; - } - - /** - * Given a pattern and a full path, determine the pattern-mapped part. <p>For example: <ul> - * <li>'{@code /docs/cvs/commit.html}' and '{@code /docs/cvs/commit.html} -> ''</li> - * <li>'{@code /docs/*}' and '{@code /docs/cvs/commit} -> '{@code cvs/commit}'</li> - * <li>'{@code /docs/cvs/*.html}' and '{@code /docs/cvs/commit.html} -> '{@code commit.html}'</li> - * <li>'{@code /docs/**}' and '{@code /docs/cvs/commit} -> '{@code cvs/commit}'</li> - * <li>'{@code /docs/**\/*.html}' and '{@code /docs/cvs/commit.html} -> '{@code cvs/commit.html}'</li> - * <li>'{@code /*.html}' and '{@code /docs/cvs/commit.html} -> '{@code docs/cvs/commit.html}'</li> - * <li>'{@code *.html}' and '{@code /docs/cvs/commit.html} -> '{@code /docs/cvs/commit.html}'</li> - * <li>'{@code *}' and '{@code /docs/cvs/commit.html} -> '{@code /docs/cvs/commit.html}'</li> </ul> - * <p>Assumes that {@link #match} returns {@code true} for '{@code pattern}' and '{@code path}', but - * does <strong>not</strong> 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<String, String> extractUriTemplateVariables(String pattern, String path) { - Map<String, String> variables = new LinkedHashMap<>(); - boolean result = doMatch(pattern, path, true, variables); - if(!result){ + /** + * 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 String pathSeparator; + + private PathSeparatorPatternCache pathSeparatorPatternCache; + + private boolean trimTokens = true; + + private volatile Boolean cachePatterns; + + private final Map<String, String[]> tokenizedPatternCache = new ConcurrentHashMap<>(256); + + final Map<String, AntPathStringMatcher> 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 convenience 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) { + Objects.requireNonNull(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 trim tokenized paths and patterns. + * Default is {@code true}. + */ + 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 keys current {@code true} + * activates an unlimited pattern cache; a keys current {@code false} turns + * the pattern cache off completely. + * <p>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 current patterns + * are coming in, with little chance for encountering a reoccurring pattern. + * + * @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 testdata + * @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<String, String> uriTemplateVariables) { + if (path.startsWith(this.pathSeparator) != pattern.startsWith(this.pathSeparator)) { + return false; + } + + String[] pattDirs = tokenizePattern(pattern); + 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 current pattern is * or **'s + if (pattIdxStart > pattIdxEnd) { + return (pattern.endsWith(this.pathSeparator) ? path.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; + } + + /** + * Tokenize the given path pattern into parts, based on this matcher's settings. + * <p>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) { + 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) { + 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); + } + + /** + * Tests 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<String, String> uriTemplateVariables) { + return getStringMatcher(pattern).matchStrings(str, uriTemplateVariables); + } + + /** + * Build or retrieve an {@link AntPathStringMatcher} for the given pattern. + * <p>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 current patterns + * are coming in, with little chance for encountering a reoccurring pattern. + * <p>This method may get 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) { + matcher = this.stringMatcherCache.get(pattern); + } + if (matcher == null) { + matcher = new AntPathStringMatcher(pattern); + 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) { + this.stringMatcherCache.put(pattern, matcher); + } + } + return matcher; + } + + /** + * Given a pattern and a full path, determine the pattern-mapped part. <p>For example: <ul> + * <li>'{@code /docs/cvs/commit.html}' and '{@code /docs/cvs/commit.html} -> ''</li> + * <li>'{@code /docs/*}' and '{@code /docs/cvs/commit} -> '{@code cvs/commit}'</li> + * <li>'{@code /docs/cvs/*.html}' and '{@code /docs/cvs/commit.html} -> '{@code commit.html}'</li> + * <li>'{@code /docs/**}' and '{@code /docs/cvs/commit} -> '{@code cvs/commit}'</li> + * <li>'{@code /docs/**\/*.html}' and '{@code /docs/cvs/commit.html} -> '{@code cvs/commit.html}'</li> + * <li>'{@code /*.html}' and '{@code /docs/cvs/commit.html} -> '{@code docs/cvs/commit.html}'</li> + * <li>'{@code *.html}' and '{@code /docs/cvs/commit.html} -> '{@code /docs/cvs/commit.html}'</li> + * <li>'{@code *}' and '{@code /docs/cvs/commit.html} -> '{@code /docs/cvs/commit.html}'</li> </ul> + * <p>Assumes that {@link #match} returns {@code true} for '{@code pattern}' and '{@code path}', but + * does <strong>not</strong> 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<String, String> extractUriTemplateVariables(String pattern, String path) { + Map<String, String> variables = new LinkedHashMap<>(); + boolean result = doMatch(pattern, path, true, variables); + if (!result) { throw new IllegalArgumentException("Pattern \"" + pattern + "\" is not a match for \"" + path + "\""); } - return variables; - } - - /** - * Combines two patterns into a new pattern that is returned. - * <p>This implementation simply concatenates the two patterns, unless the first pattern - * contains a file extension match (such as {@code *.html}. In that case, the second pattern - * should be included in the first, or an {@code IllegalArgumentException} is thrown. - * <p>For example: <table> - * <tr><th>Pattern 1</th><th>Pattern 2</th><th>Result</th></tr> <tr><td>/hotels</td><td>{@code - * null}</td><td>/hotels</td></tr> <tr><td>{@code null}</td><td>/hotels</td><td>/hotels</td></tr> - * <tr><td>/hotels</td><td>/bookings</td><td>/hotels/bookings</td></tr> <tr><td>/hotels</td><td>bookings</td><td>/hotels/bookings</td></tr> - * <tr><td>/hotels/*</td><td>/bookings</td><td>/hotels/bookings</td></tr> <tr><td>/hotels/**</td><td>/bookings</td><td>/hotels/**/bookings</td></tr> - * <tr><td>/hotels</td><td>{hotel}</td><td>/hotels/{hotel}</td></tr> <tr><td>/hotels/*</td><td>{hotel}</td><td>/hotels/{hotel}</td></tr> - * <tr><td>/hotels/**</td><td>{hotel}</td><td>/hotels/**/{hotel}</td></tr> - * <tr><td>/*.html</td><td>/hotels.html</td><td>/hotels.html</td></tr> <tr><td>/*.html</td><td>/hotels</td><td>/hotels.html</td></tr> - * <tr><td>/*.html</td><td>/*.txt</td><td>IllegalArgumentException</td></tr> </table> - * @param pattern1 the first pattern - * @param pattern2 the second pattern - * @return the combination current the two patterns - * @throws IllegalArgumentException when 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 extension1 = pattern1.substring(starDotPos1 + 1); - int dotPos2 = pattern2.indexOf('.'); - String fileName2 = (dotPos2 == -1 ? pattern2 : pattern2.substring(0, dotPos2)); - String extension2 = (dotPos2 == -1 ? "" : pattern2.substring(dotPos2)); - String extension = extension1.startsWith("*") ? extension2 : extension1; - return fileName2 + extension; - } - - private String concat(String path1, String path2) { - if (path1.endsWith(this.pathSeparator) || path2.startsWith(this.pathSeparator)) { - return path1 + path2; - } - return path1 + this.pathSeparator + path2; - } - - /** - * Given a full path, returns a {@link Comparator} suitable for sorting patterns in order current explicitness. - * <p>The returned {@code Comparator} will {@linkplain java.util.Collections#sort(java.util.List, - * java.util.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: <ol> <li>{@code /hotels/new}</li> - * <li>{@code /hotels/{hotel}}</li> <li>{@code /hotels/*}</li> </ol> the returned comparator will sort this - * list so that the order will be as indicated. - * <p>The full path given as parameter is used to testdata 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 current sorting patterns in order current explicitness - */ - public Comparator<String> getPatternComparator(String path) { - return new AntPatternComparator(path); - } - - - /** - * Tests whether or not a string matches against a pattern via a {@link Pattern}. - * <p>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 <tt>/users/{user}</tt>. - */ - 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<String> variableNames = new LinkedList<>(); - - public AntPathStringMatcher(String pattern) { - StringBuilder patternBuilder = new StringBuilder(); - Matcher m = GLOB_PATTERN.matcher(pattern); - int end = 0; - while (m.find()) { - patternBuilder.append(quote(pattern, end, m.start())); - String match = m.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(m.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 = m.end(); - } - patternBuilder.append(quote(pattern, end, pattern.length())); - this.pattern = Pattern.compile(patternBuilder.toString()); - } - - 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<String, String> uriTemplateVariables) { - Matcher matcher = this.pattern.matcher(str); - if (matcher.matches()) { - if (uriTemplateVariables != null) { - // SPR-8455 - if(!(this.variableNames.size() == matcher.groupCount())) { + return variables; + } + + /** + * Combines two patterns into a new pattern that is returned. + * <p>This implementation simply concatenates the two patterns, unless the first pattern + * contains a file extension match (such as {@code *.html}. In that case, the second pattern + * should be included in the first, or an {@code IllegalArgumentException} is thrown. + * <p>For example: <table> + * <tr><th>Pattern 1</th><th>Pattern 2</th><th>Result</th></tr> <tr><td>/hotels</td><td>{@code + * null}</td><td>/hotels</td></tr> <tr><td>{@code null}</td><td>/hotels</td><td>/hotels</td></tr> + * <tr><td>/hotels</td><td>/bookings</td><td>/hotels/bookings</td></tr> <tr><td>/hotels</td><td>bookings</td><td>/hotels/bookings</td></tr> + * <tr><td>/hotels/*</td><td>/bookings</td><td>/hotels/bookings</td></tr> <tr><td>/hotels/**</td><td>/bookings</td><td>/hotels/**/bookings</td></tr> + * <tr><td>/hotels</td><td>{hotel}</td><td>/hotels/{hotel}</td></tr> <tr><td>/hotels/*</td><td>{hotel}</td><td>/hotels/{hotel}</td></tr> + * <tr><td>/hotels/**</td><td>{hotel}</td><td>/hotels/**/{hotel}</td></tr> + * <tr><td>/*.html</td><td>/hotels.html</td><td>/hotels.html</td></tr> <tr><td>/*.html</td><td>/hotels</td><td>/hotels.html</td></tr> + * <tr><td>/*.html</td><td>/*.txt</td><td>IllegalArgumentException</td></tr> </table> + * + * @param pattern1 the first pattern + * @param pattern2 the second pattern + * @return the combination current the two patterns + * @throws IllegalArgumentException when 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 extension1 = pattern1.substring(starDotPos1 + 1); + int dotPos2 = pattern2.indexOf('.'); + String fileName2 = (dotPos2 == -1 ? pattern2 : pattern2.substring(0, dotPos2)); + String extension2 = (dotPos2 == -1 ? "" : pattern2.substring(dotPos2)); + String extension = extension1.startsWith("*") ? extension2 : extension1; + return fileName2 + extension; + } + + private String concat(String path1, String path2) { + if (path1.endsWith(this.pathSeparator) || path2.startsWith(this.pathSeparator)) { + return path1 + path2; + } + return path1 + this.pathSeparator + path2; + } + + /** + * Given a full path, returns a {@link Comparator} suitable for sorting patterns in order current explicitness. + * <p>The returned {@code Comparator} will {@linkplain java.util.Collections#sort(java.util.List, + * java.util.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: <ol> <li>{@code /hotels/new}</li> + * <li>{@code /hotels/{hotel}}</li> <li>{@code /hotels/*}</li> </ol> the returned comparator will sort this + * list so that the order will be as indicated. + * <p>The full path given as parameter is used to testdata 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 current sorting patterns in order current explicitness + */ + public Comparator<String> getPatternComparator(String path) { + return new AntPatternComparator(path); + } + + + /** + * Tests whether or not a string matches against a pattern via a {@link Pattern}. + * <p>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 <tt>/users/{user}</tt>. + */ + 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<String> variableNames = new LinkedList<>(); + + public AntPathStringMatcher(String pattern) { + StringBuilder patternBuilder = new StringBuilder(); + Matcher m = GLOB_PATTERN.matcher(pattern); + int end = 0; + while (m.find()) { + patternBuilder.append(quote(pattern, end, m.start())); + String match = m.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(m.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 = m.end(); + } + patternBuilder.append(quote(pattern, end, pattern.length())); + this.pattern = Pattern.compile(patternBuilder.toString()); + } + + 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<String, String> uriTemplateVariables) { + Matcher matcher = this.pattern.matcher(str); + if (matcher.matches()) { + if (uriTemplateVariables != null) { + // SPR-8455 + if (!(this.variableNames.size() == matcher.groupCount())) { throw new IllegalStateException( "The number current capturing groups in the pattern segment " + this.pattern + " does not match the number current 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)}. - * <p>In order, the most "generic" pattern is determined by the following: - * <ul> - * <li>if it's null or a capture all pattern (i.e. it is equal to "/**")</li> - * <li>if the other pattern is an actual match</li> - * <li>if it's a catch-all pattern (i.e. it ends with "**"</li> - * <li>if it's got more "*" than the other pattern</li> - * <li>if it's got more "{foo}" than the other pattern</li> - * <li>if it's shorter than the other pattern</li> - * </ul> - */ - protected static class AntPatternComparator implements Comparator<String> { - - 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 current - * occurrences current "*", "**", 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; - 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 (!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 current the given pattern, where template variables are considered to be 1 long. - */ - public int getLength() { - if (this.length == null) { - this.length = VARIABLE_PATTERN.matcher(this.pattern).replaceAll("#").length(); - } - 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; - } - } + 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)}. + * <p>In order, the most "generic" pattern is determined by the following: + * <ul> + * <li>if it's null or a capture all pattern (i.e. it is equal to "/**")</li> + * <li>if the other pattern is an actual match</li> + * <li>if it's a catch-all pattern (i.e. it ends with "**"</li> + * <li>if it's got more "*" than the other pattern</li> + * <li>if it's got more "{foo}" than the other pattern</li> + * <li>if it's shorter than the other pattern</li> + * </ul> + */ + protected static class AntPatternComparator implements Comparator<String> { + + 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 current + * occurrences current "*", "**", 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; + 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 (!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 current the given pattern, where template variables are considered to be 1 long. + */ + public int getLength() { + if (this.length == null) { + this.length = VARIABLE_PATTERN.matcher(this.pattern).replaceAll("#").length(); + } + 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; + } + } }
http://git-wip-us.apache.org/repos/asf/incubator-tamaya/blob/95885781/core/src/main/java/org/apache/tamaya/core/internal/resource/ClassPathResource.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/tamaya/core/internal/resource/ClassPathResource.java b/core/src/main/java/org/apache/tamaya/core/internal/resource/ClassPathResource.java index 6b10e8b..15d04d6 100644 --- a/core/src/main/java/org/apache/tamaya/core/internal/resource/ClassPathResource.java +++ b/core/src/main/java/org/apache/tamaya/core/internal/resource/ClassPathResource.java @@ -28,227 +28,231 @@ import java.util.Objects; /** * {@link Resource} implementation for class path resources. * Uses either a given ClassLoader or a given Class for loading resources. - * + * <p> * <p>Supports resolution as {@code java.io.File} if the class path * resource resides in the file system, but not for resources in a JAR. * Always supports resolution as URL. * * @author Juergen Hoeller * @author Sam Brannen - * @since 28.12.2003 * @see ClassLoader#getResourceAsStream(String) * @see Class#getResourceAsStream(String) + * @since 28.12.2003 */ public class ClassPathResource extends AbstractFileResolvingResource { - private final String path; - - private ClassLoader classLoader; - - private Class<?> clazz; - - - /** - * Create a new {@code ClassPathResource} for {@code ClassLoader} usage. - * A leading slash will be removed, as the ClassLoader resource access - * methods will not accept it. - * <p>The thread context class loader will be used for - * loading the resource. - * @param path the absolute path within the class path - * @see java.lang.ClassLoader#getResourceAsStream(String) - */ - public ClassPathResource(String path) { - this(path, (ClassLoader) null); - } - - /** - * Create a new {@code ClassPathResource} for {@code ClassLoader} usage. - * A leading slash will be removed, as the ClassLoader resource access - * methods will not accept it. - * @param path the absolute path within the classpath - * @param classLoader the class loader to load the resource with, - * or {@code null} for the thread context class loader - * @see ClassLoader#getResourceAsStream(String) - */ - public ClassPathResource(String path, ClassLoader classLoader) { - Objects.requireNonNull(path, "Path must not be null"); - String pathToUse = StringUtils.cleanPath(path); - if (pathToUse.startsWith("/")) { - pathToUse = pathToUse.substring(1); - } - this.path = pathToUse; - this.classLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader()); - } - - /** - * Create a new {@code ClassPathResource} for {@code Class} usage. - * The path can be relative to the given class, or absolute within - * the classpath via a leading slash. - * @param path relative or absolute path within the class path - * @param clazz the class to load resources with - * @see java.lang.Class#getResourceAsStream - */ - public ClassPathResource(String path, Class<?> clazz) { - Objects.requireNonNull(path, "Path must not be null"); - this.path = StringUtils.cleanPath(path); - this.clazz = clazz; - } - - /** - * Create a new {@code ClassPathResource} with optional {@code ClassLoader} - * and {@code Class}. Only for internal usage. - * @param path relative or absolute path within the classpath - * @param classLoader the class loader to load the resource with, if any - * @param clazz the class to load resources with, if any - */ - protected ClassPathResource(String path, ClassLoader classLoader, Class<?> clazz) { - this.path = StringUtils.cleanPath(path); - this.classLoader = classLoader; - this.clazz = clazz; - } - - - /** - * Return the path for this resource (as resource path within the class path). - */ - public final String getPath() { - return this.path; - } - - /** - * Return the ClassLoader that this resource will be obtained from. - */ - public final ClassLoader getClassLoader() { - return (this.clazz != null ? this.clazz.getClassLoader() : this.classLoader); - } - - - /** - * This implementation checks for the resolution current a resource URL. - * @see java.lang.ClassLoader#getResource(String) - * @see java.lang.Class#getResource(String) - */ - @Override - public boolean exists() { - return (resolveURL() != null); - } - - /** - * Resolves a URL for the underlying class path resource. - * @return the resolved URL, or {@code null} if not resolvable - */ - protected URL resolveURL() { - if (this.clazz != null) { - return this.clazz.getResource(this.path); - } - else if (this.classLoader != null) { - return this.classLoader.getResource(this.path); - } - else { - return ClassLoader.getSystemResource(this.path); - } - } - - /** - * This implementation opens an InputStream for the given class path resource. - * @see java.lang.ClassLoader#getResourceAsStream(String) - * @see java.lang.Class#getResourceAsStream(String) - */ - @Override - public InputStream getInputStream()throws IOException { - InputStream is; - if (this.clazz != null) { - is = this.clazz.getResourceAsStream(this.path); - } - else if (this.classLoader != null) { - is = this.classLoader.getResourceAsStream(this.path); - } - else { - is = ClassLoader.getSystemResourceAsStream(this.path); - } - if (is == null) { - throw new IOException(getDisplayName() + " cannot be opened because it does not exist"); - } - return is; - } - - /** - * This implementation returns a URL for the underlying class path resource, - * if available. - * @see java.lang.ClassLoader#getResource(String) - * @see java.lang.Class#getResource(String) - */ - @Override - public URL toURL() throws IOException { - URL url = resolveURL(); - if (url == null) { - throw new FileNotFoundException(getDisplayName() + " cannot be resolved to URL because it does not exist"); - } - return url; - } - - /** - * This implementation creates a ClassPathResource, applying the given path - * relative to the path current the underlying resource current this descriptor. - */ - @Override - public Resource createRelative(String relativePath) { - String pathToUse = StringUtils.applyRelativePath(this.path, relativePath); - return new ClassPathResource(pathToUse, this.classLoader, this.clazz); - } - - /** - * This implementation returns the name current the file that this class path - * resource refers to. - */ - @Override - public String getDisplayName() { - return StringUtils.getFilename(this.path); - } - - /** - * This implementation returns a description that includes the class path location. - */ + private final String path; + + private ClassLoader classLoader; + + private Class<?> clazz; + + + /** + * Create a new {@code ClassPathResource} for {@code ClassLoader} usage. + * A leading slash will be removed, as the ClassLoader resource access + * methods will not accept it. + * <p>The thread context class loader will be used for + * loading the resource. + * + * @param path the absolute path within the class path + * @see java.lang.ClassLoader#getResourceAsStream(String) + */ + public ClassPathResource(String path) { + this(path, (ClassLoader) null); + } + + /** + * Create a new {@code ClassPathResource} for {@code ClassLoader} usage. + * A leading slash will be removed, as the ClassLoader resource access + * methods will not accept it. + * + * @param path the absolute path within the classpath + * @param classLoader the class loader to load the resource with, + * or {@code null} for the thread context class loader + * @see ClassLoader#getResourceAsStream(String) + */ + public ClassPathResource(String path, ClassLoader classLoader) { + Objects.requireNonNull(path, "Path must not be null"); + String pathToUse = StringUtils.cleanPath(path); + if (pathToUse.startsWith("/")) { + pathToUse = pathToUse.substring(1); + } + this.path = pathToUse; + this.classLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader()); + } + + /** + * Create a new {@code ClassPathResource} for {@code Class} usage. + * The path can be relative to the given class, or absolute within + * the classpath via a leading slash. + * + * @param path relative or absolute path within the class path + * @param clazz the class to load resources with + * @see java.lang.Class#getResourceAsStream + */ + public ClassPathResource(String path, Class<?> clazz) { + Objects.requireNonNull(path, "Path must not be null"); + this.path = StringUtils.cleanPath(path); + this.clazz = clazz; + } + + /** + * Create a new {@code ClassPathResource} with optional {@code ClassLoader} + * and {@code Class}. Only for internal usage. + * + * @param path relative or absolute path within the classpath + * @param classLoader the class loader to load the resource with, if any + * @param clazz the class to load resources with, if any + */ + protected ClassPathResource(String path, ClassLoader classLoader, Class<?> clazz) { + this.path = StringUtils.cleanPath(path); + this.classLoader = classLoader; + this.clazz = clazz; + } + + + /** + * Return the path for this resource (as resource path within the class path). + */ + public final String getPath() { + return this.path; + } + + /** + * Return the ClassLoader that this resource will be obtained from. + */ + public final ClassLoader getClassLoader() { + return (this.clazz != null ? this.clazz.getClassLoader() : this.classLoader); + } + + + /** + * This implementation checks for the resolution current a resource URL. + * + * @see java.lang.ClassLoader#getResource(String) + * @see java.lang.Class#getResource(String) + */ @Override - public String toString() { - StringBuilder builder = new StringBuilder("ClassPathResource ["); - String pathToUse = path; - if (this.clazz != null && !pathToUse.startsWith("/")) { - builder.append(ClassUtils.classPackageAsResourcePath(this.clazz)); - builder.append('/'); - } - if (pathToUse.startsWith("/")) { - pathToUse = pathToUse.substring(1); - } - builder.append(pathToUse); - builder.append(']'); - return builder.toString(); - } - - /** - * This implementation compares the underlying class path locations. - */ - @Override - public boolean equals(Object obj) { - if (obj == this) { - return true; - } - if (obj instanceof ClassPathResource) { - ClassPathResource otherRes = (ClassPathResource) obj; - return (this.path.equals(otherRes.path) && - Objects.equals(this.classLoader, otherRes.classLoader) && + public boolean exists() { + return (resolveURL() != null); + } + + /** + * Resolves a URL for the underlying class path resource. + * + * @return the resolved URL, or {@code null} if not resolvable + */ + protected URL resolveURL() { + if (this.clazz != null) { + return this.clazz.getResource(this.path); + } else if (this.classLoader != null) { + return this.classLoader.getResource(this.path); + } else { + return ClassLoader.getSystemResource(this.path); + } + } + + /** + * This implementation opens an InputStream for the given class path resource. + * + * @see java.lang.ClassLoader#getResourceAsStream(String) + * @see java.lang.Class#getResourceAsStream(String) + */ + @Override + public InputStream getInputStream() throws IOException { + InputStream is; + if (this.clazz != null) { + is = this.clazz.getResourceAsStream(this.path); + } else if (this.classLoader != null) { + is = this.classLoader.getResourceAsStream(this.path); + } else { + is = ClassLoader.getSystemResourceAsStream(this.path); + } + if (is == null) { + throw new IOException(getDisplayName() + " cannot be opened because it does not exist"); + } + return is; + } + + /** + * This implementation returns a URL for the underlying class path resource, + * if available. + * + * @see java.lang.ClassLoader#getResource(String) + * @see java.lang.Class#getResource(String) + */ + @Override + public URL toURL() throws IOException { + URL url = resolveURL(); + if (url == null) { + throw new FileNotFoundException(getDisplayName() + " cannot be resolved to URL because it does not exist"); + } + return url; + } + + /** + * This implementation creates a ClassPathResource, applying the given path + * relative to the path current the underlying resource current this descriptor. + */ + @Override + public Resource createRelative(String relativePath) { + String pathToUse = StringUtils.applyRelativePath(this.path, relativePath); + return new ClassPathResource(pathToUse, this.classLoader, this.clazz); + } + + /** + * This implementation returns the name current the file that this class path + * resource refers to. + */ + @Override + public String getDisplayName() { + return StringUtils.getFilename(this.path); + } + + /** + * This implementation returns a description that includes the class path location. + */ + @Override + public String toString() { + StringBuilder builder = new StringBuilder("ClassPathResource ["); + String pathToUse = path; + if (this.clazz != null && !pathToUse.startsWith("/")) { + builder.append(ClassUtils.classPackageAsResourcePath(this.clazz)); + builder.append('/'); + } + if (pathToUse.startsWith("/")) { + pathToUse = pathToUse.substring(1); + } + builder.append(pathToUse); + builder.append(']'); + return builder.toString(); + } + + /** + * This implementation compares the underlying class path locations. + */ + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof ClassPathResource) { + ClassPathResource otherRes = (ClassPathResource) obj; + return (this.path.equals(otherRes.path) && + Objects.equals(this.classLoader, otherRes.classLoader) && Objects.equals(this.clazz, otherRes.clazz)); - } - return false; - } - - /** - * This implementation returns the hash code current the underlying - * class path location. - */ - @Override - public int hashCode() { - return this.path.hashCode(); - } + } + return false; + } + + /** + * This implementation returns the hash code current the underlying + * class path location. + */ + @Override + public int hashCode() { + return this.path.hashCode(); + } } http://git-wip-us.apache.org/repos/asf/incubator-tamaya/blob/95885781/core/src/main/java/org/apache/tamaya/core/internal/resource/DefaultResourceLoader.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/tamaya/core/internal/resource/DefaultResourceLoader.java b/core/src/main/java/org/apache/tamaya/core/internal/resource/DefaultResourceLoader.java index c65be65..c19ac62 100644 --- a/core/src/main/java/org/apache/tamaya/core/internal/resource/DefaultResourceLoader.java +++ b/core/src/main/java/org/apache/tamaya/core/internal/resource/DefaultResourceLoader.java @@ -1,3 +1,18 @@ +/* + * Copyright 2002-2014 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.apache.tamaya.core.internal.resource; import org.apache.tamaya.core.resources.Resource; @@ -18,7 +33,7 @@ import java.util.logging.Logger; * Simple default implementation of the resource loader. */ @Priority(0) -public class DefaultResourceLoader implements ResourceLoader{ +public class DefaultResourceLoader implements ResourceLoader { private static final Logger LOG = Logger.getLogger(DefaultResourceLoader.class.getName()); @@ -27,9 +42,9 @@ public class DefaultResourceLoader implements ResourceLoader{ @Override public List<Resource> getResources(ClassLoader classLoader, Collection<String> expressions) { List<Resource> resources = new ArrayList<>(); - for(String expression:expressions){ - if(tryClassPath(classLoader, expression, resources) || tryFile(expression, resources) || tryURL(expression, resources) - || tryAntPath(classLoader, expression, resources)){ + for (String expression : expressions) { + if (tryClassPath(classLoader, expression, resources) || tryFile(expression, resources) || tryURL(expression, resources) + || tryAntPath(classLoader, expression, resources)) { continue; } LOG.warning("Failed to resolve resource: " + expression); @@ -38,41 +53,38 @@ public class DefaultResourceLoader implements ResourceLoader{ } private boolean tryClassPath(ClassLoader classLoader, String expression, List<Resource> resources) { - try{ + try { Enumeration<URL> urls = classLoader.getResources(expression); - while(urls.hasMoreElements()){ + while (urls.hasMoreElements()) { URL url = urls.nextElement(); resources.add(new UrlResource(url)); } return !resources.isEmpty(); - } - catch(Exception e){ + } catch (Exception e) { LOG.finest(() -> "Failed to load resource from CP: " + expression); } return false; } private boolean tryFile(String expression, List<Resource> resources) { - try{ + try { File file = new File(expression); - if(file.exists()) { + if (file.exists()) { resources.add(new FileSystemResource(file)); return true; } - } - catch(Exception e){ + } catch (Exception e) { LOG.finest(() -> "Failed to load resource from file: " + expression); } return false; } private boolean tryURL(String expression, List<Resource> resources) { - try{ + try { URL url = new URL(expression); resources.add(new UrlResource(url)); return true; - } - catch(Exception e){ + } catch (Exception e) { LOG.finest(() -> "Failed to load resource from file: " + expression); } return false; @@ -81,11 +93,10 @@ public class DefaultResourceLoader implements ResourceLoader{ private boolean tryAntPath(ClassLoader classLoader, String expression, List<Resource> resources) { PathMatchingResourcePatternResolver loader = resourceLoaders.computeIfAbsent(classLoader, cl -> new PathMatchingResourcePatternResolver(cl)); - try{ + try { resources.addAll(Arrays.asList(loader.getResources(expression))); return !resources.isEmpty(); - } - catch(Exception e){ + } catch (Exception e) { LOG.finest(() -> "Failed to load resources from pattern: " + expression); } return false; http://git-wip-us.apache.org/repos/asf/incubator-tamaya/blob/95885781/core/src/main/java/org/apache/tamaya/core/internal/resource/FileSystemResource.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/tamaya/core/internal/resource/FileSystemResource.java b/core/src/main/java/org/apache/tamaya/core/internal/resource/FileSystemResource.java index efeecef..c5fa68e 100644 --- a/core/src/main/java/org/apache/tamaya/core/internal/resource/FileSystemResource.java +++ b/core/src/main/java/org/apache/tamaya/core/internal/resource/FileSystemResource.java @@ -33,188 +33,200 @@ import java.util.Objects; * Obviously supports resolution as File, and also as URL. * * @author Juergen Hoeller - * @since 28.12.2003 * @see java.io.File + * @since 28.12.2003 */ public class FileSystemResource implements Resource { - private final File file; - - private final String path; - - - /** - * Create a new {@code FileSystemResource} from a {@link File} handle. - * <p>Note: When building relative resources via {@link #createRelative}, - * the relative path will applyChanges <i>at the same directory level</i>: - * e.g. new File("C:/dir1"), relative path "dir2" -> "C:/dir2"! - * If you prefer to have relative paths built underneath the given root - * directory, use the {@link #FileSystemResource(String) constructor with a file path} - * to append a trailing slash to the root path: "C:/dir1/", which - * indicates this directory as root for all relative paths. - * @param file a File handle - */ - public FileSystemResource(File file) { - Objects.requireNonNull(file, "File must not be null"); - this.file = file; - this.path = StringUtils.cleanPath(file.getPath()); - } - - /** - * Create a new {@code FileSystemResource} from a file path. - * <p>Note: When building relative resources via {@link #createRelative}, - * it makes a difference whether the specified resource base path here - * ends with a slash or not. In the case current "C:/dir1/", relative paths - * will be built underneath that root: e.g. relative path "dir2" -> - * "C:/dir1/dir2". In the case current "C:/dir1", relative paths will applyChanges - * at the same directory level: relative path "dir2" -> "C:/dir2". - * @param path a file path - */ - public FileSystemResource(String path) { - Objects.requireNonNull(path, "Path must not be null"); - this.file = new File(path); - this.path = StringUtils.cleanPath(path); - } - - - /** - * Return the file path for this resource. - */ - public final String getPath() { - return this.path; - } - - - /** - * This implementation returns whether the underlying file exists. - * @see java.io.File#exists() - */ - @Override - public boolean exists() { - return this.file.exists(); - } - - /** - * This implementation checks whether the underlying file is marked as readable - * (and corresponds to an actual file with content, not to a directory). - * @see java.io.File#canRead() - * @see java.io.File#isDirectory() - */ - @Override - public boolean isReadable() { - return (this.file.canRead() && !this.file.isDirectory()); - } - - /** - * This implementation opens a FileInputStream for the underlying file. - * @see java.io.FileInputStream - */ - @Override - public InputStream getInputStream() throws IOException { - return new FileInputStream(this.file); - } - - /** - * This implementation returns a URL for the underlying file. - * @see java.io.File#toURI() - */ - @Override - public URL toURL() throws IOException { - return this.file.toURI().toURL(); - } - - /** - * This implementation returns a URI for the underlying file. - * @see java.io.File#toURI() - */ - @Override - public URI getURI() throws IOException { - return this.file.toURI(); - } - - /** - * This implementation returns the underlying File reference. - */ - @Override - public File toFile() { - return this.file; - } - - /** - * This implementation returns the underlying File's length. - */ - @Override - public long contentLength() throws IOException { - return this.file.length(); - } - - /** - * This implementation creates a FileSystemResource, applying the given path - * relative to the path current the underlying file current this resource descriptor. - * @see StringUtils#applyRelativePath(String, String) - */ - @Override - public Resource createRelative(String relativePath) { - String pathToUse = StringUtils.applyRelativePath(this.path, relativePath); - return new FileSystemResource(pathToUse); - } - - /** - * This implementation returns the name current the file. - * @see java.io.File#getName() - */ - @Override - public String getDisplayName() { - return this.file.getName(); - } - - /** - * This implementation returns a description that includes the absolute - * path current the file. - * @see java.io.File#getAbsolutePath() - */ - @Override - public String toString() { - return "file [" + this.file.getAbsolutePath() + "]"; - } - - - // implementation current WritableResource - - /** - * This implementation checks whether the underlying file is marked as writable - * (and corresponds to an actual file with content, not to a directory). - * @see java.io.File#canWrite() - * @see java.io.File#isDirectory() - */ - public boolean isWritable() { - return (this.file.canWrite() && !this.file.isDirectory()); - } - - /** - * This implementation opens a FileOutputStream for the underlying file. - * @see java.io.FileOutputStream - */ - public OutputStream getOutputStream() throws IOException { - return new FileOutputStream(this.file); - } - - - /** - * This implementation compares the underlying File references. - */ - @Override - public boolean equals(Object obj) { - return (obj == this || - (obj instanceof FileSystemResource && this.path.equals(((FileSystemResource) obj).path))); - } - - /** - * This implementation returns the hash code current the underlying File reference. - */ - @Override - public int hashCode() { - return this.path.hashCode(); - } + private final File file; + + private final String path; + + + /** + * Create a new {@code FileSystemResource} from a {@link File} handle. + * <p>Note: When building relative resources via {@link #createRelative}, + * the relative path will applyChanges <i>at the same directory level</i>: + * e.g. new File("C:/dir1"), relative path "dir2" -> "C:/dir2"! + * If you prefer to have relative paths built underneath the given root + * directory, use the {@link #FileSystemResource(String) constructor with a file path} + * to append a trailing slash to the root path: "C:/dir1/", which + * indicates this directory as root for all relative paths. + * + * @param file a File handle + */ + public FileSystemResource(File file) { + Objects.requireNonNull(file, "File must not be null"); + this.file = file; + this.path = StringUtils.cleanPath(file.getPath()); + } + + /** + * Create a new {@code FileSystemResource} from a file path. + * <p>Note: When building relative resources via {@link #createRelative}, + * it makes a difference whether the specified resource base path here + * ends with a slash or not. In the case current "C:/dir1/", relative paths + * will be built underneath that root: e.g. relative path "dir2" -> + * "C:/dir1/dir2". In the case current "C:/dir1", relative paths will applyChanges + * at the same directory level: relative path "dir2" -> "C:/dir2". + * + * @param path a file path + */ + public FileSystemResource(String path) { + Objects.requireNonNull(path, "Path must not be null"); + this.file = new File(path); + this.path = StringUtils.cleanPath(path); + } + + + /** + * Return the file path for this resource. + */ + public final String getPath() { + return this.path; + } + + + /** + * This implementation returns whether the underlying file exists. + * + * @see java.io.File#exists() + */ + @Override + public boolean exists() { + return this.file.exists(); + } + + /** + * This implementation checks whether the underlying file is marked as readable + * (and corresponds to an actual file with content, not to a directory). + * + * @see java.io.File#canRead() + * @see java.io.File#isDirectory() + */ + @Override + public boolean isReadable() { + return (this.file.canRead() && !this.file.isDirectory()); + } + + /** + * This implementation opens a FileInputStream for the underlying file. + * + * @see java.io.FileInputStream + */ + @Override + public InputStream getInputStream() throws IOException { + return new FileInputStream(this.file); + } + + /** + * This implementation returns a URL for the underlying file. + * + * @see java.io.File#toURI() + */ + @Override + public URL toURL() throws IOException { + return this.file.toURI().toURL(); + } + + /** + * This implementation returns a URI for the underlying file. + * + * @see java.io.File#toURI() + */ + @Override + public URI getURI() throws IOException { + return this.file.toURI(); + } + + /** + * This implementation returns the underlying File reference. + */ + @Override + public File toFile() { + return this.file; + } + + /** + * This implementation returns the underlying File's length. + */ + @Override + public long contentLength() throws IOException { + return this.file.length(); + } + + /** + * This implementation creates a FileSystemResource, applying the given path + * relative to the path current the underlying file current this resource descriptor. + * + * @see StringUtils#applyRelativePath(String, String) + */ + @Override + public Resource createRelative(String relativePath) { + String pathToUse = StringUtils.applyRelativePath(this.path, relativePath); + return new FileSystemResource(pathToUse); + } + + /** + * This implementation returns the name current the file. + * + * @see java.io.File#getName() + */ + @Override + public String getDisplayName() { + return this.file.getName(); + } + + /** + * This implementation returns a description that includes the absolute + * path current the file. + * + * @see java.io.File#getAbsolutePath() + */ + @Override + public String toString() { + return "file [" + this.file.getAbsolutePath() + "]"; + } + + + // implementation current WritableResource + + /** + * This implementation checks whether the underlying file is marked as writable + * (and corresponds to an actual file with content, not to a directory). + * + * @see java.io.File#canWrite() + * @see java.io.File#isDirectory() + */ + public boolean isWritable() { + return (this.file.canWrite() && !this.file.isDirectory()); + } + + /** + * This implementation opens a FileOutputStream for the underlying file. + * + * @see java.io.FileOutputStream + */ + public OutputStream getOutputStream() throws IOException { + return new FileOutputStream(this.file); + } + + + /** + * This implementation compares the underlying File references. + */ + @Override + public boolean equals(Object obj) { + return (obj == this || + (obj instanceof FileSystemResource && this.path.equals(((FileSystemResource) obj).path))); + } + + /** + * This implementation returns the hash code current the underlying File reference. + */ + @Override + public int hashCode() { + return this.path.hashCode(); + } }