-
Notifications
You must be signed in to change notification settings - Fork 38.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
2 changed files
with
440 additions
and
0 deletions.
There are no files selected for viewing
318 changes: 318 additions & 0 deletions
318
spring-web/src/main/java/org/springframework/web/filter/reactive/UrlHandlerFilter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,318 @@ | ||
/* | ||
* Copyright 2002-2024 the original author or authors. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package org.springframework.web.filter.reactive; | ||
|
||
import java.util.ArrayList; | ||
import java.util.Arrays; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.function.Function; | ||
|
||
import org.apache.commons.logging.Log; | ||
import org.apache.commons.logging.LogFactory; | ||
import reactor.core.publisher.Flux; | ||
import reactor.core.publisher.Mono; | ||
|
||
import org.springframework.http.HttpHeaders; | ||
import org.springframework.http.HttpStatusCode; | ||
import org.springframework.http.server.PathContainer; | ||
import org.springframework.http.server.RequestPath; | ||
import org.springframework.http.server.reactive.ServerHttpRequest; | ||
import org.springframework.http.server.reactive.ServerHttpResponse; | ||
import org.springframework.lang.Nullable; | ||
import org.springframework.util.LinkedMultiValueMap; | ||
import org.springframework.util.MultiValueMap; | ||
import org.springframework.util.StringUtils; | ||
import org.springframework.web.server.ServerWebExchange; | ||
import org.springframework.web.server.WebFilter; | ||
import org.springframework.web.server.WebFilterChain; | ||
import org.springframework.web.util.pattern.PathPattern; | ||
import org.springframework.web.util.pattern.PathPatternParser; | ||
|
||
/** | ||
* {@link org.springframework.web.server.WebFilter} that modifies the URL, and | ||
* then redirects or wraps the request to apply the change. | ||
* | ||
* <p>To create an instance, you can use the following: | ||
* | ||
* <pre> | ||
* UrlHandlerFilter filter = UrlHandlerFilter | ||
* .trailingSlashHandler("/path1/**").redirect(HttpStatus.PERMANENT_REDIRECT) | ||
* .trailingSlashHandler("/path2/**").mutateRequest() | ||
* .build(); | ||
* </pre> | ||
* | ||
* <p>This {@code WebFilter} should be ordered ahead of security filters. | ||
* | ||
* @author Rossen Stoyanchev | ||
* @since 6.2 | ||
*/ | ||
public final class UrlHandlerFilter implements WebFilter { | ||
|
||
private static final Log logger = LogFactory.getLog(UrlHandlerFilter.class); | ||
|
||
|
||
private final MultiValueMap<Handler, PathPattern> handlers; | ||
|
||
|
||
private UrlHandlerFilter(MultiValueMap<Handler, PathPattern> handlers) { | ||
this.handlers = new LinkedMultiValueMap<>(handlers); | ||
} | ||
|
||
|
||
@Override | ||
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { | ||
RequestPath path = exchange.getRequest().getPath(); | ||
for (Map.Entry<Handler, List<PathPattern>> entry : this.handlers.entrySet()) { | ||
if (!entry.getKey().canHandle(exchange)) { | ||
continue; | ||
} | ||
for (PathPattern pattern : entry.getValue()) { | ||
if (pattern.matches(path)) { | ||
return entry.getKey().handle(exchange, chain); | ||
} | ||
} | ||
} | ||
return chain.filter(exchange); | ||
} | ||
|
||
/** | ||
* Create a builder by adding a handler for URL's with a trailing slash. | ||
* @param pathPatterns path patterns to map the handler to, e.g. | ||
* <code>"/path/*"</code>, <code>"/path/**"</code>, | ||
* <code>"/path/foo/"</code>. | ||
* @return a spec to configure the trailing slash handler with | ||
* @see Builder#trailingSlashHandler(String...) | ||
*/ | ||
public static Builder.TrailingSlashSpec trailingSlashHandler(String... pathPatterns) { | ||
return new DefaultBuilder().trailingSlashHandler(pathPatterns); | ||
} | ||
|
||
|
||
/** | ||
* Builder for {@link UrlHandlerFilter}. | ||
*/ | ||
public interface Builder { | ||
|
||
/** | ||
* Add a handler for URL's with a trailing slash. | ||
* @param pathPatterns path patterns to map the handler to, e.g. | ||
* <code>"/path/*"</code>, <code>"/path/**"</code>, | ||
* <code>"/path/foo/"</code>. | ||
* @return a spec to configure the handler with | ||
*/ | ||
TrailingSlashSpec trailingSlashHandler(String... pathPatterns); | ||
|
||
/** | ||
* Build the {@link UrlHandlerFilter} instance. | ||
*/ | ||
UrlHandlerFilter build(); | ||
|
||
|
||
/** | ||
* A spec to configure a trailing slash handler. | ||
*/ | ||
interface TrailingSlashSpec { | ||
|
||
/** | ||
* Configure a request interceptor to be called just before the handler | ||
* is invoked when a URL with a trailing slash is matched. | ||
*/ | ||
TrailingSlashSpec intercept(Function<ServerHttpRequest, Mono<Void>> interceptor); | ||
|
||
/** | ||
* Handle requests by sending a redirect to the same URL but the | ||
* trailing slash trimmed. | ||
* @param statusCode the redirect status to use | ||
* @return the top level {@link Builder}, which allows adding more | ||
* handlers and then building the Filter instance. | ||
*/ | ||
Builder redirect(HttpStatusCode statusCode); | ||
|
||
/** | ||
* Handle the request by wrapping it in order to trim the trailing | ||
* slash, and delegating to the rest of the filter chain. | ||
* @return the top level {@link Builder}, which allows adding more | ||
* handlers and then building the Filter instance. | ||
*/ | ||
Builder mutateRequest(); | ||
} | ||
} | ||
|
||
|
||
/** | ||
* Default {@link Builder} implementation. | ||
*/ | ||
private static final class DefaultBuilder implements Builder { | ||
|
||
private final PathPatternParser patternParser = new PathPatternParser(); | ||
|
||
private final MultiValueMap<Handler, PathPattern> handlers = new LinkedMultiValueMap<>(); | ||
|
||
@Override | ||
public TrailingSlashSpec trailingSlashHandler(String... patterns) { | ||
return new DefaultTrailingSlashSpec(patterns); | ||
} | ||
|
||
private DefaultBuilder addHandler(List<PathPattern> pathPatterns, Handler handler) { | ||
pathPatterns.forEach(pattern -> this.handlers.add(handler, pattern)); | ||
return this; | ||
} | ||
|
||
@Override | ||
public UrlHandlerFilter build() { | ||
return new UrlHandlerFilter(this.handlers); | ||
} | ||
|
||
|
||
private final class DefaultTrailingSlashSpec implements TrailingSlashSpec { | ||
|
||
private final List<PathPattern> pathPatterns; | ||
|
||
@Nullable | ||
private List<Function<ServerHttpRequest, Mono<Void>>> interceptors; | ||
|
||
private DefaultTrailingSlashSpec(String[] patterns) { | ||
this.pathPatterns = Arrays.stream(patterns) | ||
.map(pattern -> pattern.endsWith("**") || pattern.endsWith("/") ? pattern : pattern + "/") | ||
.map(patternParser::parse) | ||
.toList(); | ||
} | ||
|
||
@Override | ||
public TrailingSlashSpec intercept(Function<ServerHttpRequest, Mono<Void>> interceptor) { | ||
this.interceptors = (this.interceptors != null ? this.interceptors : new ArrayList<>()); | ||
this.interceptors.add(interceptor); | ||
return this; | ||
} | ||
|
||
@Override | ||
public Builder redirect(HttpStatusCode statusCode) { | ||
Handler handler = new RedirectTrailingSlashHandler(statusCode, this.interceptors); | ||
return DefaultBuilder.this.addHandler(this.pathPatterns, handler); | ||
} | ||
|
||
@Override | ||
public Builder mutateRequest() { | ||
Handler handler = new RequestWrappingTrailingSlashHandler(this.interceptors); | ||
return DefaultBuilder.this.addHandler(this.pathPatterns, handler); | ||
} | ||
} | ||
} | ||
|
||
|
||
/** | ||
* Internal handler to encapsulate different ways to handle a request. | ||
*/ | ||
private interface Handler { | ||
|
||
/** | ||
* Whether the handler handles the given request. | ||
*/ | ||
boolean canHandle(ServerWebExchange exchange); | ||
|
||
/** | ||
* Handle the request, possibly delegating to the rest of the filter chain. | ||
*/ | ||
Mono<Void> handle(ServerWebExchange exchange, WebFilterChain chain); | ||
} | ||
|
||
|
||
/** | ||
* Base class for trailing slash {@link Handler} implementations. | ||
*/ | ||
private abstract static class AbstractTrailingSlashHandler implements Handler { | ||
|
||
private static final List<Function<ServerHttpRequest, Mono<Void>>> defaultInterceptors = | ||
List.of(request -> { | ||
if (logger.isTraceEnabled()) { | ||
logger.trace("Handling trailing slash URL: " + request.getMethod() + " " + request.getURI()); | ||
} | ||
return Mono.empty(); | ||
}); | ||
|
||
private final List<Function<ServerHttpRequest, Mono<Void>>> interceptors; | ||
|
||
protected AbstractTrailingSlashHandler(@Nullable List<Function<ServerHttpRequest, Mono<Void>>> interceptors) { | ||
this.interceptors = (interceptors != null ? new ArrayList<>(interceptors) : defaultInterceptors); | ||
} | ||
|
||
@Override | ||
public boolean canHandle(ServerWebExchange exchange) { | ||
List<PathContainer.Element> elements = exchange.getRequest().getPath().elements(); | ||
return (elements.size() > 1 && elements.get(elements.size() - 1).value().equals("/")); | ||
} | ||
|
||
@Override | ||
public Mono<Void> handle(ServerWebExchange exchange, WebFilterChain chain) { | ||
List<Mono<Void>> monos = new ArrayList<>(this.interceptors.size()); | ||
this.interceptors.forEach(interceptor -> monos.add(interceptor.apply(exchange.getRequest()))); | ||
return Flux.concat(monos).then(Mono.defer(() -> handleInternal(exchange, chain))); | ||
} | ||
|
||
protected abstract Mono<Void> handleInternal(ServerWebExchange exchange, WebFilterChain chain); | ||
|
||
protected String trimTrailingSlash(ServerHttpRequest request) { | ||
String path = request.getURI().getRawPath(); | ||
int index = (StringUtils.hasLength(path) ? path.lastIndexOf('/') : -1); | ||
return (index != -1 ? path.substring(0, index) : path); | ||
} | ||
} | ||
|
||
|
||
/** | ||
* Path handler that sends a redirect. | ||
*/ | ||
private static final class RedirectTrailingSlashHandler extends AbstractTrailingSlashHandler { | ||
|
||
private final HttpStatusCode statusCode; | ||
|
||
RedirectTrailingSlashHandler( | ||
HttpStatusCode statusCode, @Nullable List<Function<ServerHttpRequest, Mono<Void>>> interceptors) { | ||
|
||
super(interceptors); | ||
this.statusCode = statusCode; | ||
} | ||
|
||
@Override | ||
public Mono<Void> handleInternal(ServerWebExchange exchange, WebFilterChain chain) { | ||
ServerHttpResponse response = exchange.getResponse(); | ||
response.setStatusCode(this.statusCode); | ||
response.getHeaders().set(HttpHeaders.LOCATION, trimTrailingSlash(exchange.getRequest())); | ||
return Mono.empty(); | ||
} | ||
} | ||
|
||
|
||
/** | ||
* Path handler that mutates the request and continues processing. | ||
*/ | ||
private static final class RequestWrappingTrailingSlashHandler extends AbstractTrailingSlashHandler { | ||
|
||
RequestWrappingTrailingSlashHandler(@Nullable List<Function<ServerHttpRequest, Mono<Void>>> interceptors) { | ||
super(interceptors); | ||
} | ||
|
||
@Override | ||
public Mono<Void> handleInternal(ServerWebExchange exchange, WebFilterChain chain) { | ||
ServerHttpRequest request = exchange.getRequest(); | ||
ServerHttpRequest mutatedRequest = request.mutate().path(trimTrailingSlash(request)).build(); | ||
return chain.filter(exchange.mutate().request(mutatedRequest).build()); | ||
} | ||
} | ||
|
||
} |
Oops, something went wrong.