Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@
package org.springframework.web.reactive.accept;

import org.jspecify.annotations.Nullable;
import reactor.core.publisher.Mono;

import org.springframework.web.server.ServerWebExchange;

/**
* Contract to extract the version from a request.
*
* @author Rossen Stoyanchev
* @author Jonathan Kaplan
* @since 7.0
*/
@FunctionalInterface
Expand All @@ -37,4 +39,15 @@ interface ApiVersionResolver {
*/
@Nullable String resolveVersion(ServerWebExchange exchange);

/**
* Asynchronously resolve the version for the given request exchange.
* This method wraps the synchronous {@code resolveVersion} method
* and provides a reactive alternative.
* @param exchange the current request exchange
* @return a {@code Mono} emitting the version value, or an empty {@code Mono} if no version is found
*/
default Mono<String> resolveVersionAsync(ServerWebExchange exchange){
return Mono.justOrEmpty(this.resolveVersion(exchange));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package org.springframework.web.reactive.accept;

import org.jspecify.annotations.Nullable;
import reactor.core.publisher.Mono;

import org.springframework.web.accept.InvalidApiVersionException;
import org.springframework.web.accept.MissingApiVersionException;
Expand All @@ -27,6 +28,7 @@
* to manage API versioning for an application.
*
* @author Rossen Stoyanchev
* @author Jonathan Kaplan
* @since 7.0
* @see DefaultApiVersionStrategy
*/
Expand All @@ -37,10 +39,24 @@ public interface ApiVersionStrategy {
* @param exchange the current exchange
* @return the version, if present or {@code null}
* @see ApiVersionResolver
* @deprecated as of 7.0.3, in favor of
* {@link #resolveVersionAsync(ServerWebExchange)}
*/
@Deprecated(forRemoval = true, since = "7.0.3")
@Nullable
String resolveVersion(ServerWebExchange exchange);


/**
* Resolve the version value from a request asynchronously.
* @param exchange the current server exchange containing the request details
* @return a {@code Mono} emitting the resolved version as a {@code String},
* or an empty {@code Mono} if no version is resolved
*/
default Mono<String> resolveVersionAsync(ServerWebExchange exchange) {
return Mono.justOrEmpty(this.resolveVersion(exchange));
}

/**
* Parse the version of a request into an Object.
* @param version the value to parse
Expand All @@ -59,6 +75,16 @@ public interface ApiVersionStrategy {
void validateVersion(@Nullable Comparable<?> requestVersion, ServerWebExchange exchange)
throws MissingApiVersionException, InvalidApiVersionException;

private Mono<Comparable<?>> validateVersionAsync(@Nullable Comparable<?> requestVersion, ServerWebExchange exchange) {
try {
this.validateVersion(requestVersion, exchange);
return Mono.justOrEmpty(requestVersion);
}
catch (MissingApiVersionException | InvalidApiVersionException ex) {
return Mono.error(ex);
}
}

/**
* Return a default version to use for requests that don't specify one.
*/
Expand All @@ -70,6 +96,8 @@ void validateVersion(@Nullable Comparable<?> requestVersion, ServerWebExchange e
* @param exchange the current exchange
* @return the parsed request version, or the default version
*/
@SuppressWarnings({"DeprecatedIsStillUsed", "DuplicatedCode"})
@Deprecated(forRemoval = true, since = "7.0.3")
default @Nullable Comparable<?> resolveParseAndValidateVersion(ServerWebExchange exchange) {
String value = resolveVersion(exchange);
Comparable<?> version;
Expand All @@ -88,6 +116,32 @@ void validateVersion(@Nullable Comparable<?> requestVersion, ServerWebExchange e
return version;
}

/**
* Convenience method to return the API version from the given request exchange, parse and validate
* the version, and return the result as a reactive {@code Mono} stream. If no version
* is resolved, the default version is used.
* @param exchange the current server exchange containing the request details
* @return a {@code Mono} emitting the resolved, parsed, and validated version as a {@code Comparable<?>},
* or an error in case parsing or validation fails
*/
@SuppressWarnings("Convert2MethodRef")
default Mono<Comparable<?>> resolveParseAndValidateVersionAsync(ServerWebExchange exchange) {
return this.resolveVersionAsync(exchange)
.switchIfEmpty(Mono.justOrEmpty(this.getDefaultVersion())
.mapNotNull(comparable -> comparable.toString()))
.<Comparable<?>>handle((version, sink) -> {
try {
sink.next(this.parseVersion(version));
}
catch (Exception ex) {
sink.error(new InvalidApiVersionException(version, null, ex));
}
})
.flatMap(version -> this.validateVersionAsync(version, exchange))
.switchIfEmpty(this.validateVersionAsync(null, exchange));

}

/**
* Check if the requested API version is deprecated, and if so handle it
* accordingly, e.g. by setting response headers to signal the deprecation,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Function;
import java.util.function.Predicate;

import org.jspecify.annotations.Nullable;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import org.springframework.util.Assert;
import org.springframework.web.accept.ApiVersionParser;
Expand Down Expand Up @@ -152,6 +155,7 @@ public void addMappedVersion(String... versions) {
}
}

@SuppressWarnings("removal")
@Override
public @Nullable String resolveVersion(ServerWebExchange exchange) {
for (ApiVersionResolver resolver : this.versionResolvers) {
Expand All @@ -163,6 +167,14 @@ public void addMappedVersion(String... versions) {
return null;
}

@Override
public Mono<String> resolveVersionAsync(ServerWebExchange exchange) {
return Flux.fromIterable(this.versionResolvers)
.mapNotNull(resolver -> resolver.resolveVersionAsync(exchange))
.flatMap(Function.identity())
.next();
}

@Override
public Comparable<?> parseVersion(String version) {
return this.versionParser.parseVersion(version);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,38 +185,51 @@ protected String formatMappingName() {

@Override
public Mono<Object> getHandler(ServerWebExchange exchange) {
ApiVersionHolder versionHolder = initApiVersion(exchange);
return getHandlerInternal(exchange).map(handler -> {
if (logger.isDebugEnabled()) {
logger.debug(exchange.getLogPrefix() + "Mapped to " + handler);
}
if (versionHolder.hasError()) {
throw versionHolder.getError();
}
ServerHttpRequest request = exchange.getRequest();
if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {
CorsConfiguration config = (this.corsConfigurationSource != null ?
this.corsConfigurationSource.getCorsConfiguration(exchange) : null);
CorsConfiguration handlerConfig = getCorsConfiguration(handler, exchange);
config = (config != null ? config.combine(handlerConfig) : handlerConfig);
if (config != null) {
config.validateAllowCredentials();
config.validateAllowPrivateNetwork();
}
if (!this.corsProcessor.process(config, exchange) || CorsUtils.isPreFlightRequest(request)) {
return NO_OP_HANDLER;
}
}
if (getApiVersionStrategy() != null) {
if (versionHolder.hasVersion()) {
Comparable<?> version = versionHolder.getVersion();
getApiVersionStrategy().handleDeprecations(version, handler, exchange);
}
}
return handler;
});
final var versionHolder = this.initApiVersion(exchange);
return this.initApiVersionAsync(exchange).flatMap( versionHolderAsync->
getHandlerInternal(exchange)
.handle((handler, sink)->{
if (logger.isDebugEnabled()) {
logger.debug(exchange.getLogPrefix() + "Mapped to " + handler);
}
if (versionHolder.hasError()){
sink.error(versionHolder.getError());
}
else if (versionHolderAsync.hasError()) {
sink.error(versionHolderAsync.getError());
}
else {
sink.next(handler);
}
})
.map(handler -> {
ServerHttpRequest request = exchange.getRequest();
if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {
CorsConfiguration config = (this.corsConfigurationSource != null ?
this.corsConfigurationSource.getCorsConfiguration(exchange) :
null);
CorsConfiguration handlerConfig = getCorsConfiguration(handler, exchange);
config = (config != null ? config.combine(handlerConfig) : handlerConfig);
if (config != null) {
config.validateAllowCredentials();
config.validateAllowPrivateNetwork();
}
if (!this.corsProcessor.process(config, exchange) || CorsUtils.isPreFlightRequest(request)) {
return NO_OP_HANDLER;
}
}
if (getApiVersionStrategy() != null) {
if (versionHolder.hasVersion()) {
Comparable<?> version = versionHolder.getVersion();
getApiVersionStrategy().handleDeprecations(version, handler, exchange);
}
}
return handler;
}));
}

@Deprecated(since = "7.0.3", forRemoval = true)
@SuppressWarnings({"removal", "DeprecatedIsStillUsed"})
private ApiVersionHolder initApiVersion(ServerWebExchange exchange) {
ApiVersionHolder versionHolder;
if (this.apiVersionStrategy == null) {
Expand All @@ -235,6 +248,21 @@ private ApiVersionHolder initApiVersion(ServerWebExchange exchange) {
return versionHolder;
}

private Mono<ApiVersionHolder> initApiVersionAsync(ServerWebExchange exchange) {
if (this.apiVersionStrategy != null) {
if (exchange.getAttribute(API_VERSION_ATTRIBUTE) == null) {
return this.apiVersionStrategy
.resolveParseAndValidateVersionAsync(exchange)
.map(ApiVersionHolder::fromVersion)
.onErrorResume(ex -> Mono.just(ApiVersionHolder.fromError(new RuntimeException(ex))))
.doOnNext(holder -> exchange.getAttributes()
.put(API_VERSION_ATTRIBUTE, holder));

}
}
return Mono.just(ApiVersionHolder.EMPTY);
}

/**
* Look up a handler for the given request, returning an empty {@code Mono}
* if no specific one is found. This method is called by {@link #getHandler}.
Expand Down
Loading