diff --git a/neutrino-core/src/main/java/fun/asgc/neutrino/core/base/OrderComparator.java b/neutrino-core/src/main/java/fun/asgc/neutrino/core/base/OrderComparator.java new file mode 100644 index 00000000..e919eaf9 --- /dev/null +++ b/neutrino-core/src/main/java/fun/asgc/neutrino/core/base/OrderComparator.java @@ -0,0 +1,210 @@ +/** + * Copyright (c) 2022 aoshiguchen + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package fun.asgc.neutrino.core.base; + +import fun.asgc.neutrino.core.util.ObjectUtil; +import org.apache.commons.lang3.ObjectUtils; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * + * @author: aoshiguchen + * @date: 2022/7/27 + */ +public class OrderComparator implements Comparator { + + /** + * Shared default instance of {@code OrderComparator}. + */ + public static final OrderComparator INSTANCE = new OrderComparator(); + + + /** + * Build an adapted order comparator with the given source provider. + * @param sourceProvider the order source provider to use + * @return the adapted comparator + * @since 4.1 + */ + public Comparator withSourceProvider(final OrderSourceProvider sourceProvider) { + return new Comparator() { + @Override + public int compare(Object o1, Object o2) { + return doCompare(o1, o2, sourceProvider); + } + }; + } + + @Override + public int compare(Object o1, Object o2) { + return doCompare(o1, o2, null); + } + + private int doCompare(Object o1, Object o2, OrderSourceProvider sourceProvider) { + boolean p1 = (o1 instanceof PriorityOrdered); + boolean p2 = (o2 instanceof PriorityOrdered); + if (p1 && !p2) { + return -1; + } + else if (p2 && !p1) { + return 1; + } + + // Direct evaluation instead of Integer.compareTo to avoid unnecessary object creation. + int i1 = getOrder(o1, sourceProvider); + int i2 = getOrder(o2, sourceProvider); + return (i1 < i2) ? -1 : (i1 > i2) ? 1 : 0; + } + + /** + * Determine the order value for the given object. + *

The default implementation checks against the given {@link OrderSourceProvider} + * using {@link #findOrder} and falls back to a regular {@link #getOrder(Object)} call. + * @param obj the object to check + * @return the order value, or {@code Ordered.LOWEST_PRECEDENCE} as fallback + */ + private int getOrder(Object obj, OrderSourceProvider sourceProvider) { + Integer order = null; + if (sourceProvider != null) { + Object orderSource = sourceProvider.getOrderSource(obj); + if (orderSource != null && orderSource.getClass().isArray()) { + Object[] sources = ObjectUtil.toObjectArray(orderSource); + for (Object source : sources) { + order = findOrder(source); + if (order != null) { + break; + } + } + } + else { + order = findOrder(orderSource); + } + } + return (order != null ? order : getOrder(obj)); + } + + /** + * Determine the order value for the given object. + *

The default implementation checks against the {@link Ordered} interface + * through delegating to {@link #findOrder}. Can be overridden in subclasses. + * @param obj the object to check + * @return the order value, or {@code Ordered.LOWEST_PRECEDENCE} as fallback + */ + protected int getOrder(Object obj) { + Integer order = findOrder(obj); + return (order != null ? order : Ordered.LOWEST_PRECEDENCE); + } + + /** + * Find an order value indicated by the given object. + *

The default implementation checks against the {@link Ordered} interface. + * Can be overridden in subclasses. + * @param obj the object to check + * @return the order value, or {@code null} if none found + */ + protected Integer findOrder(Object obj) { + return (obj instanceof Ordered ? ((Ordered) obj).getOrder() : null); + } + + /** + * Determine a priority value for the given object, if any. + *

The default implementation always returns {@code null}. + * Subclasses may override this to give specific kinds of values a + * 'priority' characteristic, in addition to their 'order' semantics. + * A priority indicates that it may be used for selecting one object over + * another, in addition to serving for ordering purposes in a list/array. + * @param obj the object to check + * @return the priority value, or {@code null} if none + * @since 4.1 + */ + public Integer getPriority(Object obj) { + return null; + } + + + /** + * Sort the given List with a default OrderComparator. + *

Optimized to skip sorting for lists with size 0 or 1, + * in order to avoid unnecessary array extraction. + * @param list the List to sort + * @see java.util.Collections#sort(java.util.List, java.util.Comparator) + */ + public static void sort(List list) { + if (list.size() > 1) { + Collections.sort(list, INSTANCE); + } + } + + /** + * Sort the given array with a default OrderComparator. + *

Optimized to skip sorting for lists with size 0 or 1, + * in order to avoid unnecessary array extraction. + * @param array the array to sort + * @see java.util.Arrays#sort(Object[], java.util.Comparator) + */ + public static void sort(Object[] array) { + if (array.length > 1) { + Arrays.sort(array, INSTANCE); + } + } + + /** + * Sort the given array or List with a default OrderComparator, + * if necessary. Simply skips sorting when given any other value. + *

Optimized to skip sorting for lists with size 0 or 1, + * in order to avoid unnecessary array extraction. + * @param value the array or List to sort + * @see java.util.Arrays#sort(Object[], java.util.Comparator) + */ + public static void sortIfNecessary(Object value) { + if (value instanceof Object[]) { + sort((Object[]) value); + } + else if (value instanceof List) { + sort((List) value); + } + } + + + /** + * Strategy interface to provide an order source for a given object. + * @since 4.1 + */ + public interface OrderSourceProvider { + + /** + * Return an order source for the specified object, i.e. an object that + * should be checked for an order value as a replacement to the given object. + *

Can also be an array of order source objects. + *

If the returned object does not indicate any order, the comparator + * will fall back to checking the original object. + * @param obj the object to find an order source for + * @return the order source for that object, or {@code null} if none found + */ + Object getOrderSource(Object obj); + } + +} + diff --git a/neutrino-core/src/main/java/fun/asgc/neutrino/core/base/Ordered.java b/neutrino-core/src/main/java/fun/asgc/neutrino/core/base/Ordered.java new file mode 100644 index 00000000..eaaec279 --- /dev/null +++ b/neutrino-core/src/main/java/fun/asgc/neutrino/core/base/Ordered.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2022 aoshiguchen + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package fun.asgc.neutrino.core.base; + +/** + * + * @author: aoshiguchen + * @date: 2022/7/27 + */ +public interface Ordered { + /** + * Useful constant for the highest precedence value. + * @see java.lang.Integer#MIN_VALUE + */ + int HIGHEST_PRECEDENCE = Integer.MIN_VALUE; + + /** + * Useful constant for the lowest precedence value. + * @see java.lang.Integer#MAX_VALUE + */ + int LOWEST_PRECEDENCE = Integer.MAX_VALUE; + + + /** + * Get the order value of this object. + *

Higher values are interpreted as lower priority. As a consequence, + * the object with the lowest value has the highest priority (somewhat + * analogous to Servlet {@code load-on-startup} values). + *

Same order values will result in arbitrary sort positions for the + * affected objects. + * @return the order value + * @see #HIGHEST_PRECEDENCE + * @see #LOWEST_PRECEDENCE + */ + int getOrder(); +} diff --git a/neutrino-core/src/main/java/fun/asgc/neutrino/core/base/PriorityOrdered.java b/neutrino-core/src/main/java/fun/asgc/neutrino/core/base/PriorityOrdered.java new file mode 100644 index 00000000..dd853338 --- /dev/null +++ b/neutrino-core/src/main/java/fun/asgc/neutrino/core/base/PriorityOrdered.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2022 aoshiguchen + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package fun.asgc.neutrino.core.base; + +/** + * + * @author: aoshiguchen + * @date: 2022/7/27 + */ +public interface PriorityOrdered extends Ordered { + +} diff --git a/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/AntPathMatcher.java b/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/AntPathMatcher.java new file mode 100644 index 00000000..b57ccc8d --- /dev/null +++ b/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/AntPathMatcher.java @@ -0,0 +1,874 @@ +/** + * Copyright (c) 2022 aoshiguchen + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package fun.asgc.neutrino.core.web; + +import fun.asgc.neutrino.core.util.Assert; +import fun.asgc.neutrino.core.util.StringUtil; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * + * @author: aoshiguchen + * @date: 2022/7/27 + */ +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 tokenizedPatternCache = new ConcurrentHashMap(256); + + final Map stringMatcherCache = new ConcurrentHashMap(256); + + + /** + * Create a new instance with the {@link #DEFAULT_PATH_SEPARATOR}. + */ + public AntPathMatcher() { + this.pathSeparator = DEFAULT_PATH_SEPARATOR; + this.pathSeparatorPatternCache = new PathSeparatorPatternCache(DEFAULT_PATH_SEPARATOR); + } + + /** + * A convenient, alternative constructor to use with a custom path separator. + * @param pathSeparator the path separator to use, must not be {@code null}. + * @since 4.1 + */ + public AntPathMatcher(String pathSeparator) { + Assert.notNull(pathSeparator, "'pathSeparator' is required"); + this.pathSeparator = pathSeparator; + this.pathSeparatorPatternCache = new PathSeparatorPatternCache(pathSeparator); + } + + + /** + * Set the path separator to use for pattern parsing. + *

Default is "/", as in Ant. + */ + public void setPathSeparator(String pathSeparator) { + this.pathSeparator = (pathSeparator != null ? pathSeparator : DEFAULT_PATH_SEPARATOR); + this.pathSeparatorPatternCache = new PathSeparatorPatternCache(this.pathSeparator); + } + + /** + * Specify whether to perform pattern matching in a case-sensitive fashion. + *

Default is {@code true}. Switch this to {@code false} for case-insensitive matching. + * @since 4.2 + */ + public void setCaseSensitive(boolean caseSensitive) { + this.caseSensitive = caseSensitive; + } + + /** + * Specify whether to trim tokenized paths and patterns. + *

Default is {@code false}. + */ + public void setTrimTokens(boolean trimTokens) { + this.trimTokens = trimTokens; + } + + /** + * Specify whether to cache parsed pattern metadata for patterns passed + * into this matcher's {@link #match} method. A value of {@code true} + * activates an unlimited pattern cache; a value of {@code false} turns + * the pattern cache off completely. + *

Default is for the cache to be on, but with the variant to automatically + * turn it off when encountering too many patterns to cache at runtime + * (the threshold is 65536), assuming that arbitrary permutations of patterns + * are coming in, with little chance for encountering a recurring pattern. + * @since 4.0.1 + * @see #getStringMatcher(String) + */ + public void setCachePatterns(boolean cachePatterns) { + this.cachePatterns = cachePatterns; + } + + private void deactivatePatternCache() { + this.cachePatterns = false; + this.tokenizedPatternCache.clear(); + this.stringMatcherCache.clear(); + } + + + @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 uriTemplateVariables) { + if (path.startsWith(this.pathSeparator) != pattern.startsWith(this.pathSeparator)) { + return false; + } + + String[] pattDirs = tokenizePattern(pattern); + if (fullMatch && this.caseSensitive && !isPotentialMatch(path, pattDirs)) { + return false; + } + + String[] pathDirs = tokenizePath(path); + + int pattIdxStart = 0; + int pattIdxEnd = pattDirs.length - 1; + int pathIdxStart = 0; + int pathIdxEnd = pathDirs.length - 1; + + // Match all elements up to the first ** + while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) { + String pattDir = pattDirs[pattIdxStart]; + if ("**".equals(pattDir)) { + break; + } + if (!matchStrings(pattDir, pathDirs[pathIdxStart], uriTemplateVariables)) { + return false; + } + pattIdxStart++; + pathIdxStart++; + } + + if (pathIdxStart > pathIdxEnd) { + // Path is exhausted, only match if rest of pattern is * or **'s + if (pattIdxStart > pattIdxEnd) { + return (pattern.endsWith(this.pathSeparator) == path.endsWith(this.pathSeparator)); + } + if (!fullMatch) { + return true; + } + if (pattIdxStart == pattIdxEnd && pattDirs[pattIdxStart].equals("*") && path.endsWith(this.pathSeparator)) { + return true; + } + for (int i = pattIdxStart; i <= pattIdxEnd; i++) { + if (!pattDirs[i].equals("**")) { + return false; + } + } + return true; + } + else if (pattIdxStart > pattIdxEnd) { + // String not exhausted, but pattern is. Failure. + return false; + } + else if (!fullMatch && "**".equals(pattDirs[pattIdxStart])) { + // Path start definitely matches due to "**" part in pattern. + return true; + } + + // up to last '**' + while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) { + String pattDir = pattDirs[pattIdxEnd]; + if (pattDir.equals("**")) { + break; + } + if (!matchStrings(pattDir, pathDirs[pathIdxEnd], uriTemplateVariables)) { + return false; + } + pattIdxEnd--; + pathIdxEnd--; + } + if (pathIdxStart > pathIdxEnd) { + // String is exhausted + for (int i = pattIdxStart; i <= pattIdxEnd; i++) { + if (!pattDirs[i].equals("**")) { + return false; + } + } + return true; + } + + while (pattIdxStart != pattIdxEnd && pathIdxStart <= pathIdxEnd) { + int patIdxTmp = -1; + for (int i = pattIdxStart + 1; i <= pattIdxEnd; i++) { + if (pattDirs[i].equals("**")) { + patIdxTmp = i; + break; + } + } + if (patIdxTmp == pattIdxStart + 1) { + // '**/**' situation, so skip one + pattIdxStart++; + continue; + } + // Find the pattern between padIdxStart & padIdxTmp in str between + // strIdxStart & strIdxEnd + int patLength = (patIdxTmp - pattIdxStart - 1); + int strLength = (pathIdxEnd - pathIdxStart + 1); + int foundIdx = -1; + + strLoop: + for (int i = 0; i <= strLength - patLength; i++) { + for (int j = 0; j < patLength; j++) { + String subPat = pattDirs[pattIdxStart + j + 1]; + String subStr = pathDirs[pathIdxStart + i + j]; + if (!matchStrings(subPat, subStr, uriTemplateVariables)) { + continue strLoop; + } + } + foundIdx = pathIdxStart + i; + break; + } + + if (foundIdx == -1) { + return false; + } + + pattIdxStart = patIdxTmp; + pathIdxStart = foundIdx + patLength; + } + + for (int i = pattIdxStart; i <= pattIdxEnd; i++) { + if (!pattDirs[i].equals("**")) { + return false; + } + } + + return true; + } + + private boolean isPotentialMatch(String path, String[] pattDirs) { + if (!this.trimTokens) { + int pos = 0; + for (String pattDir : pattDirs) { + int skipped = skipSeparator(path, pos, this.pathSeparator); + pos += skipped; + skipped = skipSegment(path, pos, pattDir); + if (skipped < pattDir.length()) { + return (skipped > 0 || (pattDir.length() > 0 && isWildcardChar(pattDir.charAt(0)))); + } + pos += skipped; + } + } + return true; + } + + private int skipSegment(String path, int pos, String prefix) { + int skipped = 0; + for (int i = 0; i < prefix.length(); i++) { + char c = prefix.charAt(i); + if (isWildcardChar(c)) { + return skipped; + } + int currPos = pos + skipped; + if (currPos >= path.length()) { + return 0; + } + if (c == path.charAt(currPos)) { + skipped++; + } + } + return skipped; + } + + private int skipSeparator(String path, int pos, String separator) { + int skipped = 0; + while (path.startsWith(separator, pos + skipped)) { + skipped += separator.length(); + } + return skipped; + } + + private boolean isWildcardChar(char c) { + for (char candidate : WILDCARD_CHARS) { + if (c == candidate) { + return true; + } + } + return false; + } + + /** + * Tokenize the given path pattern into parts, based on this matcher's settings. + *

Performs caching based on {@link #setCachePatterns}, delegating to + * {@link #tokenizePath(String)} for the actual tokenization algorithm. + * @param pattern the pattern to tokenize + * @return the tokenized pattern parts + */ + protected String[] tokenizePattern(String pattern) { + String[] tokenized = null; + Boolean cachePatterns = this.cachePatterns; + if (cachePatterns == null || cachePatterns.booleanValue()) { + tokenized = this.tokenizedPatternCache.get(pattern); + } + if (tokenized == null) { + tokenized = tokenizePath(pattern); + if (cachePatterns == null && this.tokenizedPatternCache.size() >= CACHE_TURNOFF_THRESHOLD) { + // Try to adapt to the runtime situation that we're encountering: + // There are obviously too many different patterns coming in here... + // So let's turn off the cache since the patterns are unlikely to be reoccurring. + deactivatePatternCache(); + return tokenized; + } + if (cachePatterns == null || cachePatterns.booleanValue()) { + this.tokenizedPatternCache.put(pattern, tokenized); + } + } + return tokenized; + } + + /** + * Tokenize the given path String into parts, based on this matcher's settings. + * @param path the path to tokenize + * @return the tokenized path parts + */ + protected String[] tokenizePath(String path) { + return StringUtil.tokenizeToStringArray(path, this.pathSeparator, this.trimTokens, true); + } + + /** + * Test whether or not a string matches against a pattern. + * @param pattern the pattern to match against (never {@code null}) + * @param str the String which must be matched against the pattern (never {@code null}) + * @return {@code true} if the string matches against the pattern, or {@code false} otherwise + */ + private boolean matchStrings(String pattern, String str, Map uriTemplateVariables) { + return getStringMatcher(pattern).matchStrings(str, uriTemplateVariables); + } + + /** + * Build or retrieve an {@link AntPathStringMatcher} for the given pattern. + *

The default implementation checks this AntPathMatcher's internal cache + * (see {@link #setCachePatterns}), creating a new AntPathStringMatcher instance + * if no cached copy is found. + *

When encountering too many patterns to cache at runtime (the threshold is 65536), + * it turns the default cache off, assuming that arbitrary permutations of patterns + * are coming in, with little chance for encountering a recurring pattern. + *

This method may be overridden to implement a custom cache strategy. + * @param pattern the pattern to match against (never {@code null}) + * @return a corresponding AntPathStringMatcher (never {@code null}) + * @see #setCachePatterns + */ + protected AntPathStringMatcher getStringMatcher(String pattern) { + AntPathStringMatcher matcher = null; + Boolean cachePatterns = this.cachePatterns; + if (cachePatterns == null || cachePatterns.booleanValue()) { + matcher = this.stringMatcherCache.get(pattern); + } + if (matcher == null) { + matcher = new AntPathStringMatcher(pattern, this.caseSensitive); + if (cachePatterns == null && this.stringMatcherCache.size() >= CACHE_TURNOFF_THRESHOLD) { + // Try to adapt to the runtime situation that we're encountering: + // There are obviously too many different patterns coming in here... + // So let's turn off the cache since the patterns are unlikely to be reoccurring. + deactivatePatternCache(); + return matcher; + } + if (cachePatterns == null || cachePatterns.booleanValue()) { + this.stringMatcherCache.put(pattern, matcher); + } + } + return matcher; + } + + /** + * Given a pattern and a full path, determine the pattern-mapped part.

For example:

    + *
  • '{@code /docs/cvs/commit.html}' and '{@code /docs/cvs/commit.html} -> ''
  • + *
  • '{@code /docs/*}' and '{@code /docs/cvs/commit} -> '{@code cvs/commit}'
  • + *
  • '{@code /docs/cvs/*.html}' and '{@code /docs/cvs/commit.html} -> '{@code commit.html}'
  • + *
  • '{@code /docs/**}' and '{@code /docs/cvs/commit} -> '{@code cvs/commit}'
  • + *
  • '{@code /docs/**\/*.html}' and '{@code /docs/cvs/commit.html} -> '{@code cvs/commit.html}'
  • + *
  • '{@code /*.html}' and '{@code /docs/cvs/commit.html} -> '{@code docs/cvs/commit.html}'
  • + *
  • '{@code *.html}' and '{@code /docs/cvs/commit.html} -> '{@code /docs/cvs/commit.html}'
  • + *
  • '{@code *}' and '{@code /docs/cvs/commit.html} -> '{@code /docs/cvs/commit.html}'
+ *

Assumes that {@link #match} returns {@code true} for '{@code pattern}' and '{@code path}', but + * does not enforce this. + */ + @Override + public String extractPathWithinPattern(String pattern, String path) { + String[] patternParts = StringUtil.tokenizeToStringArray(pattern, this.pathSeparator, this.trimTokens, true); + String[] pathParts = StringUtil.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 extractUriTemplateVariables(String pattern, String path) { + Map variables = new LinkedHashMap(); + boolean result = doMatch(pattern, path, true, variables); + if (!result) { + throw new IllegalStateException("Pattern \"" + pattern + "\" is not a match for \"" + path + "\""); + } + return variables; + } + + /** + * Combine two patterns into a new pattern. + *

This implementation simply concatenates the two patterns, unless + * the first pattern contains a file extension match (e.g., {@code *.html}). + * In that case, the second pattern will be merged into the first. Otherwise, + * an {@code IllegalArgumentException} will be thrown. + *

Examples

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Pattern 1Pattern 2Result
{@code null}{@code null} 
/hotels{@code null}/hotels
{@code null}/hotels/hotels
/hotels/bookings/hotels/bookings
/hotelsbookings/hotels/bookings
/hotels/*/bookings/hotels/bookings
/hotels/**/bookings/hotels/**/bookings
/hotels{hotel}/hotels/{hotel}
/hotels/*{hotel}/hotels/{hotel}
/hotels/**{hotel}/hotels/**/{hotel}
/*.html/hotels.html/hotels.html
/*.html/hotels/hotels.html
/*.html/*.txt{@code IllegalArgumentException}
+ * @param pattern1 the first pattern + * @param pattern2 the second pattern + * @return the combination of the two patterns + * @throws IllegalArgumentException if the two patterns cannot be combined + */ + @Override + public String combine(String pattern1, String pattern2) { + if (!StringUtil.hasText(pattern1) && !StringUtil.hasText(pattern2)) { + return ""; + } + if (!StringUtil.hasText(pattern1)) { + return pattern2; + } + if (!StringUtil.hasText(pattern2)) { + return pattern1; + } + + boolean pattern1ContainsUriVar = (pattern1.indexOf('{') != -1); + if (!pattern1.equals(pattern2) && !pattern1ContainsUriVar && match(pattern1, pattern2)) { + // /* + /hotel -> /hotel ; "/*.*" + "/*.html" -> /*.html + // However /user + /user -> /usr/user ; /{foo} + /bar -> /{foo}/bar + return pattern2; + } + + // /hotels/* + /booking -> /hotels/booking + // /hotels/* + booking -> /hotels/booking + if (pattern1.endsWith(this.pathSeparatorPatternCache.getEndsOnWildCard())) { + return concat(pattern1.substring(0, pattern1.length() - 2), pattern2); + } + + // /hotels/** + /booking -> /hotels/**/booking + // /hotels/** + booking -> /hotels/**/booking + if (pattern1.endsWith(this.pathSeparatorPatternCache.getEndsOnDoubleWildCard())) { + return concat(pattern1, pattern2); + } + + int starDotPos1 = pattern1.indexOf("*."); + if (pattern1ContainsUriVar || starDotPos1 == -1 || this.pathSeparator.equals(".")) { + // simply concatenate the two patterns + return concat(pattern1, pattern2); + } + + String ext1 = pattern1.substring(starDotPos1 + 1); + int dotPos2 = pattern2.indexOf('.'); + String file2 = (dotPos2 == -1 ? pattern2 : pattern2.substring(0, dotPos2)); + String ext2 = (dotPos2 == -1 ? "" : pattern2.substring(dotPos2)); + boolean ext1All = (ext1.equals(".*") || ext1.equals("")); + boolean ext2All = (ext2.equals(".*") || ext2.equals("")); + if (!ext1All && !ext2All) { + throw new IllegalArgumentException("Cannot combine patterns: " + pattern1 + " vs " + pattern2); + } + String ext = (ext1All ? ext2 : ext1); + return file2 + ext; + } + + private String concat(String path1, String path2) { + boolean path1EndsWithSeparator = path1.endsWith(this.pathSeparator); + boolean path2StartsWithSeparator = path2.startsWith(this.pathSeparator); + + if (path1EndsWithSeparator && path2StartsWithSeparator) { + return path1 + path2.substring(1); + } + else if (path1EndsWithSeparator || path2StartsWithSeparator) { + return path1 + path2; + } + else { + return path1 + this.pathSeparator + path2; + } + } + + /** + * Given a full path, returns a {@link Comparator} suitable for sorting patterns in order of + * explicitness. + *

This{@code Comparator} will {@linkplain java.util.Collections#sort(List, 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: + *

    + *
  1. {@code /hotels/new}
  2. + *
  3. {@code /hotels/{hotel}}
  4. {@code /hotels/*}
  5. + *
+ * the returned comparator will sort this list so that the order will be as indicated. + *

The full path given as parameter is used to test for exact matches. So when the given path + * is {@code /hotels/2}, the pattern {@code /hotels/2} will be sorted before {@code /hotels/1}. + * @param path the full path to use for comparison + * @return a comparator capable of sorting patterns in order of explicitness + */ + @Override + public Comparator getPatternComparator(String path) { + return new AntPatternComparator(path); + } + + + /** + * Tests whether or not a string matches against a pattern via a {@link Pattern}. + *

The pattern may contain special characters: '*' means zero or more characters; '?' means one and + * only one character; '{' and '}' indicate a URI template pattern. For example /users/{user}. + */ + protected static class AntPathStringMatcher { + + private static final Pattern GLOB_PATTERN = Pattern.compile("\\?|\\*|\\{((?:\\{[^/]+?\\}|[^/{}]|\\\\[{}])+?)\\}"); + + private static final String DEFAULT_VARIABLE_PATTERN = "(.*)"; + + private final Pattern pattern; + + private final List variableNames = new LinkedList(); + + public AntPathStringMatcher(String pattern) { + this(pattern, true); + } + + public AntPathStringMatcher(String pattern, boolean caseSensitive) { + StringBuilder patternBuilder = new StringBuilder(); + Matcher matcher = GLOB_PATTERN.matcher(pattern); + int end = 0; + while (matcher.find()) { + patternBuilder.append(quote(pattern, end, matcher.start())); + String match = matcher.group(); + if ("?".equals(match)) { + patternBuilder.append('.'); + } + else if ("*".equals(match)) { + patternBuilder.append(".*"); + } + else if (match.startsWith("{") && match.endsWith("}")) { + int colonIdx = match.indexOf(':'); + if (colonIdx == -1) { + patternBuilder.append(DEFAULT_VARIABLE_PATTERN); + this.variableNames.add(matcher.group(1)); + } + else { + String variablePattern = match.substring(colonIdx + 1, match.length() - 1); + patternBuilder.append('('); + patternBuilder.append(variablePattern); + patternBuilder.append(')'); + String variableName = match.substring(1, colonIdx); + this.variableNames.add(variableName); + } + } + end = matcher.end(); + } + patternBuilder.append(quote(pattern, end, pattern.length())); + this.pattern = (caseSensitive ? Pattern.compile(patternBuilder.toString()) : + Pattern.compile(patternBuilder.toString(), Pattern.CASE_INSENSITIVE)); + } + + private String quote(String s, int start, int end) { + if (start == end) { + return ""; + } + return Pattern.quote(s.substring(start, end)); + } + + /** + * Main entry point. + * @return {@code true} if the string matches against the pattern, or {@code false} otherwise. + */ + public boolean matchStrings(String str, Map uriTemplateVariables) { + Matcher matcher = this.pattern.matcher(str); + if (matcher.matches()) { + if (uriTemplateVariables != null) { + // SPR-8455 + if (this.variableNames.size() != matcher.groupCount()) { + throw new IllegalArgumentException("The number of capturing groups in the pattern segment " + + this.pattern + " does not match the number of URI template variables it defines, " + + "which can occur if capturing groups are used in a URI template regex. " + + "Use non-capturing groups instead."); + } + for (int i = 1; i <= matcher.groupCount(); i++) { + String name = this.variableNames.get(i - 1); + String value = matcher.group(i); + uriTemplateVariables.put(name, value); + } + } + return true; + } + else { + return false; + } + } + } + + + /** + * The default {@link Comparator} implementation returned by + * {@link #getPatternComparator(String)}. + *

In order, the most "generic" pattern is determined by the following: + *

    + *
  • if it's null or a capture all pattern (i.e. it is equal to "/**")
  • + *
  • if the other pattern is an actual match
  • + *
  • if it's a catch-all pattern (i.e. it ends with "**"
  • + *
  • if it's got more "*" than the other pattern
  • + *
  • if it's got more "{foo}" than the other pattern
  • + *
  • if it's shorter than the other pattern
  • + *
+ */ + protected static class AntPatternComparator implements Comparator { + + private final String path; + + public AntPatternComparator(String path) { + this.path = path; + } + + /** + * Compare two patterns to determine which should match first, i.e. which + * is the most specific regarding the current path. + * @return a negative integer, zero, or a positive integer as pattern1 is + * more specific, equally specific, or less specific than pattern2. + */ + @Override + public int compare(String pattern1, String pattern2) { + PatternInfo info1 = new PatternInfo(pattern1); + PatternInfo info2 = new PatternInfo(pattern2); + + if (info1.isLeastSpecific() && info2.isLeastSpecific()) { + return 0; + } + else if (info1.isLeastSpecific()) { + return 1; + } + else if (info2.isLeastSpecific()) { + return -1; + } + + boolean pattern1EqualsPath = pattern1.equals(path); + boolean pattern2EqualsPath = pattern2.equals(path); + if (pattern1EqualsPath && pattern2EqualsPath) { + return 0; + } + else if (pattern1EqualsPath) { + return -1; + } + else if (pattern2EqualsPath) { + return 1; + } + + if (info1.isPrefixPattern() && info2.getDoubleWildcards() == 0) { + return 1; + } + else if (info2.isPrefixPattern() && info1.getDoubleWildcards() == 0) { + return -1; + } + + if (info1.getTotalCount() != info2.getTotalCount()) { + return info1.getTotalCount() - info2.getTotalCount(); + } + + if (info1.getLength() != info2.getLength()) { + return info2.getLength() - info1.getLength(); + } + + if (info1.getSingleWildcards() < info2.getSingleWildcards()) { + return -1; + } + else if (info2.getSingleWildcards() < info1.getSingleWildcards()) { + return 1; + } + + if (info1.getUriVars() < info2.getUriVars()) { + return -1; + } + else if (info2.getUriVars() < info1.getUriVars()) { + return 1; + } + + return 0; + } + + + /** + * Value class that holds information about the pattern, e.g. number of + * occurrences of "*", "**", and "{" pattern elements. + */ + private static class PatternInfo { + + private final String pattern; + + private int uriVars; + + private int singleWildcards; + + private int doubleWildcards; + + private boolean catchAllPattern; + + private boolean prefixPattern; + + private Integer length; + + public PatternInfo(String pattern) { + this.pattern = pattern; + if (this.pattern != null) { + initCounters(); + this.catchAllPattern = this.pattern.equals("/**"); + this.prefixPattern = !this.catchAllPattern && this.pattern.endsWith("/**"); + } + if (this.uriVars == 0) { + this.length = (this.pattern != null ? this.pattern.length() : 0); + } + } + + protected void initCounters() { + int pos = 0; + while (pos < this.pattern.length()) { + if (this.pattern.charAt(pos) == '{') { + this.uriVars++; + pos++; + } + else if (this.pattern.charAt(pos) == '*') { + if (pos + 1 < this.pattern.length() && this.pattern.charAt(pos + 1) == '*') { + this.doubleWildcards++; + pos += 2; + } + else if (pos > 0 && !this.pattern.substring(pos - 1).equals(".*")) { + this.singleWildcards++; + pos++; + } + else { + pos++; + } + } + else { + pos++; + } + } + } + + public int getUriVars() { + return this.uriVars; + } + + public int getSingleWildcards() { + return this.singleWildcards; + } + + public int getDoubleWildcards() { + return this.doubleWildcards; + } + + public boolean isLeastSpecific() { + return (this.pattern == null || this.catchAllPattern); + } + + public boolean isPrefixPattern() { + return this.prefixPattern; + } + + public int getTotalCount() { + return this.uriVars + this.singleWildcards + (2 * this.doubleWildcards); + } + + /** + * Returns the length of the given pattern, where template variables are considered to be 1 long. + */ + public int getLength() { + if (this.length == null) { + this.length = 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; + } + } +} diff --git a/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/HttpRequestHandler.java b/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/HttpRequestHandler.java index 1de9c6b6..169100dc 100644 --- a/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/HttpRequestHandler.java +++ b/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/HttpRequestHandler.java @@ -32,6 +32,11 @@ import fun.asgc.neutrino.core.web.annotation.RequestBody; import fun.asgc.neutrino.core.web.annotation.RequestParam; import fun.asgc.neutrino.core.web.context.HttpContextHolder; import fun.asgc.neutrino.core.web.context.HttpRequestParser; +import fun.asgc.neutrino.core.web.context.WebContextHolder; +import fun.asgc.neutrino.core.web.interceptor.HandlerInterceptor; +import fun.asgc.neutrino.core.web.interceptor.InterceptorRegistration; +import fun.asgc.neutrino.core.web.interceptor.InterceptorRegistry; +import fun.asgc.neutrino.core.web.interceptor.MappedInterceptor; import fun.asgc.neutrino.core.web.router.DefaultHttpRouter; import fun.asgc.neutrino.core.web.router.HttpRouteParam; import fun.asgc.neutrino.core.web.router.HttpRouteResult; @@ -46,7 +51,9 @@ import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Parameter; +import java.util.ArrayList; import java.util.Date; +import java.util.List; import java.util.Set; /** @@ -62,6 +69,7 @@ public class HttpRequestHandler { private ApplicationConfig applicationConfig; @Autowired private DefaultHttpRouter defaultHttpRouter; + private PathMatcher pathMatcher = new AntPathMatcher(); public void handle() { ChannelHandlerContext context = HttpContextHolder.getChannelHandlerContext(); @@ -77,6 +85,10 @@ public class HttpRequestHandler { return; } if (HttpRouterType.METHOD == httpRouteResult.getType()) { + if (!preHandle(context, requestParser, httpRouteResult.getPageRoute(), httpRouteResult.getMethod())) { + return; + } + Object invokeResult = invoke(httpRouteResult.getInstance(), httpRouteResult.getMethod()); String res = String.valueOf(invokeResult); if (null != invokeResult && !TypeUtil.isNormalBasicType(invokeResult.getClass())) { @@ -87,6 +99,10 @@ public class HttpRequestHandler { context.writeAndFlush(fullHttpResponse).addListener(ChannelFutureListener.CLOSE); return; } else if(HttpRouterType.PAGE == httpRouteResult.getType()) { + if (!preHandle(context, requestParser, httpRouteResult.getPageRoute(), null)) { + return; + } + // 前端页面 String mimeType = MimeType.getMimeType(MimeType.parseSuffix(httpRouteResult.getPageLocation())); if (mimeType.startsWith("text/")) { @@ -110,6 +126,37 @@ public class HttpRequestHandler { } } + private boolean preHandle(ChannelHandlerContext context, HttpRequestParser requestParser, String route, Method targetMethod) throws Exception { + List list = getInterceptorsForPath(route); + if (!CollectionUtil.isEmpty(list)) { + for (HandlerInterceptor handlerInterceptor : list) { + if (!handlerInterceptor.preHandle(context, requestParser, route, targetMethod)) { + return Boolean.FALSE; + } + } + } + return Boolean.TRUE; + } + + private List getInterceptorsForPath(String lookupPath) { + List result = new ArrayList(); + InterceptorRegistry interceptorRegistry = WebContextHolder.getInterceptorRegistry(); + if (CollectionUtil.isEmpty(interceptorRegistry.getInterceptors())) { + return result; + } + for (Object interceptor : interceptorRegistry.getInterceptors()) { + if (interceptor instanceof HandlerInterceptor) { + result.add((HandlerInterceptor) interceptor); + } else if(interceptor instanceof MappedInterceptor) { + MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor; + if (mappedInterceptor.matches(lookupPath, pathMatcher)) { + result.add(mappedInterceptor.getInterceptor()); + } + } + } + return result; + } + private void exceptionHandler(Throwable e) { log.error("Http处理异常", e); HttpServerUtil.send500Response(HttpContextHolder.getChannelHandlerContext(), e); diff --git a/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/PathMatcher.java b/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/PathMatcher.java new file mode 100644 index 00000000..42e279c5 --- /dev/null +++ b/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/PathMatcher.java @@ -0,0 +1,120 @@ +/** + * Copyright (c) 2022 aoshiguchen + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package fun.asgc.neutrino.core.web; + +import java.util.Comparator; +import java.util.Map; + +/** + * + * @author: aoshiguchen + * @date: 2022/7/27 + */ +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 extractUriTemplateVariables(String pattern, String path); + + /** + * Given a full path, returns a {@link Comparator} suitable for sorting patterns + * in order of explicitness for that path. + *

The full algorithm used depends on the underlying implementation, but generally, + * the returned {@code Comparator} will + * {@linkplain java.util.Collections#sort(java.util.List, 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 getPatternComparator(String path); + + /** + * Combines two patterns into a new pattern that is returned. + *

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/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/config/WebMvcConfigurer.java b/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/config/WebMvcConfigurer.java new file mode 100644 index 00000000..b7cf5955 --- /dev/null +++ b/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/config/WebMvcConfigurer.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2022 aoshiguchen + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package fun.asgc.neutrino.core.web.config; + +import fun.asgc.neutrino.core.web.interceptor.InterceptorRegistry; + +/** + * + * @author: aoshiguchen + * @date: 2022/7/27 + */ +public interface WebMvcConfigurer { + + default void addInterceptors(InterceptorRegistry registry) { + + } +} diff --git a/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/context/HttpContextHolder.java b/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/context/HttpContextHolder.java index 71e8114f..be4ccc82 100644 --- a/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/context/HttpContextHolder.java +++ b/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/context/HttpContextHolder.java @@ -21,6 +21,7 @@ */ package fun.asgc.neutrino.core.web.context; +import fun.asgc.neutrino.core.web.interceptor.InterceptorRegistry; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.FullHttpRequest; diff --git a/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/context/HttpRequestParser.java b/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/context/HttpRequestParser.java index 23d53519..b5b397d6 100644 --- a/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/context/HttpRequestParser.java +++ b/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/context/HttpRequestParser.java @@ -172,6 +172,10 @@ public class HttpRequestParser { return null; } + public FullHttpRequest getRequest() { + return request; + } + public String getHeaderValue(String name) { return request.headers().get(name); } @@ -203,6 +207,9 @@ public class HttpRequestParser { private String getRoutePath(String url) { String httpContextPath = WebContextHolder.getHttpContextPath(); + if (null == url || url.length() < httpContextPath.length()) { + return url; + } if (StringUtil.isEmpty(httpContextPath)) { return url; } diff --git a/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/context/WebContextHolder.java b/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/context/WebContextHolder.java index 5b59018a..cd0d436e 100644 --- a/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/context/WebContextHolder.java +++ b/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/context/WebContextHolder.java @@ -30,6 +30,8 @@ import fun.asgc.neutrino.core.bean.SimpleBeanFactory; import fun.asgc.neutrino.core.context.ApplicationConfig; import fun.asgc.neutrino.core.util.*; import fun.asgc.neutrino.core.web.annotation.RestController; +import fun.asgc.neutrino.core.web.config.WebMvcConfigurer; +import fun.asgc.neutrino.core.web.interceptor.InterceptorRegistry; import java.util.List; import java.util.stream.Collectors; @@ -48,6 +50,7 @@ public class WebContextHolder { private static volatile Long maxContentLength; private static volatile Integer port; private static List controllerBeanWrapperList; + private static volatile InterceptorRegistry interceptorRegistry; @Autowired private ApplicationConfig applicationConfig; @Autowired @@ -60,6 +63,7 @@ public class WebContextHolder { initFavicon(); initMaxContentLength(); initControllerBeanWrapperList(); + initInterceptorRegistry(); port = http.getPort(); } @@ -120,7 +124,24 @@ public class WebContextHolder { .filter(beanWrapper -> beanWrapper.getType().isAnnotationPresent(RestController.class)).collect(Collectors.toList()); } + private void initInterceptorRegistry() { + interceptorRegistry = new InterceptorRegistry(); + List beanWrapperList = applicationBeanFactory.beanWrapperList(); + if (CollectionUtil.isEmpty(beanWrapperList)) { + return; + } + List webMvcConfigurerList = applicationBeanFactory.getBeanList(WebMvcConfigurer.class); + if (CollectionUtil.isEmpty(webMvcConfigurerList)) { + return; + } + webMvcConfigurerList.forEach(webMvcConfigurer -> webMvcConfigurer.addInterceptors(interceptorRegistry)); + } + public static List getControllerBeanWrapperList() { return controllerBeanWrapperList; } + + public static InterceptorRegistry getInterceptorRegistry() { + return interceptorRegistry; + } } diff --git a/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/interceptor/HandlerInterceptor.java b/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/interceptor/HandlerInterceptor.java new file mode 100644 index 00000000..1c12abbb --- /dev/null +++ b/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/interceptor/HandlerInterceptor.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2022 aoshiguchen + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package fun.asgc.neutrino.core.web.interceptor; + +import fun.asgc.neutrino.core.web.context.HttpRequestParser; +import io.netty.channel.ChannelHandlerContext; + +import java.lang.reflect.Method; + +/** + * + * @author: aoshiguchen + * @date: 2022/7/27 + */ +public interface HandlerInterceptor { + default boolean preHandle(ChannelHandlerContext context, HttpRequestParser requestParser, String route, Method targetMethod) throws Exception { + return true; + } + + default void postHandle(ChannelHandlerContext context, HttpRequestParser requestParser, String route, Method targetMethod) throws Exception { + } + + default void afterCompletion(ChannelHandlerContext context, HttpRequestParser requestParser, String route, Method targetMethod, Throwable e) throws Exception { + } + +} diff --git a/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/interceptor/InterceptorRegistration.java b/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/interceptor/InterceptorRegistration.java new file mode 100644 index 00000000..ffc65cb9 --- /dev/null +++ b/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/interceptor/InterceptorRegistration.java @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2022 aoshiguchen + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package fun.asgc.neutrino.core.web.interceptor; + +import fun.asgc.neutrino.core.base.Ordered; +import fun.asgc.neutrino.core.util.Assert; +import fun.asgc.neutrino.core.util.StringUtil; +import fun.asgc.neutrino.core.web.PathMatcher; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * + * @author: aoshiguchen + * @date: 2022/7/27 + */ +public class InterceptorRegistration { + private final HandlerInterceptor interceptor; + private final List includePatterns = new ArrayList<>(); + private final List excludePatterns = new ArrayList<>(); + private PathMatcher pathMatcher; + private int order = 0; + + public InterceptorRegistration(HandlerInterceptor interceptor) { + Assert.notNull(interceptor, "Interceptor is required"); + this.interceptor = interceptor; + } + + public InterceptorRegistration addPathPatterns(String... patterns) { + return this.addPathPatterns(Arrays.asList(patterns)); + } + + public InterceptorRegistration addPathPatterns(List patterns) { + this.includePatterns.addAll(patterns); + return this; + } + + public InterceptorRegistration excludePathPatterns(String... patterns) { + return this.excludePathPatterns(Arrays.asList(patterns)); + } + + public InterceptorRegistration excludePathPatterns(List patterns) { + this.excludePatterns.addAll(patterns); + return this; + } + + public InterceptorRegistration pathMatcher(PathMatcher pathMatcher) { + this.pathMatcher = pathMatcher; + return this; + } + + public InterceptorRegistration order(int order) { + this.order = order; + return this; + } + + protected int getOrder() { + return this.order; + } + + public Object getInterceptor() { + if (this.includePatterns.isEmpty() && this.excludePatterns.isEmpty()) { + return this.interceptor; + } else { + String[] include = StringUtil.toStringArray(this.includePatterns); + String[] exclude = StringUtil.toStringArray(this.excludePatterns); + MappedInterceptor mappedInterceptor = new MappedInterceptor(include, exclude, this.interceptor); + if (this.pathMatcher != null) { + mappedInterceptor.setPathMatcher(this.pathMatcher); + } + + return mappedInterceptor; + } + } + + Ordered toOrdered() { + return new Ordered() { + @Override + public int getOrder() { + return order; + } + }; + } +} diff --git a/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/interceptor/InterceptorRegistry.java b/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/interceptor/InterceptorRegistry.java new file mode 100644 index 00000000..ad04c05d --- /dev/null +++ b/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/interceptor/InterceptorRegistry.java @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2022 aoshiguchen + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package fun.asgc.neutrino.core.web.interceptor; + +import fun.asgc.neutrino.core.base.OrderComparator; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * + * @author: aoshiguchen + * @date: 2022/7/27 + */ +public class InterceptorRegistry { + + private final List registrations = new ArrayList(); + + public InterceptorRegistration addInterceptor(HandlerInterceptor interceptor) { + InterceptorRegistration registration = new InterceptorRegistration(interceptor); + this.registrations.add(registration); + return registration; + } + + /** + * Return all registered interceptors. + */ + public List getInterceptors() { + Collections.sort(this.registrations, INTERCEPTOR_ORDER_COMPARATOR); + List result = new ArrayList(this.registrations.size()); + for (InterceptorRegistration registration : this.registrations) { + result.add(registration.getInterceptor()); + } + return result; + } + + + private static final Comparator INTERCEPTOR_ORDER_COMPARATOR = + OrderComparator.INSTANCE.withSourceProvider(new OrderComparator.OrderSourceProvider() { + @Override + public Object getOrderSource(final Object object) { + if (object instanceof InterceptorRegistration) { + return ((InterceptorRegistration) object).toOrdered(); + } + return null; + } + }); + +} diff --git a/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/interceptor/MappedInterceptor.java b/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/interceptor/MappedInterceptor.java new file mode 100644 index 00000000..5703d735 --- /dev/null +++ b/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/interceptor/MappedInterceptor.java @@ -0,0 +1,114 @@ +/** + * Copyright (c) 2022 aoshiguchen + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package fun.asgc.neutrino.core.web.interceptor; + +import fun.asgc.neutrino.core.web.PathMatcher; +import fun.asgc.neutrino.core.web.context.HttpRequestParser; +import io.netty.channel.ChannelHandlerContext; +import org.apache.commons.lang3.ObjectUtils; + +import java.lang.reflect.Method; + +/** + * + * @author: aoshiguchen + * @date: 2022/7/27 + */ +public class MappedInterceptor { + private final String[] includePatterns; + private final String[] excludePatterns; + private final HandlerInterceptor interceptor; + private PathMatcher pathMatcher; + + public MappedInterceptor(String[] includePatterns, HandlerInterceptor interceptor) { + this(includePatterns, null, (HandlerInterceptor)interceptor); + } + + public MappedInterceptor(String[] includePatterns, String[] excludePatterns, HandlerInterceptor interceptor) { + this.includePatterns = includePatterns; + this.excludePatterns = excludePatterns; + this.interceptor = interceptor; + } + + public void setPathMatcher(PathMatcher pathMatcher) { + this.pathMatcher = pathMatcher; + } + + public PathMatcher getPathMatcher() { + return this.pathMatcher; + } + + public String[] getPathPatterns() { + return this.includePatterns; + } + + public HandlerInterceptor getInterceptor() { + return this.interceptor; + } + + public boolean matches(String lookupPath, PathMatcher pathMatcher) { + PathMatcher pathMatcherToUse = this.pathMatcher != null ? this.pathMatcher : pathMatcher; + String[] var4; + int var5; + int var6; + String pattern; + if (!ObjectUtils.isEmpty(this.excludePatterns)) { + var4 = this.excludePatterns; + var5 = var4.length; + + for(var6 = 0; var6 < var5; ++var6) { + pattern = var4[var6]; + if (pathMatcherToUse.match(pattern, lookupPath)) { + return false; + } + } + } + + if (ObjectUtils.isEmpty(this.includePatterns)) { + return true; + } else { + var4 = this.includePatterns; + var5 = var4.length; + + for(var6 = 0; var6 < var5; ++var6) { + pattern = var4[var6]; + if (pathMatcherToUse.match(pattern, lookupPath)) { + return true; + } + } + + return false; + } + } + + public boolean preHandle(ChannelHandlerContext context, HttpRequestParser requestParser, String route, Method targetMethod) throws Exception { + return this.interceptor.preHandle(context, requestParser, route, targetMethod); + } + + public void postHandle(ChannelHandlerContext context, HttpRequestParser requestParser, String route, Method targetMethod) throws Exception { + this.interceptor.postHandle(context, requestParser, route, targetMethod); + } + + public void afterCompletion(ChannelHandlerContext context, HttpRequestParser requestParser, String route, Method targetMethod, Throwable e) throws Exception { + this.interceptor.afterCompletion(context, requestParser, route, targetMethod, e); + } +} diff --git a/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/router/DefaultHttpRouter.java b/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/router/DefaultHttpRouter.java index 264e2bb1..b9c0b486 100644 --- a/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/router/DefaultHttpRouter.java +++ b/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/router/DefaultHttpRouter.java @@ -200,18 +200,24 @@ public class DefaultHttpRouter implements HttpRouter { .setType(HttpRouterType.METHOD) .setMethod(method) .setBeanIdentity(beanWrapper.getIdentity()) + .setPageRoute(path) ); } - private synchronized void addRoute(String path, String pageLocation) { + private synchronized void addRoute(String path, String pageLocation, String pageRoute) { log.info("addRoute path:{} file:{}", path, pageLocation); HttpRouteIdentity identity = new HttpRouteIdentity(HttpMethod.GET, path); if (routeCache.containsKey(identity)) { return; } + pageRoute = pageRoute.replaceAll("\\\\\\\\", "\\\\"); + if (pageRoute.equals("/")) { + pageRoute += "index.html"; + } routeCache.put(identity, new HttpRouteInfo() .setType(HttpRouterType.PAGE) .setPageLocation(pageLocation) + .setPageRoute(pageRoute) ); } @@ -230,7 +236,7 @@ public class DefaultHttpRouter implements HttpRouter { } try (InputStream in = FileUtil.getInputStream(path)){ if (null != in) { - addRoute(httpRouteParam.getUrl(), path); + addRoute(httpRouteParam.getUrl(), path, httpRouteParam.getUrl()); httpRouteInfo = routeCache.get(identity); break; } @@ -247,11 +253,13 @@ public class DefaultHttpRouter implements HttpRouter { if (HttpRouterType.PAGE == httpRouteInfo.getType()) { return new HttpRouteResult() .setType(httpRouteInfo.getType()) - .setPageLocation(httpRouteInfo.getPageLocation()); + .setPageLocation(httpRouteInfo.getPageLocation()) + .setPageRoute(httpRouteInfo.getPageRoute()); } return new HttpRouteResult() .setType(httpRouteInfo.getType()) .setMethod(httpRouteInfo.getMethod()) + .setPageRoute(httpRouteInfo.getPageRoute()) .setInstance(BeanManager.getBean(httpRouteInfo.getBeanIdentity())); } diff --git a/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/router/HttpRouteInfo.java b/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/router/HttpRouteInfo.java index 03fa6727..bf131414 100644 --- a/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/router/HttpRouteInfo.java +++ b/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/router/HttpRouteInfo.java @@ -51,4 +51,8 @@ public class HttpRouteInfo { * 页面位置 */ private String pageLocation; + /** + * 页面路由 + */ + private String pageRoute; } diff --git a/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/router/HttpRouteResult.java b/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/router/HttpRouteResult.java index b62b218a..577302d2 100644 --- a/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/router/HttpRouteResult.java +++ b/neutrino-core/src/main/java/fun/asgc/neutrino/core/web/router/HttpRouteResult.java @@ -50,4 +50,8 @@ public class HttpRouteResult { * 页面位置 */ private String pageLocation; + /** + * 页面路由 + */ + private String pageRoute; } diff --git a/neutrino-core/src/test/java/fun/asgc/neutrino/core/web/interceptor/InterceptorRegistryTest.java b/neutrino-core/src/test/java/fun/asgc/neutrino/core/web/interceptor/InterceptorRegistryTest.java new file mode 100644 index 00000000..3c701d66 --- /dev/null +++ b/neutrino-core/src/test/java/fun/asgc/neutrino/core/web/interceptor/InterceptorRegistryTest.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2022 aoshiguchen + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package fun.asgc.neutrino.core.web.interceptor; + +import fun.asgc.neutrino.core.web.AntPathMatcher; +import org.junit.Test; + +/** + * + * @author: aoshiguchen + * @date: 2022/7/28 + */ +public class InterceptorRegistryTest { + + @Test + public void test1() { + AntPathMatcher antPathMatcher = new AntPathMatcher(); + System.out.println(antPathMatcher.match("/a/*", "/a/11")); + System.out.println(antPathMatcher.match("/a/*", "/a/11/22")); + System.out.println(antPathMatcher.match("/a/**", "/a/11/22")); + System.out.println(antPathMatcher.match("/a/*.html", "/a/11/22")); + System.out.println(antPathMatcher.match("/a/*.html", "/a/11.html")); + System.out.println(antPathMatcher.match("/a/*.html", "/a/11/a.html")); + System.out.println(antPathMatcher.match("/a/**/*.html", "/a/11/a.html")); + System.out.println(antPathMatcher.match("/a/**/*.html", "/a/11/22/33/a.html")); + System.out.println(antPathMatcher.match("/", "/a/11/22/33/a.html")); + System.out.println(antPathMatcher.match("/**", "/a/11/22/33/a.html")); + System.out.println(antPathMatcher.match("*.html", "/a/11/22/33/a.html")); + System.out.println(antPathMatcher.match("**.html", "/a/11/22/33/a.html")); + System.out.println(antPathMatcher.match("**/**.html", "/a/11/22/33/a.html")); + System.out.println(antPathMatcher.match("/**/*.html", "/a/11/22/33/a.html")); + System.out.println(antPathMatcher.match("/**/*.html", "/a.html")); + + + } + +} diff --git a/neutrino-core/src/test/java/fun/asgc/neutrino/core/web/test1/BaseAuthInterceptor.java b/neutrino-core/src/test/java/fun/asgc/neutrino/core/web/test1/BaseAuthInterceptor.java new file mode 100644 index 00000000..652d1d0a --- /dev/null +++ b/neutrino-core/src/test/java/fun/asgc/neutrino/core/web/test1/BaseAuthInterceptor.java @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2022 aoshiguchen + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package fun.asgc.neutrino.core.web.test1; + +import fun.asgc.neutrino.core.web.context.HttpRequestParser; +import fun.asgc.neutrino.core.web.interceptor.HandlerInterceptor; +import io.netty.channel.ChannelHandlerContext; +import lombok.extern.slf4j.Slf4j; + +import java.lang.reflect.Method; + +/** + * + * @author: aoshiguchen + * @date: 2022/7/28 + */ +@Slf4j +public class BaseAuthInterceptor implements HandlerInterceptor { + + /** + * 登录验证 + * @param context + * @param requestParser + * @param route + * @param targetMethod + * @return + * @throws Exception + */ + @Override + public boolean preHandle(ChannelHandlerContext context, HttpRequestParser requestParser, String route, Method targetMethod) throws Exception { + // 登录验证 + String authorize = requestParser.getHeaderValue("Authorize"); + if (!"123abc".equals(authorize)) { + throw new UserNotLoginException(); + } + return Boolean.TRUE; + } + + @Override + public void postHandle(ChannelHandlerContext context, HttpRequestParser requestParser, String route, Method targetMethod) throws Exception { + + } + + @Override + public void afterCompletion(ChannelHandlerContext context, HttpRequestParser requestParser, String route, Method targetMethod, Throwable e) throws Exception { + + } +} diff --git a/neutrino-core/src/test/java/fun/asgc/neutrino/core/web/test1/UserNotLoginException.java b/neutrino-core/src/test/java/fun/asgc/neutrino/core/web/test1/UserNotLoginException.java new file mode 100644 index 00000000..66e6ead0 --- /dev/null +++ b/neutrino-core/src/test/java/fun/asgc/neutrino/core/web/test1/UserNotLoginException.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2022 aoshiguchen + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package fun.asgc.neutrino.core.web.test1; + +/** + * + * @author: aoshiguchen + * @date: 2022/7/28 + */ +public class UserNotLoginException extends Exception { +} diff --git a/neutrino-core/src/test/java/fun/asgc/neutrino/core/web/test1/WebConfigurerAdapter.java b/neutrino-core/src/test/java/fun/asgc/neutrino/core/web/test1/WebConfigurerAdapter.java new file mode 100644 index 00000000..56b50776 --- /dev/null +++ b/neutrino-core/src/test/java/fun/asgc/neutrino/core/web/test1/WebConfigurerAdapter.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2022 aoshiguchen + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package fun.asgc.neutrino.core.web.test1; + +import fun.asgc.neutrino.core.annotation.Configuration; +import fun.asgc.neutrino.core.web.config.WebMvcConfigurer; +import fun.asgc.neutrino.core.web.interceptor.InterceptorRegistry; + +/** + * + * @author: aoshiguchen + * @date: 2022/7/28 + */ +@Configuration +public class WebConfigurerAdapter implements WebMvcConfigurer { + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new BaseAuthInterceptor()) + .addPathPatterns("/**").excludePathPatterns("/**/*.html", "/**/*.js", "/**/*.ico"); + } +} diff --git a/neutrino-core/src/test/resources/application.yml b/neutrino-core/src/test/resources/application.yml index 7123ece4..8b4c35e4 100644 --- a/neutrino-core/src/test/resources/application.yml +++ b/neutrino-core/src/test/resources/application.yml @@ -2,7 +2,7 @@ neutrino: application: name: neutrino-proxy-server http: - enable: false + enable: true port: 8080 context-path: /test max-content-length-desc: 128K