Rename spring-web-reactive to spring-webflux
Issue: SPR-15190
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive;
|
||||
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.validation.support.BindingAwareConcurrentModel;
|
||||
import org.springframework.web.bind.support.WebBindingInitializer;
|
||||
import org.springframework.web.bind.support.WebExchangeDataBinder;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* A context for binding requests to method arguments that provides access to
|
||||
* the default model, data binding, validation, and type conversion.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public class BindingContext {
|
||||
|
||||
private final Model model = new BindingAwareConcurrentModel();
|
||||
|
||||
private final WebBindingInitializer initializer;
|
||||
|
||||
|
||||
public BindingContext() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public BindingContext(WebBindingInitializer initializer) {
|
||||
this.initializer = initializer;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the default model.
|
||||
*/
|
||||
public Model getModel() {
|
||||
return this.model;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a {@link WebExchangeDataBinder} for applying data binding, type
|
||||
* conversion, and validation on the given "target" object.
|
||||
* @param exchange the current exchange
|
||||
* @param target the object to create a data binder for
|
||||
* @param name the name of the target object
|
||||
* @return the {@link WebExchangeDataBinder} instance
|
||||
*/
|
||||
public WebExchangeDataBinder createDataBinder(ServerWebExchange exchange, Object target, String name) {
|
||||
WebExchangeDataBinder dataBinder = createBinderInstance(target, name);
|
||||
if (this.initializer != null) {
|
||||
this.initializer.initBinder(dataBinder);
|
||||
}
|
||||
return initDataBinder(dataBinder, exchange);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link WebExchangeDataBinder} without a "target" object, i.e.
|
||||
* for applying type conversion to simple types.
|
||||
* @param exchange the current exchange
|
||||
* @param name the name of the target object
|
||||
* @return a Mono for the created {@link WebExchangeDataBinder} instance
|
||||
*/
|
||||
public WebExchangeDataBinder createDataBinder(ServerWebExchange exchange, String name) {
|
||||
return createDataBinder(exchange, null, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the data binder instance.
|
||||
*/
|
||||
protected WebExchangeDataBinder createBinderInstance(Object target, String objectName) {
|
||||
return new WebExchangeDataBinder(target, objectName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the data binder instance for the given exchange.
|
||||
*/
|
||||
protected WebExchangeDataBinder initDataBinder(WebExchangeDataBinder binder, ServerWebExchange exchange) {
|
||||
return binder;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
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.beans.factory.BeanFactoryUtils;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.server.reactive.HttpHandler;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebHandler;
|
||||
import org.springframework.web.server.adapter.HttpWebHandlerAdapter;
|
||||
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
|
||||
|
||||
/**
|
||||
* Central dispatcher for HTTP request handlers/controllers. Dispatches to registered
|
||||
* handlers for processing a web request, providing convenient mapping facilities.
|
||||
*
|
||||
* <p>It can use any {@link HandlerMapping} implementation to control the routing of
|
||||
* requests to handler objects. HandlerMapping objects can be defined as beans in
|
||||
* the application context.
|
||||
*
|
||||
* <p>It can use any {@link HandlerAdapter}; this allows for using any handler interface.
|
||||
* HandlerAdapter objects can be added as beans in the application context.
|
||||
*
|
||||
* <p>It can use any {@link HandlerResultHandler}; this allows to process the result of
|
||||
* the request handling. HandlerResultHandler objects can be added as beans in the
|
||||
* application context.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Sebastien Deleuze
|
||||
* @author Juergen Hoeller
|
||||
* @since 5.0
|
||||
*/
|
||||
public class DispatcherHandler implements WebHandler, ApplicationContextAware {
|
||||
|
||||
@SuppressWarnings("ThrowableInstanceNeverThrown")
|
||||
private static final Exception HANDLER_NOT_FOUND_EXCEPTION =
|
||||
new ResponseStatusException(HttpStatus.NOT_FOUND, "No matching handler");
|
||||
|
||||
|
||||
private static final Log logger = LogFactory.getLog(DispatcherHandler.class);
|
||||
|
||||
private List<HandlerMapping> handlerMappings;
|
||||
|
||||
private List<HandlerAdapter> handlerAdapters;
|
||||
|
||||
private List<HandlerResultHandler> resultHandlers;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new {@code DispatcherHandler} which needs to be configured with
|
||||
* an {@link ApplicationContext} through {@link #setApplicationContext}.
|
||||
*/
|
||||
public DispatcherHandler() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@code DispatcherHandler} for the given {@link ApplicationContext}.
|
||||
* @param applicationContext the application context to find the handler beans in
|
||||
*/
|
||||
public DispatcherHandler(ApplicationContext applicationContext) {
|
||||
initStrategies(applicationContext);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) {
|
||||
initStrategies(applicationContext);
|
||||
}
|
||||
|
||||
protected void initStrategies(ApplicationContext context) {
|
||||
Map<String, HandlerMapping> mappingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(
|
||||
context, HandlerMapping.class, true, false);
|
||||
|
||||
this.handlerMappings = new ArrayList<>(mappingBeans.values());
|
||||
AnnotationAwareOrderComparator.sort(this.handlerMappings);
|
||||
|
||||
Map<String, HandlerAdapter> adapterBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(
|
||||
context, HandlerAdapter.class, true, false);
|
||||
|
||||
this.handlerAdapters = new ArrayList<>(adapterBeans.values());
|
||||
AnnotationAwareOrderComparator.sort(this.handlerAdapters);
|
||||
|
||||
Map<String, HandlerResultHandler> beans = BeanFactoryUtils.beansOfTypeIncludingAncestors(
|
||||
context, HandlerResultHandler.class, true, false);
|
||||
|
||||
this.resultHandlers = new ArrayList<>(beans.values());
|
||||
AnnotationAwareOrderComparator.sort(this.resultHandlers);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Mono<Void> handle(ServerWebExchange exchange) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
ServerHttpRequest request = exchange.getRequest();
|
||||
logger.debug("Processing " + request.getMethod() + " request for [" + request.getURI() + "]");
|
||||
}
|
||||
return Flux.fromIterable(this.handlerMappings)
|
||||
.concatMap(mapping -> mapping.getHandler(exchange))
|
||||
.next()
|
||||
.otherwiseIfEmpty(Mono.error(HANDLER_NOT_FOUND_EXCEPTION))
|
||||
.then(handler -> invokeHandler(exchange, handler))
|
||||
.then(result -> handleResult(exchange, result));
|
||||
}
|
||||
|
||||
private Mono<HandlerResult> invokeHandler(ServerWebExchange exchange, Object handler) {
|
||||
for (HandlerAdapter handlerAdapter : this.handlerAdapters) {
|
||||
if (handlerAdapter.supports(handler)) {
|
||||
return handlerAdapter.handle(exchange, handler);
|
||||
}
|
||||
}
|
||||
return Mono.error(new IllegalStateException("No HandlerAdapter: " + handler));
|
||||
}
|
||||
|
||||
private Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) {
|
||||
return getResultHandler(result).handleResult(exchange, result)
|
||||
.otherwise(ex -> result.applyExceptionHandler(ex).then(exceptionResult ->
|
||||
getResultHandler(exceptionResult).handleResult(exchange, exceptionResult)));
|
||||
}
|
||||
|
||||
private HandlerResultHandler getResultHandler(HandlerResult handlerResult) {
|
||||
for (HandlerResultHandler resultHandler : this.resultHandlers) {
|
||||
if (resultHandler.supports(handlerResult)) {
|
||||
return resultHandler;
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("No HandlerResultHandler for " + handlerResult.getReturnValue());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Expose a dispatcher-based {@link WebHandler} for the given application context,
|
||||
* typically for further configuration with filters and exception handlers through
|
||||
* a {@link org.springframework.web.server.adapter.WebHttpHandlerBuilder}.
|
||||
* @param applicationContext the application context to find the handler beans in
|
||||
* @see #DispatcherHandler(ApplicationContext)
|
||||
* @see org.springframework.web.server.adapter.WebHttpHandlerBuilder#webHandler
|
||||
*/
|
||||
public static WebHandler toWebHandler(ApplicationContext applicationContext) {
|
||||
return new DispatcherHandler(applicationContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expose a dispatcher-based {@link HttpHandler} for the given application context,
|
||||
* typically for direct registration with an engine adapter such as
|
||||
* {@link org.springframework.http.server.reactive.ServletHttpHandlerAdapter}.
|
||||
*
|
||||
* <p>Delegates to {@link WebHttpHandlerBuilder#applicationContext} that
|
||||
* detects the target {@link DispatcherHandler} along with
|
||||
* {@link org.springframework.web.server.WebFilter}s, and
|
||||
* {@link org.springframework.web.server.WebExceptionHandler}s in the given
|
||||
* ApplicationContext.
|
||||
*
|
||||
* @param context the application context to find the handler beans in
|
||||
* @see #DispatcherHandler(ApplicationContext)
|
||||
* @see HttpWebHandlerAdapter
|
||||
* @see org.springframework.http.server.reactive.ServletHttpHandlerAdapter
|
||||
* @see org.springframework.http.server.reactive.ReactorHttpHandlerAdapter
|
||||
* @see org.springframework.http.server.reactive.RxNettyHttpHandlerAdapter
|
||||
* @see org.springframework.http.server.reactive.UndertowHttpHandlerAdapter
|
||||
*/
|
||||
public static HttpHandler toHttpHandler(ApplicationContext context) {
|
||||
return WebHttpHandlerBuilder.applicationContext(context).build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* Contract that decouples the {@link DispatcherHandler} from the details of
|
||||
* invoking a handler and makes it possible to support any handler type.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Sebastien Deleuze
|
||||
* @since 5.0
|
||||
*/
|
||||
public interface HandlerAdapter {
|
||||
|
||||
/**
|
||||
* Whether this {@code HandlerAdapter} supports the given {@code handler}.
|
||||
* @param handler handler object to check
|
||||
* @return whether or not the handler is supported
|
||||
*/
|
||||
boolean supports(Object handler);
|
||||
|
||||
/**
|
||||
* Handle the request with the given handler.
|
||||
* <p>Implementations are encouraged to handle exceptions resulting from the
|
||||
* invocation of a handler in order and if necessary to return an alternate
|
||||
* result that represents an error response.
|
||||
* <p>Furthermore since an async {@code HandlerResult} may produce an error
|
||||
* later during result handling implementations are also encouraged to
|
||||
* {@link HandlerResult#setExceptionHandler(Function) set an exception
|
||||
* handler} on the {@code HandlerResult} so that may also be applied later
|
||||
* after result handling.
|
||||
* @param exchange current server exchange
|
||||
* @param handler the selected handler which must have been previously
|
||||
* checked via {@link #supports(Object)}
|
||||
* @return {@link Mono} that emits a single {@code HandlerResult} or none if
|
||||
* the request has been fully handled and doesn't require further handling.
|
||||
*/
|
||||
Mono<HandlerResult> handle(ServerWebExchange exchange, Object handler);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* Interface to be implemented by objects that define a mapping between
|
||||
* requests and handler objects.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Sebastien Deleuze
|
||||
* @since 5.0
|
||||
*/
|
||||
public interface HandlerMapping {
|
||||
|
||||
/**
|
||||
* Name of the {@link ServerWebExchange} attribute that contains the
|
||||
* best matching pattern within the handler mapping.
|
||||
* <p>Note: This attribute is not required to be supported by all
|
||||
* HandlerMapping implementations. URL-based HandlerMappings will
|
||||
* typically support it, but handlers should not necessarily expect
|
||||
* this request attribute to be present in all scenarios.
|
||||
*/
|
||||
String BEST_MATCHING_PATTERN_ATTRIBUTE = HandlerMapping.class.getName() + ".bestMatchingPattern";
|
||||
|
||||
/**
|
||||
* Name of the {@link ServerWebExchange} attribute that contains the path
|
||||
* within the handler mapping, in case of a pattern match, or the full
|
||||
* relevant URI (typically within the DispatcherServlet's mapping) else.
|
||||
* <p>Note: This attribute is not required to be supported by all
|
||||
* HandlerMapping implementations. URL-based HandlerMappings will
|
||||
* typically support it, but handlers should not necessarily expect
|
||||
* this request attribute to be present in all scenarios.
|
||||
*/
|
||||
String PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE = HandlerMapping.class.getName() + ".pathWithinHandlerMapping";
|
||||
|
||||
/**
|
||||
* Name of the {@link ServerWebExchange} attribute that contains the URI
|
||||
* templates map, mapping variable names to values.
|
||||
* <p>Note: This attribute is not required to be supported by all
|
||||
* HandlerMapping implementations. URL-based HandlerMappings will
|
||||
* typically support it, but handlers should not necessarily expect
|
||||
* this request attribute to be present in all scenarios.
|
||||
*/
|
||||
String URI_TEMPLATE_VARIABLES_ATTRIBUTE = HandlerMapping.class.getName() + ".uriTemplateVariables";
|
||||
|
||||
/**
|
||||
* Name of the {@link ServerWebExchange} attribute that contains a map with
|
||||
* URI matrix variables.
|
||||
* <p>Note: This attribute is not required to be supported by all
|
||||
* HandlerMapping implementations and may also not be present depending on
|
||||
* whether the HandlerMapping is configured to keep matrix variable content
|
||||
* in the request URI.
|
||||
*/
|
||||
String MATRIX_VARIABLES_ATTRIBUTE = HandlerMapping.class.getName() + ".matrixVariables";
|
||||
|
||||
/**
|
||||
* Name of the {@link ServerWebExchange} attribute that contains the set of
|
||||
* producible MediaTypes applicable to the mapped handler.
|
||||
* <p>Note: This attribute is not required to be supported by all
|
||||
* HandlerMapping implementations. Handlers should not necessarily expect
|
||||
* this request attribute to be present in all scenarios.
|
||||
*/
|
||||
String PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE = HandlerMapping.class.getName() + ".producibleMediaTypes";
|
||||
|
||||
|
||||
/**
|
||||
* Return a handler for this request.
|
||||
* @param exchange current server exchange
|
||||
* @return a {@link Mono} that emits one value or none in case the request
|
||||
* cannot be resolved to a handler
|
||||
*/
|
||||
Mono<Object> getHandler(ServerWebExchange exchange);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* Copyright 2002-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Represent the result of the invocation of a handler or a handler method.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public class HandlerResult {
|
||||
|
||||
private final Object handler;
|
||||
|
||||
private final Object returnValue;
|
||||
|
||||
private final ResolvableType returnType;
|
||||
|
||||
private final BindingContext bindingContext;
|
||||
|
||||
private Function<Throwable, Mono<HandlerResult>> exceptionHandler;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new {@code HandlerResult}.
|
||||
* @param handler the handler that handled the request
|
||||
* @param returnValue the return value from the handler possibly {@code null}
|
||||
* @param returnType the return value type
|
||||
*/
|
||||
public HandlerResult(Object handler, Object returnValue, MethodParameter returnType) {
|
||||
this(handler, returnValue, returnType, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@code HandlerResult}.
|
||||
* @param handler the handler that handled the request
|
||||
* @param returnValue the return value from the handler possibly {@code null}
|
||||
* @param returnType the return value type
|
||||
* @param context the binding context used for request handling
|
||||
*/
|
||||
public HandlerResult(Object handler, Object returnValue, MethodParameter returnType,
|
||||
BindingContext context) {
|
||||
|
||||
Assert.notNull(handler, "'handler' is required");
|
||||
Assert.notNull(returnType, "'returnType' is required");
|
||||
this.handler = handler;
|
||||
this.returnValue = returnValue;
|
||||
this.returnType = ResolvableType.forMethodParameter(returnType);
|
||||
this.bindingContext = (context != null ? context : new BindingContext());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the handler that handled the request.
|
||||
*/
|
||||
public Object getHandler() {
|
||||
return this.handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the value returned from the handler wrapped as {@link Optional}.
|
||||
*/
|
||||
public Optional<Object> getReturnValue() {
|
||||
return Optional.ofNullable(this.returnValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the type of the value returned from the handler.
|
||||
*/
|
||||
public ResolvableType getReturnType() {
|
||||
return this.returnType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the {@link MethodParameter} from which
|
||||
* {@link #getReturnType() returnType} was created.
|
||||
*/
|
||||
public MethodParameter getReturnTypeSource() {
|
||||
return (MethodParameter) this.returnType.getSource();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the BindingContext used for request handling.
|
||||
*/
|
||||
public BindingContext getBindingContext() {
|
||||
return this.bindingContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the model used for request handling. This is a shortcut for
|
||||
* {@code getBindingContext().getModel()}.
|
||||
*/
|
||||
public Model getModel() {
|
||||
return this.bindingContext.getModel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure an exception handler that may be used to produce an alternative
|
||||
* result when result handling fails. Especially for an async return value
|
||||
* errors may occur after the invocation of the handler.
|
||||
* @param function the error handler
|
||||
* @return the current instance
|
||||
*/
|
||||
public HandlerResult setExceptionHandler(Function<Throwable, Mono<HandlerResult>> function) {
|
||||
this.exceptionHandler = function;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether there is an exception handler.
|
||||
*/
|
||||
public boolean hasExceptionHandler() {
|
||||
return (this.exceptionHandler != null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the exception handler and return the alternative result.
|
||||
* @param failure the exception
|
||||
* @return the new result or the same error if there is no exception handler
|
||||
*/
|
||||
public Mono<HandlerResult> applyExceptionHandler(Throwable failure) {
|
||||
return (hasExceptionHandler() ? this.exceptionHandler.apply(failure) : Mono.error(failure));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* Process the {@link HandlerResult}, usually returned by an {@link HandlerAdapter}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Sebastien Deleuze
|
||||
* @since 5.0
|
||||
*/
|
||||
public interface HandlerResultHandler {
|
||||
|
||||
/**
|
||||
* Whether this handler supports the given {@link HandlerResult}.
|
||||
* @param result result object to check
|
||||
* @return whether or not this object can use the given result
|
||||
*/
|
||||
boolean supports(HandlerResult result);
|
||||
|
||||
/**
|
||||
* Process the given result modifying response headers and/or writing data
|
||||
* to the response.
|
||||
* @param exchange current server exchange
|
||||
* @param result the result from the handling
|
||||
* @return {@code Mono<Void>} to indicate when request handling is complete.
|
||||
*/
|
||||
Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.web.reactive.accept;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.server.NotAcceptableStatusException;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* Abstract base class for {@link MappingContentTypeResolver} implementations.
|
||||
* Maintains the actual mappings and pre-implements the overall algorithm with
|
||||
* sub-classes left to provide a way to extract the lookup key (e.g. file
|
||||
* extension, query parameter, etc) for a given exchange.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public abstract class AbstractMappingContentTypeResolver implements MappingContentTypeResolver {
|
||||
|
||||
/** Primary lookup for media types by key (e.g. "json" -> "application/json") */
|
||||
private final Map<String, MediaType> mediaTypeLookup = new ConcurrentHashMap<>(64);
|
||||
|
||||
/** Reverse lookup for keys associated with a media type */
|
||||
private final MultiValueMap<MediaType, String> keyLookup = new LinkedMultiValueMap<>(64);
|
||||
|
||||
|
||||
/**
|
||||
* Create an instance with the given map of file extensions and media types.
|
||||
*/
|
||||
public AbstractMappingContentTypeResolver(Map<String, MediaType> mediaTypes) {
|
||||
if (mediaTypes != null) {
|
||||
for (Map.Entry<String, MediaType> entry : mediaTypes.entrySet()) {
|
||||
String extension = entry.getKey().toLowerCase(Locale.ENGLISH);
|
||||
MediaType mediaType = entry.getValue();
|
||||
this.mediaTypeLookup.put(extension, mediaType);
|
||||
this.keyLookup.add(mediaType, extension);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public Map<String, MediaType> getMediaTypes() {
|
||||
return this.mediaTypeLookup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sub-classes can use this method to look up a MediaType by key.
|
||||
* @param key the key converted to lower case
|
||||
* @return a MediaType or {@code null}
|
||||
*/
|
||||
protected MediaType getMediaType(String key) {
|
||||
return this.mediaTypeLookup.get(key.toLowerCase(Locale.ENGLISH));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sub-classes can use this method get all mapped media types.
|
||||
*/
|
||||
protected List<MediaType> getAllMediaTypes() {
|
||||
return new ArrayList<>(this.mediaTypeLookup.values());
|
||||
}
|
||||
|
||||
|
||||
// RequestedContentTypeResolver implementation
|
||||
|
||||
@Override
|
||||
public List<MediaType> resolveMediaTypes(ServerWebExchange exchange)
|
||||
throws NotAcceptableStatusException {
|
||||
|
||||
String key = extractKey(exchange);
|
||||
return resolveMediaTypes(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* An overloaded resolve method with a pre-resolved lookup key.
|
||||
* @param key the key for looking up media types
|
||||
* @return a list of resolved media types or an empty list
|
||||
* @throws NotAcceptableStatusException
|
||||
*/
|
||||
public List<MediaType> resolveMediaTypes(String key) throws NotAcceptableStatusException {
|
||||
if (StringUtils.hasText(key)) {
|
||||
MediaType mediaType = getMediaType(key);
|
||||
if (mediaType != null) {
|
||||
handleMatch(key, mediaType);
|
||||
return Collections.singletonList(mediaType);
|
||||
}
|
||||
mediaType = handleNoMatch(key);
|
||||
if (mediaType != null) {
|
||||
MediaType previous = this.mediaTypeLookup.putIfAbsent(key, mediaType);
|
||||
if (previous == null) {
|
||||
this.keyLookup.add(mediaType, key);
|
||||
}
|
||||
return Collections.singletonList(mediaType);
|
||||
}
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the key to use to look up a media type from the given exchange,
|
||||
* e.g. file extension, query parameter, etc.
|
||||
* @return the key or {@code null}
|
||||
*/
|
||||
protected abstract String extractKey(ServerWebExchange exchange);
|
||||
|
||||
/**
|
||||
* Override to provide handling when a key is successfully resolved via
|
||||
* {@link #getMediaType(String)}.
|
||||
*/
|
||||
@SuppressWarnings("UnusedParameters")
|
||||
protected void handleMatch(String key, MediaType mediaType) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Override to provide handling when a key is not resolved via.
|
||||
* {@link #getMediaType(String)}. If a MediaType is returned from
|
||||
* this method it will be added to the mappings.
|
||||
*/
|
||||
@SuppressWarnings("UnusedParameters")
|
||||
protected MediaType handleNoMatch(String key) throws NotAcceptableStatusException {
|
||||
return null;
|
||||
}
|
||||
|
||||
// MappingContentTypeResolver implementation
|
||||
|
||||
@Override
|
||||
public Set<String> getKeysFor(MediaType mediaType) {
|
||||
List<String> keys = this.keyLookup.get(mediaType);
|
||||
return (keys != null ? new HashSet<>(keys) : Collections.emptySet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getKeys() {
|
||||
return new HashSet<>(this.mediaTypeLookup.keySet());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.web.reactive.accept;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.server.NotAcceptableStatusException;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* A {@link RequestedContentTypeResolver} that contains and delegates to a list of other
|
||||
* resolvers.
|
||||
*
|
||||
* <p>Also an implementation of {@link MappingContentTypeResolver} that delegates
|
||||
* to those resolvers from the list that are also of type
|
||||
* {@code MappingContentTypeResolver}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public class CompositeContentTypeResolver implements MappingContentTypeResolver {
|
||||
|
||||
private final List<RequestedContentTypeResolver> resolvers = new ArrayList<>();
|
||||
|
||||
|
||||
public CompositeContentTypeResolver(List<RequestedContentTypeResolver> resolvers) {
|
||||
Assert.notEmpty(resolvers, "At least one resolver is expected.");
|
||||
this.resolvers.addAll(resolvers);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return a read-only list of the configured resolvers.
|
||||
*/
|
||||
public List<RequestedContentTypeResolver> getResolvers() {
|
||||
return Collections.unmodifiableList(this.resolvers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the first {@link RequestedContentTypeResolver} of the given type.
|
||||
* @param resolverType the resolver type
|
||||
* @return the first matching resolver or {@code null}.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends RequestedContentTypeResolver> T findResolver(Class<T> resolverType) {
|
||||
for (RequestedContentTypeResolver resolver : this.resolvers) {
|
||||
if (resolverType.isInstance(resolver)) {
|
||||
return (T) resolver;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public List<MediaType> resolveMediaTypes(ServerWebExchange exchange) throws NotAcceptableStatusException {
|
||||
for (RequestedContentTypeResolver resolver : this.resolvers) {
|
||||
List<MediaType> mediaTypes = resolver.resolveMediaTypes(exchange);
|
||||
if (mediaTypes.isEmpty() || (mediaTypes.size() == 1 && mediaTypes.contains(MediaType.ALL))) {
|
||||
continue;
|
||||
}
|
||||
return mediaTypes;
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getKeysFor(MediaType mediaType) {
|
||||
Set<String> result = new LinkedHashSet<>();
|
||||
for (RequestedContentTypeResolver resolver : this.resolvers) {
|
||||
if (resolver instanceof MappingContentTypeResolver)
|
||||
result.addAll(((MappingContentTypeResolver) resolver).getKeysFor(mediaType));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getKeys() {
|
||||
Set<String> result = new LinkedHashSet<>();
|
||||
for (RequestedContentTypeResolver resolver : this.resolvers) {
|
||||
if (resolver instanceof MappingContentTypeResolver)
|
||||
result.addAll(((MappingContentTypeResolver) resolver).getKeys());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2002-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.accept;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* A {@link RequestedContentTypeResolver} that resolves to a fixed list of media types.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public class FixedContentTypeResolver implements RequestedContentTypeResolver {
|
||||
|
||||
private final List<MediaType> mediaTypes;
|
||||
|
||||
|
||||
/**
|
||||
* Create an instance with the given content type.
|
||||
*/
|
||||
public FixedContentTypeResolver(MediaType mediaTypes) {
|
||||
this.mediaTypes = Collections.singletonList(mediaTypes);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public List<MediaType> resolveMediaTypes(ServerWebExchange exchange) {
|
||||
return this.mediaTypes;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.web.reactive.accept;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.http.InvalidMediaTypeException;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.server.NotAcceptableStatusException;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* A {@link RequestedContentTypeResolver} that checks the 'Accept' request header.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public class HeaderContentTypeResolver implements RequestedContentTypeResolver {
|
||||
|
||||
@Override
|
||||
public List<MediaType> resolveMediaTypes(ServerWebExchange exchange) throws NotAcceptableStatusException {
|
||||
try {
|
||||
List<MediaType> mediaTypes = exchange.getRequest().getHeaders().getAccept();
|
||||
MediaType.sortBySpecificityAndQuality(mediaTypes);
|
||||
return mediaTypes;
|
||||
}
|
||||
catch (InvalidMediaTypeException ex) {
|
||||
String value = exchange.getRequest().getHeaders().getFirst("Accept");
|
||||
throw new NotAcceptableStatusException(
|
||||
"Could not parse 'Accept' header [" + value + "]: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.accept;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
/**
|
||||
* An extension of {@link RequestedContentTypeResolver} that maintains a mapping
|
||||
* between keys (e.g. file extension, query parameter) and media types.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public interface MappingContentTypeResolver extends RequestedContentTypeResolver {
|
||||
|
||||
/**
|
||||
* Resolve the given media type to a list of path extensions.
|
||||
*
|
||||
* @param mediaType the media type to resolve
|
||||
* @return a list of extensions or an empty list, never {@code null}
|
||||
*/
|
||||
Set<String> getKeysFor(MediaType mediaType);
|
||||
|
||||
/**
|
||||
* Return all registered keys (e.g. "json", "xml").
|
||||
* @return a list of keys or an empty list, never {@code null}
|
||||
*/
|
||||
Set<String> getKeys();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.accept;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.server.NotAcceptableStatusException;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* A {@link RequestedContentTypeResolver} that extracts the media type lookup
|
||||
* key from a known query parameter named "format" by default.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public class ParameterContentTypeResolver extends AbstractMappingContentTypeResolver {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(ParameterContentTypeResolver.class);
|
||||
|
||||
private String parameterName = "format";
|
||||
|
||||
|
||||
/**
|
||||
* Create an instance with the given map of file extensions and media types.
|
||||
*/
|
||||
public ParameterContentTypeResolver(Map<String, MediaType> mediaTypes) {
|
||||
super(mediaTypes);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the name of the parameter to use to determine requested media types.
|
||||
* <p>By default this is set to {@code "format"}.
|
||||
*/
|
||||
public void setParameterName(String parameterName) {
|
||||
Assert.notNull(parameterName, "parameterName is required");
|
||||
this.parameterName = parameterName;
|
||||
}
|
||||
|
||||
public String getParameterName() {
|
||||
return this.parameterName;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected String extractKey(ServerWebExchange exchange) {
|
||||
return exchange.getRequest().getQueryParams().getFirst(getParameterName());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleMatch(String mediaTypeKey, MediaType mediaType) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Requested media type is '" + mediaType +
|
||||
"' based on '" + getParameterName() + "'='" + mediaTypeKey + "'.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MediaType handleNoMatch(String key) throws NotAcceptableStatusException {
|
||||
throw new NotAcceptableStatusException(getAllMediaTypes());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.accept;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.MediaTypeFactory;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.server.NotAcceptableStatusException;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.util.UriUtils;
|
||||
|
||||
/**
|
||||
* A {@link RequestedContentTypeResolver} that extracts the file extension from
|
||||
* the request path and uses that as the media type lookup key.
|
||||
*
|
||||
* <p>If the file extension is not found in the explicit registrations provided
|
||||
* to the constructor, the Java Activation Framework (JAF) is used as a fallback
|
||||
* mechanism. The presence of the JAF is detected and enabled automatically but
|
||||
* the {@link #setUseJaf(boolean)} property may be set to false.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public class PathExtensionContentTypeResolver extends AbstractMappingContentTypeResolver {
|
||||
|
||||
private boolean useJaf = true;
|
||||
|
||||
private boolean ignoreUnknownExtensions = true;
|
||||
|
||||
|
||||
/**
|
||||
* Create an instance with the given map of file extensions and media types.
|
||||
*/
|
||||
public PathExtensionContentTypeResolver(Map<String, MediaType> mediaTypes) {
|
||||
super(mediaTypes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance without any mappings to start with. Mappings may be added
|
||||
* later on if any extensions are resolved through the Java Activation framework.
|
||||
*/
|
||||
public PathExtensionContentTypeResolver() {
|
||||
super(null);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Whether to use the Java Activation Framework to look up file extensions.
|
||||
* <p>By default this is set to "true" but depends on JAF being present.
|
||||
*/
|
||||
public void setUseJaf(boolean useJaf) {
|
||||
this.useJaf = useJaf;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to ignore requests with unknown file extension. Setting this to
|
||||
* {@code false} results in {@code HttpMediaTypeNotAcceptableException}.
|
||||
* <p>By default this is set to {@code true}.
|
||||
*/
|
||||
public void setIgnoreUnknownExtensions(boolean ignoreUnknownExtensions) {
|
||||
this.ignoreUnknownExtensions = ignoreUnknownExtensions;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected String extractKey(ServerWebExchange exchange) {
|
||||
String path = exchange.getRequest().getURI().getRawPath();
|
||||
String extension = UriUtils.extractFileExtension(path);
|
||||
return (StringUtils.hasText(extension)) ? extension.toLowerCase(Locale.ENGLISH) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MediaType handleNoMatch(String key) throws NotAcceptableStatusException {
|
||||
if (this.useJaf) {
|
||||
MediaType mediaType = MediaTypeFactory.getMediaType("file." + key);
|
||||
if (mediaType != null && !MediaType.APPLICATION_OCTET_STREAM.equals(mediaType)) {
|
||||
return mediaType;
|
||||
}
|
||||
}
|
||||
if (!this.ignoreUnknownExtensions) {
|
||||
throw new NotAcceptableStatusException(getAllMediaTypes());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A public method exposing the knowledge of the path extension resolver to
|
||||
* determine the media type for a given {@link Resource}. First it checks
|
||||
* the explicitly registered mappings and then falls back on JAF.
|
||||
* @param resource the resource
|
||||
* @return the MediaType for the extension, or {@code null} if none determined
|
||||
*/
|
||||
public MediaType resolveMediaTypeForResource(Resource resource) {
|
||||
Assert.notNull(resource, "Resource must not be null");
|
||||
MediaType mediaType = null;
|
||||
String filename = resource.getFilename();
|
||||
String extension = StringUtils.getFilenameExtension(filename);
|
||||
if (extension != null) {
|
||||
mediaType = getMediaType(extension);
|
||||
}
|
||||
if (mediaType == null) {
|
||||
mediaType = MediaTypeFactory.getMediaType(filename);
|
||||
}
|
||||
if (MediaType.APPLICATION_OCTET_STREAM.equals(mediaType)) {
|
||||
mediaType = null;
|
||||
}
|
||||
return mediaType;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.web.reactive.accept;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.server.NotAcceptableStatusException;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* Strategy for resolving the requested media types for a
|
||||
* {@code ServerWebExchange}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public interface RequestedContentTypeResolver {
|
||||
|
||||
/**
|
||||
* Resolve the given request to a list of requested media types. The returned
|
||||
* list is ordered by specificity first and by quality parameter second.
|
||||
*
|
||||
* @param exchange the current exchange
|
||||
* @return the requested media types or an empty list
|
||||
*
|
||||
* @throws NotAcceptableStatusException if the requested media types is invalid
|
||||
*/
|
||||
List<MediaType> resolveMediaTypes(ServerWebExchange exchange)
|
||||
throws NotAcceptableStatusException;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.web.reactive.accept;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
|
||||
/**
|
||||
* Factory to create a {@link CompositeContentTypeResolver} and configure it with
|
||||
* one or more {@link RequestedContentTypeResolver} instances with build style
|
||||
* methods. The following table shows methods, resulting strategy instances, and
|
||||
* if in use by default:
|
||||
*
|
||||
* <table>
|
||||
* <tr>
|
||||
* <th>Property Setter</th>
|
||||
* <th>Underlying Strategy</th>
|
||||
* <th>Default Setting</th>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>{@link #favorPathExtension}</td>
|
||||
* <td>{@link PathExtensionContentTypeResolver Path Extension resolver}</td>
|
||||
* <td>On</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>{@link #favorParameter}</td>
|
||||
* <td>{@link ParameterContentTypeResolver Parameter resolver}</td>
|
||||
* <td>Off</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>{@link #ignoreAcceptHeader}</td>
|
||||
* <td>{@link HeaderContentTypeResolver Header resolver}</td>
|
||||
* <td>Off</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>{@link #defaultContentType}</td>
|
||||
* <td>{@link FixedContentTypeResolver Fixed content resolver}</td>
|
||||
* <td>Not set</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>{@link #defaultContentTypeResolver}</td>
|
||||
* <td>{@link RequestedContentTypeResolver}</td>
|
||||
* <td>Not set</td>
|
||||
* </tr>
|
||||
* </table>
|
||||
*
|
||||
* <p>The order in which resolvers are configured is fixed. Config methods may
|
||||
* only turn individual resolvers on or off. If you need a custom order for any
|
||||
* reason simply instantiate {@code {@link CompositeContentTypeResolver}}
|
||||
* directly.
|
||||
*
|
||||
* <p>For the path extension and parameter resolvers you may explicitly add
|
||||
* {@link #mediaTypes(Map)}. This will be used to resolve path extensions or a
|
||||
* parameter value such as "json" to a media type such as "application/json".
|
||||
*
|
||||
* <p>The path extension strategy will also use the Java Activation framework
|
||||
* (JAF), if available, to resolve a path extension to a MediaType. You may
|
||||
* {@link #useJaf suppress} the use of JAF.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public class RequestedContentTypeResolverBuilder {
|
||||
|
||||
private boolean favorPathExtension = true;
|
||||
|
||||
private boolean favorParameter = false;
|
||||
|
||||
private boolean ignoreAcceptHeader = false;
|
||||
|
||||
private Map<String, MediaType> mediaTypes = new HashMap<>();
|
||||
|
||||
private boolean ignoreUnknownPathExtensions = true;
|
||||
|
||||
private Boolean useJaf;
|
||||
|
||||
private String parameterName = "format";
|
||||
|
||||
private RequestedContentTypeResolver contentTypeResolver;
|
||||
|
||||
|
||||
/**
|
||||
* Whether the path extension in the URL path should be used to determine
|
||||
* the requested media type.
|
||||
* <p>By default this is set to {@code true} in which case a request
|
||||
* for {@code /hotels.pdf} will be interpreted as a request for
|
||||
* {@code "application/pdf"} regardless of the 'Accept' header.
|
||||
*/
|
||||
public RequestedContentTypeResolverBuilder favorPathExtension(boolean favorPathExtension) {
|
||||
this.favorPathExtension = favorPathExtension;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a mapping from a key, extracted from a path extension or a query
|
||||
* parameter, to a MediaType. This is required in order for the parameter
|
||||
* strategy to work. Any extensions explicitly registered here are also
|
||||
* whitelisted for the purpose of Reflected File Download attack detection
|
||||
* (see Spring Framework reference documentation for more details on RFD
|
||||
* attack protection).
|
||||
* <p>The path extension strategy will also try to use JAF (if present) to
|
||||
* resolve path extensions. To change this behavior see {@link #useJaf}.
|
||||
* @param mediaTypes media type mappings
|
||||
*/
|
||||
public RequestedContentTypeResolverBuilder mediaTypes(Map<String, MediaType> mediaTypes) {
|
||||
if (!CollectionUtils.isEmpty(mediaTypes)) {
|
||||
for (Map.Entry<String, MediaType> entry : mediaTypes.entrySet()) {
|
||||
String extension = entry.getKey().toLowerCase(Locale.ENGLISH);
|
||||
this.mediaTypes.put(extension, entry.getValue());
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alternative to {@link #mediaTypes} to add a single mapping.
|
||||
*/
|
||||
public RequestedContentTypeResolverBuilder mediaType(String key, MediaType mediaType) {
|
||||
this.mediaTypes.put(key, mediaType);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to ignore requests with path extension that cannot be resolved
|
||||
* to any media type. Setting this to {@code false} will result in an
|
||||
* {@link org.springframework.web.HttpMediaTypeNotAcceptableException} if
|
||||
* there is no match.
|
||||
* <p>By default this is set to {@code true}.
|
||||
*/
|
||||
public RequestedContentTypeResolverBuilder ignoreUnknownPathExtensions(boolean ignore) {
|
||||
this.ignoreUnknownPathExtensions = ignore;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* When {@link #favorPathExtension favorPathExtension} is set, this
|
||||
* property determines whether to allow use of JAF (Java Activation Framework)
|
||||
* to resolve a path extension to a specific MediaType.
|
||||
* <p>By default this is not set in which case
|
||||
* {@code PathExtensionContentNegotiationStrategy} will use JAF if available.
|
||||
*/
|
||||
public RequestedContentTypeResolverBuilder useJaf(boolean useJaf) {
|
||||
this.useJaf = useJaf;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a request parameter ("format" by default) should be used to
|
||||
* determine the requested media type. For this option to work you must
|
||||
* register {@link #mediaTypes media type mappings}.
|
||||
* <p>By default this is set to {@code false}.
|
||||
* @see #parameterName
|
||||
*/
|
||||
public RequestedContentTypeResolverBuilder favorParameter(boolean favorParameter) {
|
||||
this.favorParameter = favorParameter;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the query parameter name to use when {@link #favorParameter} is on.
|
||||
* <p>The default parameter name is {@code "format"}.
|
||||
*/
|
||||
public RequestedContentTypeResolverBuilder parameterName(String parameterName) {
|
||||
Assert.notNull(parameterName, "parameterName is required");
|
||||
this.parameterName = parameterName;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to disable checking the 'Accept' request header.
|
||||
* <p>By default this value is set to {@code false}.
|
||||
*/
|
||||
public RequestedContentTypeResolverBuilder ignoreAcceptHeader(boolean ignoreAcceptHeader) {
|
||||
this.ignoreAcceptHeader = ignoreAcceptHeader;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default content type to use when no content type is requested.
|
||||
* <p>By default this is not set.
|
||||
* @see #defaultContentTypeResolver
|
||||
*/
|
||||
public RequestedContentTypeResolverBuilder defaultContentType(MediaType contentType) {
|
||||
this.contentTypeResolver = new FixedContentTypeResolver(contentType);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a custom {@link RequestedContentTypeResolver} to use to determine
|
||||
* the content type to use when no content type is requested.
|
||||
* <p>By default this is not set.
|
||||
* @see #defaultContentType
|
||||
*/
|
||||
public RequestedContentTypeResolverBuilder defaultContentTypeResolver(RequestedContentTypeResolver resolver) {
|
||||
this.contentTypeResolver = resolver;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public CompositeContentTypeResolver build() {
|
||||
List<RequestedContentTypeResolver> resolvers = new ArrayList<>();
|
||||
|
||||
if (this.favorPathExtension) {
|
||||
PathExtensionContentTypeResolver resolver = new PathExtensionContentTypeResolver(this.mediaTypes);
|
||||
resolver.setIgnoreUnknownExtensions(this.ignoreUnknownPathExtensions);
|
||||
if (this.useJaf != null) {
|
||||
resolver.setUseJaf(this.useJaf);
|
||||
}
|
||||
resolvers.add(resolver);
|
||||
}
|
||||
|
||||
if (this.favorParameter) {
|
||||
ParameterContentTypeResolver resolver = new ParameterContentTypeResolver(this.mediaTypes);
|
||||
resolver.setParameterName(this.parameterName);
|
||||
resolvers.add(resolver);
|
||||
}
|
||||
|
||||
if (!this.ignoreAcceptHeader) {
|
||||
resolvers.add(new HeaderContentTypeResolver());
|
||||
}
|
||||
|
||||
if (this.contentTypeResolver != null) {
|
||||
resolvers.add(this.contentTypeResolver);
|
||||
}
|
||||
|
||||
return new CompositeContentTypeResolver(resolvers);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* {@link org.springframework.web.reactive.accept.RequestedContentTypeResolver}
|
||||
* strategy and implementations to resolve the requested content type for a
|
||||
* given request.
|
||||
*/
|
||||
package org.springframework.web.reactive.accept;
|
||||
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
|
||||
/**
|
||||
* Assists with the creation of a {@link CorsConfiguration} instance mapped to
|
||||
* a path pattern. By default all origins, headers, and credentials for
|
||||
* {@code GET}, {@code HEAD}, and {@code POST} requests are allowed while the
|
||||
* max age is set to 30 minutes.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
* @see CorsRegistry
|
||||
*/
|
||||
public class CorsRegistration {
|
||||
|
||||
private final String pathPattern;
|
||||
|
||||
private final CorsConfiguration config;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new {@link CorsRegistration} that allows all origins, headers, and
|
||||
* credentials for {@code GET}, {@code HEAD}, and {@code POST} requests with
|
||||
* max age set to 1800 seconds (30 minutes) for the specified path.
|
||||
*
|
||||
* @param pathPattern the path that the CORS configuration should apply to;
|
||||
* exact path mapping URIs (such as {@code "/admin"}) are supported as well
|
||||
* as Ant-style path patterns (such as {@code "/admin/**"}).
|
||||
*/
|
||||
public CorsRegistration(String pathPattern) {
|
||||
this.pathPattern = pathPattern;
|
||||
this.config = new CorsConfiguration().applyPermitDefaultValues();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the origins to allow, e.g. {@code "http://domain1.com"}.
|
||||
* <p>The special value {@code "*"} allows all domains.
|
||||
* <p>By default all origins are allowed.
|
||||
*/
|
||||
public CorsRegistration allowedOrigins(String... origins) {
|
||||
this.config.setAllowedOrigins(new ArrayList<>(Arrays.asList(origins)));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the HTTP methods to allow, e.g. {@code "GET"}, {@code "POST"}, etc.
|
||||
* <p>The special value {@code "*"} allows all methods.
|
||||
* <p>By default "simple" methods {@code GET}, {@code HEAD}, and {@code POST}
|
||||
* are allowed.
|
||||
*/
|
||||
public CorsRegistration allowedMethods(String... methods) {
|
||||
this.config.setAllowedMethods(new ArrayList<>(Arrays.asList(methods)));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the list of headers that a pre-flight request can list as allowed
|
||||
* for use during an actual request.
|
||||
* <p>The special value {@code "*"} may be used to allow all headers.
|
||||
* <p>A header name is not required to be listed if it is one of:
|
||||
* {@code Cache-Control}, {@code Content-Language}, {@code Expires},
|
||||
* {@code Last-Modified}, or {@code Pragma} as per the CORS spec.
|
||||
* <p>By default all headers are allowed.
|
||||
*/
|
||||
public CorsRegistration allowedHeaders(String... headers) {
|
||||
this.config.setAllowedHeaders(new ArrayList<>(Arrays.asList(headers)));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the list of response headers other than "simple" headers, i.e.
|
||||
* {@code Cache-Control}, {@code Content-Language}, {@code Content-Type},
|
||||
* {@code Expires}, {@code Last-Modified}, or {@code Pragma}, that an
|
||||
* actual response might have and can be exposed.
|
||||
* <p>Note that {@code "*"} is not supported on this property.
|
||||
* <p>By default this is not set.
|
||||
*/
|
||||
public CorsRegistration exposedHeaders(String... headers) {
|
||||
this.config.setExposedHeaders(new ArrayList<>(Arrays.asList(headers)));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure how long in seconds the response from a pre-flight request
|
||||
* can be cached by clients.
|
||||
* <p>By default this is set to 1800 seconds (30 minutes).
|
||||
*/
|
||||
public CorsRegistration maxAge(long maxAge) {
|
||||
this.config.setMaxAge(maxAge);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether user credentials are supported.
|
||||
* <p>By default this is set to {@code true} in which case user credentials
|
||||
* are supported.
|
||||
*/
|
||||
public CorsRegistration allowCredentials(boolean allowCredentials) {
|
||||
this.config.setAllowCredentials(allowCredentials);
|
||||
return this;
|
||||
}
|
||||
|
||||
protected String getPathPattern() {
|
||||
return this.pathPattern;
|
||||
}
|
||||
|
||||
protected CorsConfiguration getCorsConfiguration() {
|
||||
return this.config;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
|
||||
/**
|
||||
* {@code CorsRegistry} assists with the registration of {@link CorsConfiguration}
|
||||
* mapped to a path pattern.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @since 5.0
|
||||
*/
|
||||
public class CorsRegistry {
|
||||
|
||||
private final List<CorsRegistration> registrations = new ArrayList<>();
|
||||
|
||||
|
||||
/**
|
||||
* Enable cross origin request handling for the specified path pattern.
|
||||
*
|
||||
* <p>Exact path mapping URIs (such as {@code "/admin"}) are supported as
|
||||
* well as Ant-style path patterns (such as {@code "/admin/**"}).
|
||||
*
|
||||
* <p>By default, all origins, all headers, credentials and {@code GET},
|
||||
* {@code HEAD}, and {@code POST} methods are allowed, and the max age
|
||||
* is set to 30 minutes.
|
||||
*/
|
||||
public CorsRegistration addMapping(String pathPattern) {
|
||||
CorsRegistration registration = new CorsRegistration(pathPattern);
|
||||
this.registrations.add(registration);
|
||||
return registration;
|
||||
}
|
||||
|
||||
protected Map<String, CorsConfiguration> getCorsConfigurations() {
|
||||
Map<String, CorsConfiguration> configs = new LinkedHashMap<>(this.registrations.size());
|
||||
for (CorsRegistration registration : this.registrations) {
|
||||
configs.put(registration.getPathPattern(), registration.getCorsConfiguration());
|
||||
}
|
||||
return configs;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.config;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.format.FormatterRegistry;
|
||||
import org.springframework.http.codec.HttpMessageReader;
|
||||
import org.springframework.http.codec.HttpMessageWriter;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.validation.MessageCodesResolver;
|
||||
import org.springframework.validation.Validator;
|
||||
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder;
|
||||
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver;
|
||||
|
||||
/**
|
||||
* A subclass of {@code WebReactiveConfigurationSupport} that detects and delegates
|
||||
* to all beans of type {@link WebReactiveConfigurer} allowing them to customize the
|
||||
* configuration provided by {@code WebReactiveConfigurationSupport}. This is the
|
||||
* class actually imported by {@link EnableWebReactive @EnableWebReactive}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @since 5.0
|
||||
*/
|
||||
@Configuration
|
||||
public class DelegatingWebReactiveConfiguration extends WebReactiveConfigurationSupport {
|
||||
|
||||
private final WebReactiveConfigurerComposite configurers = new WebReactiveConfigurerComposite();
|
||||
|
||||
@Autowired(required = false)
|
||||
public void setConfigurers(List<WebReactiveConfigurer> configurers) {
|
||||
if (!CollectionUtils.isEmpty(configurers)) {
|
||||
this.configurers.addWebReactiveConfigurers(configurers);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configureContentTypeResolver(RequestedContentTypeResolverBuilder builder) {
|
||||
this.configurers.configureContentTypeResolver(builder);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addCorsMappings(CorsRegistry registry) {
|
||||
this.configurers.addCorsMappings(registry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configurePathMatching(PathMatchConfigurer configurer) {
|
||||
this.configurers.configurePathMatching(configurer);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
this.configurers.addResourceHandlers(registry);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
|
||||
this.configurers.addArgumentResolvers(resolvers);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configureMessageReaders(List<HttpMessageReader<?>> messageReaders) {
|
||||
this.configurers.configureMessageReaders(messageReaders);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void extendMessageReaders(List<HttpMessageReader<?>> messageReaders) {
|
||||
this.configurers.extendMessageReaders(messageReaders);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addFormatters(FormatterRegistry registry) {
|
||||
this.configurers.addFormatters(registry);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Validator getValidator() {
|
||||
return this.configurers.getValidator().orElse(super.getValidator());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MessageCodesResolver getMessageCodesResolver() {
|
||||
return this.configurers.getMessageCodesResolver().orElse(super.getMessageCodesResolver());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configureMessageWriters(List<HttpMessageWriter<?>> messageWriters) {
|
||||
this.configurers.configureMessageWriters(messageWriters);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void extendMessageWriters(List<HttpMessageWriter<?>> messageWriters) {
|
||||
this.configurers.extendMessageWriters(messageWriters);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configureViewResolvers(ViewResolverRegistry registry) {
|
||||
this.configurers.configureViewResolvers(registry);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.config;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
/**
|
||||
* Adding this annotation to an {@code @Configuration} class imports the Spring Web
|
||||
* Reactive configuration from {@link WebReactiveConfigurationSupport}, e.g.:
|
||||
*
|
||||
* <pre class="code">
|
||||
* @Configuration
|
||||
* @EnableWebReactive
|
||||
* @ComponentScan(basePackageClasses = MyConfiguration.class)
|
||||
* public class MyConfiguration {
|
||||
*
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* <p>To customize the imported configuration implement
|
||||
* {@link WebReactiveConfigurer} and override individual methods as shown below:
|
||||
*
|
||||
* <pre class="code">
|
||||
* @Configuration
|
||||
* @EnableWebReactive
|
||||
* @ComponentScan(basePackageClasses = MyConfiguration.class)
|
||||
* public class MyConfiguration implements WebReactiveConfigurer {
|
||||
*
|
||||
* @Override
|
||||
* public void addFormatters(FormatterRegistry formatterRegistry) {
|
||||
* formatterRegistry.addConverter(new MyConverter());
|
||||
* }
|
||||
*
|
||||
* @Override
|
||||
* public void configureMessageWriters(List<HttpMessageWriter<?>> messageWriters) {
|
||||
* messageWriters.add(new MyHttpMessageWriter());
|
||||
* }
|
||||
*
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* <p><strong>Note:</strong> only one {@code @Configuration} class may have the
|
||||
* {@code @EnableWebReactive} annotation to import the Spring Web Reactive
|
||||
* configuration. There can however be multiple {@code @Configuration} classes
|
||||
* implementing {@code WebReactiveConfigurer} in order to customize the provided
|
||||
* configuration.
|
||||
*
|
||||
* <p>If {@link WebReactiveConfigurer} does not expose some more advanced setting
|
||||
* that needs to be configured consider removing the {@code @EnableWebReactive}
|
||||
* annotation and extending directly from {@link WebReactiveConfigurationSupport}
|
||||
* or {@link DelegatingWebReactiveConfiguration} if you still want to allow
|
||||
* {@link WebReactiveConfigurer} instances to customize the configuration.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
* @see WebReactiveConfigurer
|
||||
* @see WebReactiveConfigurationSupport
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
@Documented
|
||||
@Import(DelegatingWebReactiveConfiguration.class)
|
||||
public @interface EnableWebReactive {
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.config;
|
||||
|
||||
import org.springframework.util.PathMatcher;
|
||||
import org.springframework.web.server.support.HttpRequestPathHelper;
|
||||
|
||||
/**
|
||||
* Assist with configuring {@code HandlerMapping}'s with path matching options.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public class PathMatchConfigurer {
|
||||
|
||||
private Boolean suffixPatternMatch;
|
||||
|
||||
private Boolean trailingSlashMatch;
|
||||
|
||||
private Boolean registeredSuffixPatternMatch;
|
||||
|
||||
private HttpRequestPathHelper pathHelper;
|
||||
|
||||
private PathMatcher pathMatcher;
|
||||
|
||||
|
||||
/**
|
||||
* Whether to use suffix pattern match (".*") when matching patterns to
|
||||
* requests. If enabled a method mapped to "/users" also matches to "/users.*".
|
||||
* <p>By default this is set to {@code true}.
|
||||
* @see #registeredSuffixPatternMatch
|
||||
*/
|
||||
public PathMatchConfigurer setUseSuffixPatternMatch(Boolean suffixPatternMatch) {
|
||||
this.suffixPatternMatch = suffixPatternMatch;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to match to URLs irrespective of the presence of a trailing slash.
|
||||
* If enabled a method mapped to "/users" also matches to "/users/".
|
||||
* <p>The default value is {@code true}.
|
||||
*/
|
||||
public PathMatchConfigurer setUseTrailingSlashMatch(Boolean trailingSlashMatch) {
|
||||
this.trailingSlashMatch = trailingSlashMatch;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether suffix pattern matching should work only against path extensions
|
||||
* that are explicitly registered. This is generally recommended to reduce
|
||||
* ambiguity and to avoid issues such as when a "." (dot) appears in the path
|
||||
* for other reasons.
|
||||
* <p>By default this is set to "true".
|
||||
*/
|
||||
public PathMatchConfigurer setUseRegisteredSuffixPatternMatch(Boolean registeredSuffixPatternMatch) {
|
||||
this.registeredSuffixPatternMatch = registeredSuffixPatternMatch;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a {@code HttpRequestPathHelper} for the resolution of lookup paths.
|
||||
* <p>Default is {@code HttpRequestPathHelper}.
|
||||
*/
|
||||
public PathMatchConfigurer setPathHelper(HttpRequestPathHelper pathHelper) {
|
||||
this.pathHelper = pathHelper;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the PathMatcher for matching URL paths against registered URL patterns.
|
||||
* <p>Default is {@link org.springframework.util.AntPathMatcher AntPathMatcher}.
|
||||
*/
|
||||
public PathMatchConfigurer setPathMatcher(PathMatcher pathMatcher) {
|
||||
this.pathMatcher = pathMatcher;
|
||||
return this;
|
||||
}
|
||||
|
||||
protected Boolean isUseSuffixPatternMatch() {
|
||||
return this.suffixPatternMatch;
|
||||
}
|
||||
|
||||
protected Boolean isUseTrailingSlashMatch() {
|
||||
return this.trailingSlashMatch;
|
||||
}
|
||||
|
||||
protected Boolean isUseRegisteredSuffixPatternMatch() {
|
||||
return this.registeredSuffixPatternMatch;
|
||||
}
|
||||
|
||||
protected HttpRequestPathHelper getPathHelper() {
|
||||
return this.pathHelper;
|
||||
}
|
||||
|
||||
protected PathMatcher getPathMatcher() {
|
||||
return this.pathMatcher;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.cache.concurrent.ConcurrentMapCache;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.web.reactive.resource.CachingResourceResolver;
|
||||
import org.springframework.web.reactive.resource.CachingResourceTransformer;
|
||||
import org.springframework.web.reactive.resource.CssLinkResourceTransformer;
|
||||
import org.springframework.web.reactive.resource.PathResourceResolver;
|
||||
import org.springframework.web.reactive.resource.ResourceResolver;
|
||||
import org.springframework.web.reactive.resource.ResourceTransformer;
|
||||
import org.springframework.web.reactive.resource.VersionResourceResolver;
|
||||
import org.springframework.web.reactive.resource.WebJarsResourceResolver;
|
||||
|
||||
/**
|
||||
* Assists with the registration of resource resolvers and transformers.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public class ResourceChainRegistration {
|
||||
|
||||
private static final String DEFAULT_CACHE_NAME = "spring-resource-chain-cache";
|
||||
|
||||
private static final boolean isWebJarsAssetLocatorPresent = ClassUtils.isPresent(
|
||||
"org.webjars.WebJarAssetLocator", ResourceChainRegistration.class.getClassLoader());
|
||||
|
||||
|
||||
private final List<ResourceResolver> resolvers = new ArrayList<>(4);
|
||||
|
||||
private final List<ResourceTransformer> transformers = new ArrayList<>(4);
|
||||
|
||||
private boolean hasVersionResolver;
|
||||
|
||||
private boolean hasPathResolver;
|
||||
|
||||
private boolean hasCssLinkTransformer;
|
||||
|
||||
private boolean hasWebjarsResolver;
|
||||
|
||||
|
||||
public ResourceChainRegistration(boolean cacheResources) {
|
||||
this(cacheResources, cacheResources ? new ConcurrentMapCache(DEFAULT_CACHE_NAME) : null);
|
||||
}
|
||||
|
||||
public ResourceChainRegistration(boolean cacheResources, Cache cache) {
|
||||
Assert.isTrue(!cacheResources || cache != null, "'cache' is required when cacheResources=true");
|
||||
if (cacheResources) {
|
||||
this.resolvers.add(new CachingResourceResolver(cache));
|
||||
this.transformers.add(new CachingResourceTransformer(cache));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add a resource resolver to the chain.
|
||||
* @param resolver the resolver to add
|
||||
* @return the current instance for chained method invocation
|
||||
*/
|
||||
public ResourceChainRegistration addResolver(ResourceResolver resolver) {
|
||||
Assert.notNull(resolver, "The provided ResourceResolver should not be null");
|
||||
this.resolvers.add(resolver);
|
||||
if (resolver instanceof VersionResourceResolver) {
|
||||
this.hasVersionResolver = true;
|
||||
}
|
||||
else if (resolver instanceof PathResourceResolver) {
|
||||
this.hasPathResolver = true;
|
||||
}
|
||||
else if (resolver instanceof WebJarsResourceResolver) {
|
||||
this.hasWebjarsResolver = true;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a resource transformer to the chain.
|
||||
* @param transformer the transformer to add
|
||||
* @return the current instance for chained method invocation
|
||||
*/
|
||||
public ResourceChainRegistration addTransformer(ResourceTransformer transformer) {
|
||||
Assert.notNull(transformer, "The provided ResourceTransformer should not be null");
|
||||
this.transformers.add(transformer);
|
||||
if (transformer instanceof CssLinkResourceTransformer) {
|
||||
this.hasCssLinkTransformer = true;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
protected List<ResourceResolver> getResourceResolvers() {
|
||||
if (!this.hasPathResolver) {
|
||||
List<ResourceResolver> result = new ArrayList<>(this.resolvers);
|
||||
if (isWebJarsAssetLocatorPresent && !this.hasWebjarsResolver) {
|
||||
result.add(new WebJarsResourceResolver());
|
||||
}
|
||||
result.add(new PathResourceResolver());
|
||||
return result;
|
||||
}
|
||||
return this.resolvers;
|
||||
}
|
||||
|
||||
protected List<ResourceTransformer> getResourceTransformers() {
|
||||
if (this.hasVersionResolver && !this.hasCssLinkTransformer) {
|
||||
List<ResourceTransformer> result = new ArrayList<>(this.transformers);
|
||||
boolean hasTransformers = !this.transformers.isEmpty();
|
||||
boolean hasCaching = hasTransformers && this.transformers.get(0) instanceof CachingResourceTransformer;
|
||||
result.add(hasCaching ? 1 : 0, new CssLinkResourceTransformer());
|
||||
return result;
|
||||
}
|
||||
return this.transformers;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.http.CacheControl;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.reactive.resource.ResourceWebHandler;
|
||||
|
||||
/**
|
||||
* Assist with creating and configuring a static resources handler.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public class ResourceHandlerRegistration {
|
||||
|
||||
private final ResourceLoader resourceLoader;
|
||||
|
||||
private final String[] pathPatterns;
|
||||
|
||||
private final List<Resource> locations = new ArrayList<>();
|
||||
|
||||
private CacheControl cacheControl;
|
||||
|
||||
private ResourceChainRegistration resourceChainRegistration;
|
||||
|
||||
|
||||
/**
|
||||
* Create a {@link ResourceHandlerRegistration} instance.
|
||||
* @param resourceLoader a resource loader for turning a String location
|
||||
* into a {@link Resource}
|
||||
* @param pathPatterns one or more resource URL path patterns
|
||||
*/
|
||||
public ResourceHandlerRegistration(ResourceLoader resourceLoader, String... pathPatterns) {
|
||||
Assert.notEmpty(pathPatterns, "At least one path pattern is required for resource handling.");
|
||||
this.resourceLoader = resourceLoader;
|
||||
this.pathPatterns = pathPatterns;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add one or more resource locations from which to serve static content.
|
||||
* Each location must point to a valid directory. Multiple locations may
|
||||
* be specified as a comma-separated list, and the locations will be checked
|
||||
* for a given resource in the order specified.
|
||||
*
|
||||
* <p>For example, {{@code "/"},
|
||||
* {@code "classpath:/META-INF/public-web-resources/"}} allows resources to
|
||||
* be served both from the web application root and from any JAR on the
|
||||
* classpath that contains a {@code /META-INF/public-web-resources/} directory,
|
||||
* with resources in the web application root taking precedence.
|
||||
* @return the same {@link ResourceHandlerRegistration} instance, for
|
||||
* chained method invocation
|
||||
*/
|
||||
public ResourceHandlerRegistration addResourceLocations(String... resourceLocations) {
|
||||
for (String location : resourceLocations) {
|
||||
this.locations.add(resourceLoader.getResource(location));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the {@link CacheControl} which should be used
|
||||
* by the resource handler.
|
||||
*
|
||||
* @param cacheControl the CacheControl configuration to use
|
||||
* @return the same {@link ResourceHandlerRegistration} instance, for
|
||||
* chained method invocation
|
||||
*/
|
||||
public ResourceHandlerRegistration setCacheControl(CacheControl cacheControl) {
|
||||
this.cacheControl = cacheControl;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a chain of resource resolvers and transformers to use. This
|
||||
* can be useful, for example, to apply a version strategy to resource URLs.
|
||||
*
|
||||
* <p>If this method is not invoked, by default only a simple
|
||||
* {@code PathResourceResolver} is used in order to match URL paths to
|
||||
* resources under the configured locations.
|
||||
*
|
||||
* @param cacheResources whether to cache the result of resource resolution;
|
||||
* setting this to "true" is recommended for production (and "false" for
|
||||
* development, especially when applying a version strategy)
|
||||
* @return the same {@link ResourceHandlerRegistration} instance, for
|
||||
* chained method invocation
|
||||
*/
|
||||
public ResourceChainRegistration resourceChain(boolean cacheResources) {
|
||||
this.resourceChainRegistration = new ResourceChainRegistration(cacheResources);
|
||||
return this.resourceChainRegistration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a chain of resource resolvers and transformers to use. This
|
||||
* can be useful, for example, to apply a version strategy to resource URLs.
|
||||
*
|
||||
* <p>If this method is not invoked, by default only a simple
|
||||
* {@code PathResourceResolver} is used in order to match URL paths to
|
||||
* resources under the configured locations.
|
||||
*
|
||||
* @param cacheResources whether to cache the result of resource resolution;
|
||||
* setting this to "true" is recommended for production (and "false" for
|
||||
* development, especially when applying a version strategy
|
||||
* @param cache the cache to use for storing resolved and transformed resources;
|
||||
* by default a {@link org.springframework.cache.concurrent.ConcurrentMapCache}
|
||||
* is used. Since Resources aren't serializable and can be dependent on the
|
||||
* application host, one should not use a distributed cache but rather an
|
||||
* in-memory cache.
|
||||
* @return the same {@link ResourceHandlerRegistration} instance, for chained method invocation
|
||||
*/
|
||||
public ResourceChainRegistration resourceChain(boolean cacheResources, Cache cache) {
|
||||
this.resourceChainRegistration = new ResourceChainRegistration(cacheResources, cache);
|
||||
return this.resourceChainRegistration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL path patterns for the resource handler.
|
||||
*/
|
||||
protected String[] getPathPatterns() {
|
||||
return this.pathPatterns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link ResourceWebHandler} instance.
|
||||
*/
|
||||
protected ResourceWebHandler getRequestHandler() {
|
||||
ResourceWebHandler handler = new ResourceWebHandler();
|
||||
if (this.resourceChainRegistration != null) {
|
||||
handler.setResourceResolvers(this.resourceChainRegistration.getResourceResolvers());
|
||||
handler.setResourceTransformers(this.resourceChainRegistration.getResourceTransformers());
|
||||
}
|
||||
handler.setLocations(this.locations);
|
||||
if (this.cacheControl != null) {
|
||||
handler.setCacheControl(this.cacheControl);
|
||||
}
|
||||
return handler;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.beans.factory.BeanInitializationException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.reactive.accept.CompositeContentTypeResolver;
|
||||
import org.springframework.web.reactive.handler.AbstractHandlerMapping;
|
||||
import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping;
|
||||
import org.springframework.web.reactive.resource.ResourceWebHandler;
|
||||
import org.springframework.web.server.WebHandler;
|
||||
|
||||
/**
|
||||
* Stores registrations of resource handlers for serving static resources such
|
||||
* as images, css files and others through Spring MVC including setting cache
|
||||
* headers optimized for efficient loading in a web browser. Resources can be
|
||||
* served out of locations under web application root, from the classpath, and
|
||||
* others.
|
||||
*
|
||||
* <p>To create a resource handler, use {@link #addResourceHandler(String...)}
|
||||
* providing the URL path patterns for which the handler should be invoked to
|
||||
* serve static resources (e.g. {@code "/resources/**"}).
|
||||
*
|
||||
* <p>Then use additional methods on the returned
|
||||
* {@link ResourceHandlerRegistration} to add one or more locations from which
|
||||
* to serve static content from (e.g. {{@code "/"},
|
||||
* {@code "classpath:/META-INF/public-web-resources/"}}) or to specify a cache
|
||||
* period for served resources.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public class ResourceHandlerRegistry {
|
||||
|
||||
private final ApplicationContext applicationContext;
|
||||
|
||||
private final CompositeContentTypeResolver contentTypeResolver;
|
||||
|
||||
private final List<ResourceHandlerRegistration> registrations = new ArrayList<>();
|
||||
|
||||
private int order = Integer.MAX_VALUE -1;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new resource handler registry for the given application context.
|
||||
* @param applicationContext the Spring application context
|
||||
*/
|
||||
public ResourceHandlerRegistry(ApplicationContext applicationContext) {
|
||||
this(applicationContext, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new resource handler registry for the given application context.
|
||||
* @param applicationContext the Spring application context
|
||||
* @param contentTypeResolver the content type resolver to use
|
||||
*/
|
||||
public ResourceHandlerRegistry(ApplicationContext applicationContext,
|
||||
CompositeContentTypeResolver contentTypeResolver) {
|
||||
|
||||
Assert.notNull(applicationContext, "ApplicationContext is required");
|
||||
this.applicationContext = applicationContext;
|
||||
this.contentTypeResolver = contentTypeResolver;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add a resource handler for serving static resources based on the specified
|
||||
* URL path patterns. The handler will be invoked for every incoming request
|
||||
* that matches to one of the specified path patterns.
|
||||
*
|
||||
* <p>Patterns like {@code "/static/**"} or {@code "/css/{filename:\\w+\\.css}"}
|
||||
* are allowed. See {@link org.springframework.util.AntPathMatcher} for more
|
||||
* details on the syntax.
|
||||
* @return A {@link ResourceHandlerRegistration} to use to further
|
||||
* configure the registered resource handler
|
||||
*/
|
||||
public ResourceHandlerRegistration addResourceHandler(String... patterns) {
|
||||
ResourceHandlerRegistration registration =
|
||||
new ResourceHandlerRegistration(this.applicationContext, patterns);
|
||||
this.registrations.add(registration);
|
||||
return registration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a resource handler has already been registered for the given path pattern.
|
||||
*/
|
||||
public boolean hasMappingForPattern(String pathPattern) {
|
||||
for (ResourceHandlerRegistration registration : this.registrations) {
|
||||
if (Arrays.asList(registration.getPathPatterns()).contains(pathPattern)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the order to use for resource handling relative to other
|
||||
* {@code HandlerMapping}s configured in the Spring configuration.
|
||||
* <p>The default value used is {@code Integer.MAX_VALUE-1}.
|
||||
*/
|
||||
public ResourceHandlerRegistry setOrder(int order) {
|
||||
this.order = order;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a handler mapping with the mapped resource handlers; or {@code null} in case
|
||||
* of no registrations.
|
||||
*/
|
||||
protected AbstractHandlerMapping getHandlerMapping() {
|
||||
if (this.registrations.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<String, WebHandler> urlMap = new LinkedHashMap<>();
|
||||
for (ResourceHandlerRegistration registration : this.registrations) {
|
||||
for (String pathPattern : registration.getPathPatterns()) {
|
||||
ResourceWebHandler handler = registration.getRequestHandler();
|
||||
handler.setContentTypeResolver(this.contentTypeResolver);
|
||||
try {
|
||||
handler.afterPropertiesSet();
|
||||
handler.afterSingletonsInstantiated();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new BeanInitializationException("Failed to init ResourceHttpRequestHandler", ex);
|
||||
}
|
||||
urlMap.put(pathPattern, handler);
|
||||
}
|
||||
}
|
||||
|
||||
SimpleUrlHandlerMapping handlerMapping = new SimpleUrlHandlerMapping();
|
||||
handlerMapping.setOrder(this.order);
|
||||
handlerMapping.setUrlMap(urlMap);
|
||||
return handlerMapping;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.config;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.reactive.result.view.UrlBasedViewResolver;
|
||||
|
||||
/**
|
||||
* Assist with configuring properties of a {@link UrlBasedViewResolver}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public class UrlBasedViewResolverRegistration {
|
||||
|
||||
private final UrlBasedViewResolver viewResolver;
|
||||
|
||||
|
||||
public UrlBasedViewResolverRegistration(UrlBasedViewResolver viewResolver) {
|
||||
Assert.notNull(viewResolver, "ViewResolver must not be null");
|
||||
this.viewResolver = viewResolver;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the prefix that gets prepended to view names when building a URL.
|
||||
* @see UrlBasedViewResolver#setPrefix
|
||||
*/
|
||||
public UrlBasedViewResolverRegistration prefix(String prefix) {
|
||||
this.viewResolver.setPrefix(prefix);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the suffix that gets appended to view names when building a URL.
|
||||
* @see UrlBasedViewResolver#setSuffix
|
||||
*/
|
||||
public UrlBasedViewResolverRegistration suffix(String suffix) {
|
||||
this.viewResolver.setSuffix(suffix);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the view class that should be used to create views.
|
||||
* @see UrlBasedViewResolver#setViewClass
|
||||
*/
|
||||
public UrlBasedViewResolverRegistration viewClass(Class<?> viewClass) {
|
||||
this.viewResolver.setViewClass(viewClass);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the view names (or name patterns) that can be handled by this view
|
||||
* resolver. View names can contain simple wildcards such that 'my*', '*Report'
|
||||
* and '*Repo*' will all match the view name 'myReport'.
|
||||
* @see UrlBasedViewResolver#setViewNames
|
||||
*/
|
||||
public UrlBasedViewResolverRegistration viewNames(String... viewNames) {
|
||||
this.viewResolver.setViewNames(viewNames);
|
||||
return this;
|
||||
}
|
||||
|
||||
protected UrlBasedViewResolver getViewResolver() {
|
||||
return this.viewResolver;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.beans.factory.BeanFactoryUtils;
|
||||
import org.springframework.beans.factory.BeanInitializationException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.web.reactive.result.view.HttpMessageWriterView;
|
||||
import org.springframework.web.reactive.result.view.UrlBasedViewResolver;
|
||||
import org.springframework.web.reactive.result.view.View;
|
||||
import org.springframework.web.reactive.result.view.ViewResolver;
|
||||
import org.springframework.web.reactive.result.view.freemarker.FreeMarkerConfigurer;
|
||||
import org.springframework.web.reactive.result.view.freemarker.FreeMarkerViewResolver;
|
||||
|
||||
/**
|
||||
* Assist with the configuration of a chain of {@link ViewResolver}'s supporting
|
||||
* different template mechanisms.
|
||||
*
|
||||
* <p>In addition, you can also configure {@link #defaultViews(View...)
|
||||
* defaultViews} for rendering according to the requested content type, e.g.
|
||||
* JSON, XML, etc.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public class ViewResolverRegistry {
|
||||
|
||||
private final ApplicationContext applicationContext;
|
||||
|
||||
private final List<ViewResolver> viewResolvers = new ArrayList<>(4);
|
||||
|
||||
private final List<View> defaultViews = new ArrayList<>(4);
|
||||
|
||||
private Integer order;
|
||||
|
||||
|
||||
public ViewResolverRegistry(ApplicationContext applicationContext) {
|
||||
Assert.notNull(applicationContext, "ApplicationContext must not be null");
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Register a {@code FreeMarkerViewResolver} with a ".ftl" suffix.
|
||||
* <p><strong>Note</strong> that you must also configure FreeMarker by
|
||||
* adding a {@link FreeMarkerConfigurer} bean.
|
||||
*/
|
||||
public UrlBasedViewResolverRegistration freeMarker() {
|
||||
if (this.applicationContext != null && !hasBeanOfType(FreeMarkerConfigurer.class)) {
|
||||
throw new BeanInitializationException("In addition to a FreeMarker view resolver " +
|
||||
"there must also be a single FreeMarkerConfig bean in this web application context " +
|
||||
"(or its parent): FreeMarkerConfigurer is the usual implementation. " +
|
||||
"This bean may be given any name.");
|
||||
}
|
||||
FreeMarkerRegistration registration = new FreeMarkerRegistration();
|
||||
UrlBasedViewResolver resolver = registration.getViewResolver();
|
||||
resolver.setApplicationContext(this.applicationContext);
|
||||
this.viewResolvers.add(resolver);
|
||||
return registration;
|
||||
}
|
||||
|
||||
protected boolean hasBeanOfType(Class<?> beanType) {
|
||||
return !ObjectUtils.isEmpty(BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
|
||||
this.applicationContext, beanType, false, false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a {@link ViewResolver} bean instance. This may be useful to
|
||||
* configure a 3rd party resolver implementation or as an alternative to
|
||||
* other registration methods in this class when they don't expose some
|
||||
* more advanced property that needs to be set.
|
||||
*/
|
||||
public void viewResolver(ViewResolver viewResolver) {
|
||||
this.viewResolvers.add(viewResolver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default views associated with any view name and selected based on the
|
||||
* best match for the requested content type.
|
||||
* <p>Use {@link HttpMessageWriterView
|
||||
* HttpMessageWriterView} to adapt and use any existing
|
||||
* {@code HttpMessageWriter} (e.g. JSON, XML) as a {@code View}.
|
||||
*/
|
||||
public void defaultViews(View... defaultViews) {
|
||||
this.defaultViews.addAll(Arrays.asList(defaultViews));
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether any view resolvers have been registered.
|
||||
*/
|
||||
public boolean hasRegistrations() {
|
||||
return (!this.viewResolvers.isEmpty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the order for the
|
||||
* {@link org.springframework.web.reactive.result.view.ViewResolutionResultHandler
|
||||
* ViewResolutionResultHandler}.
|
||||
* <p>By default this property is not set, which means the result handler is
|
||||
* ordered at {@link Ordered#LOWEST_PRECEDENCE}.
|
||||
*/
|
||||
public void order(int order) {
|
||||
this.order = order;
|
||||
}
|
||||
|
||||
protected int getOrder() {
|
||||
return (this.order != null ? this.order : Ordered.LOWEST_PRECEDENCE);
|
||||
}
|
||||
|
||||
protected List<ViewResolver> getViewResolvers() {
|
||||
return this.viewResolvers;
|
||||
}
|
||||
|
||||
protected List<View> getDefaultViews() {
|
||||
return this.defaultViews;
|
||||
}
|
||||
|
||||
|
||||
private static class FreeMarkerRegistration extends UrlBasedViewResolverRegistration {
|
||||
|
||||
public FreeMarkerRegistration() {
|
||||
super(new FreeMarkerViewResolver());
|
||||
getViewResolver().setSuffix(".ftl");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,550 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.BeanInitializationException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.ReactiveAdapterRegistry;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.core.codec.ByteArrayDecoder;
|
||||
import org.springframework.core.codec.ByteArrayEncoder;
|
||||
import org.springframework.core.codec.ByteBufferDecoder;
|
||||
import org.springframework.core.codec.ByteBufferEncoder;
|
||||
import org.springframework.core.codec.CharSequenceEncoder;
|
||||
import org.springframework.core.codec.DataBufferDecoder;
|
||||
import org.springframework.core.codec.DataBufferEncoder;
|
||||
import org.springframework.core.codec.Encoder;
|
||||
import org.springframework.core.codec.ResourceDecoder;
|
||||
import org.springframework.core.codec.StringDecoder;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.format.Formatter;
|
||||
import org.springframework.format.FormatterRegistry;
|
||||
import org.springframework.format.support.DefaultFormattingConversionService;
|
||||
import org.springframework.format.support.FormattingConversionService;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.DecoderHttpMessageReader;
|
||||
import org.springframework.http.codec.EncoderHttpMessageWriter;
|
||||
import org.springframework.http.codec.HttpMessageReader;
|
||||
import org.springframework.http.codec.HttpMessageWriter;
|
||||
import org.springframework.http.codec.Jackson2ServerHttpMessageReader;
|
||||
import org.springframework.http.codec.Jackson2ServerHttpMessageWriter;
|
||||
import org.springframework.http.codec.ResourceHttpMessageWriter;
|
||||
import org.springframework.http.codec.ServerSentEventHttpMessageWriter;
|
||||
import org.springframework.http.codec.json.Jackson2JsonDecoder;
|
||||
import org.springframework.http.codec.json.Jackson2JsonEncoder;
|
||||
import org.springframework.http.codec.xml.Jaxb2XmlDecoder;
|
||||
import org.springframework.http.codec.xml.Jaxb2XmlEncoder;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.validation.Errors;
|
||||
import org.springframework.validation.MessageCodesResolver;
|
||||
import org.springframework.validation.Validator;
|
||||
import org.springframework.web.bind.WebDataBinder;
|
||||
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.reactive.DispatcherHandler;
|
||||
import org.springframework.web.reactive.HandlerMapping;
|
||||
import org.springframework.web.reactive.accept.CompositeContentTypeResolver;
|
||||
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder;
|
||||
import org.springframework.web.reactive.handler.AbstractHandlerMapping;
|
||||
import org.springframework.web.reactive.result.SimpleHandlerAdapter;
|
||||
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter;
|
||||
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping;
|
||||
import org.springframework.web.reactive.result.method.annotation.ResponseBodyResultHandler;
|
||||
import org.springframework.web.reactive.result.method.annotation.ResponseEntityResultHandler;
|
||||
import org.springframework.web.reactive.result.view.ViewResolutionResultHandler;
|
||||
import org.springframework.web.reactive.result.view.ViewResolver;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebExceptionHandler;
|
||||
import org.springframework.web.server.handler.ResponseStatusExceptionHandler;
|
||||
|
||||
/**
|
||||
* The main class for Spring Web Reactive configuration.
|
||||
*
|
||||
* <p>Import directly or extend and override protected methods to customize.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public class WebReactiveConfigurationSupport implements ApplicationContextAware {
|
||||
|
||||
private static final boolean jackson2Present =
|
||||
ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper",
|
||||
WebReactiveConfigurationSupport.class.getClassLoader()) &&
|
||||
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator",
|
||||
WebReactiveConfigurationSupport.class.getClassLoader());
|
||||
|
||||
private static final boolean jaxb2Present =
|
||||
ClassUtils.isPresent("javax.xml.bind.Binder", WebReactiveConfigurationSupport.class.getClassLoader());
|
||||
|
||||
|
||||
private Map<String, CorsConfiguration> corsConfigurations;
|
||||
|
||||
private PathMatchConfigurer pathMatchConfigurer;
|
||||
|
||||
private List<HttpMessageReader<?>> messageReaders;
|
||||
|
||||
private List<HttpMessageWriter<?>> messageWriters;
|
||||
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
protected ApplicationContext getApplicationContext() {
|
||||
return this.applicationContext;
|
||||
}
|
||||
|
||||
|
||||
@Bean
|
||||
public DispatcherHandler webHandler() {
|
||||
return new DispatcherHandler();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(0)
|
||||
public WebExceptionHandler responseStatusExceptionHandler() {
|
||||
return new ResponseStatusExceptionHandler();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
|
||||
RequestMappingHandlerMapping mapping = createRequestMappingHandlerMapping();
|
||||
mapping.setOrder(0);
|
||||
mapping.setContentTypeResolver(webReactiveContentTypeResolver());
|
||||
mapping.setCorsConfigurations(getCorsConfigurations());
|
||||
|
||||
PathMatchConfigurer configurer = getPathMatchConfigurer();
|
||||
if (configurer.isUseSuffixPatternMatch() != null) {
|
||||
mapping.setUseSuffixPatternMatch(configurer.isUseSuffixPatternMatch());
|
||||
}
|
||||
if (configurer.isUseRegisteredSuffixPatternMatch() != null) {
|
||||
mapping.setUseRegisteredSuffixPatternMatch(configurer.isUseRegisteredSuffixPatternMatch());
|
||||
}
|
||||
if (configurer.isUseTrailingSlashMatch() != null) {
|
||||
mapping.setUseTrailingSlashMatch(configurer.isUseTrailingSlashMatch());
|
||||
}
|
||||
if (configurer.getPathMatcher() != null) {
|
||||
mapping.setPathMatcher(configurer.getPathMatcher());
|
||||
}
|
||||
if (configurer.getPathHelper() != null) {
|
||||
mapping.setPathHelper(configurer.getPathHelper());
|
||||
}
|
||||
|
||||
return mapping;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override to plug a sub-class of {@link RequestMappingHandlerMapping}.
|
||||
*/
|
||||
protected RequestMappingHandlerMapping createRequestMappingHandlerMapping() {
|
||||
return new RequestMappingHandlerMapping();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CompositeContentTypeResolver webReactiveContentTypeResolver() {
|
||||
RequestedContentTypeResolverBuilder builder = new RequestedContentTypeResolverBuilder();
|
||||
builder.mediaTypes(getDefaultMediaTypeMappings());
|
||||
configureContentTypeResolver(builder);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Override to configure media type mappings.
|
||||
* @see RequestedContentTypeResolverBuilder#mediaTypes(Map)
|
||||
*/
|
||||
protected Map<String, MediaType> getDefaultMediaTypeMappings() {
|
||||
Map<String, MediaType> map = new HashMap<>();
|
||||
if (jackson2Present) {
|
||||
map.put("json", MediaType.APPLICATION_JSON);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override to configure how the requested content type is resolved.
|
||||
*/
|
||||
protected void configureContentTypeResolver(RequestedContentTypeResolverBuilder builder) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for building the global CORS configuration. This method is final.
|
||||
* Use {@link #addCorsMappings(CorsRegistry)} to customize the CORS conifg.
|
||||
*/
|
||||
protected final Map<String, CorsConfiguration> getCorsConfigurations() {
|
||||
if (this.corsConfigurations == null) {
|
||||
CorsRegistry registry = new CorsRegistry();
|
||||
addCorsMappings(registry);
|
||||
this.corsConfigurations = registry.getCorsConfigurations();
|
||||
}
|
||||
return this.corsConfigurations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override this method to configure cross origin requests processing.
|
||||
* @see CorsRegistry
|
||||
*/
|
||||
protected void addCorsMappings(CorsRegistry registry) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for building the {@link PathMatchConfigurer}. This method is
|
||||
* final, use {@link #configurePathMatching} to customize path matching.
|
||||
*/
|
||||
protected final PathMatchConfigurer getPathMatchConfigurer() {
|
||||
if (this.pathMatchConfigurer == null) {
|
||||
this.pathMatchConfigurer = new PathMatchConfigurer();
|
||||
configurePathMatching(this.pathMatchConfigurer);
|
||||
}
|
||||
return this.pathMatchConfigurer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override to configure path matching options.
|
||||
*/
|
||||
public void configurePathMatching(PathMatchConfigurer configurer) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a handler mapping ordered at Integer.MAX_VALUE-1 with mapped
|
||||
* resource handlers. To configure resource handling, override
|
||||
* {@link #addResourceHandlers}.
|
||||
*/
|
||||
@Bean
|
||||
public HandlerMapping resourceHandlerMapping() {
|
||||
ResourceHandlerRegistry registry =
|
||||
new ResourceHandlerRegistry(this.applicationContext, webReactiveContentTypeResolver());
|
||||
addResourceHandlers(registry);
|
||||
|
||||
AbstractHandlerMapping handlerMapping = registry.getHandlerMapping();
|
||||
if (handlerMapping != null) {
|
||||
PathMatchConfigurer pathMatchConfigurer = getPathMatchConfigurer();
|
||||
if (pathMatchConfigurer.getPathMatcher() != null) {
|
||||
handlerMapping.setPathMatcher(pathMatchConfigurer.getPathMatcher());
|
||||
}
|
||||
if (pathMatchConfigurer.getPathHelper() != null) {
|
||||
handlerMapping.setPathHelper(pathMatchConfigurer.getPathHelper());
|
||||
}
|
||||
}
|
||||
else {
|
||||
handlerMapping = new EmptyHandlerMapping();
|
||||
}
|
||||
return handlerMapping;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override this method to add resource handlers for serving static resources.
|
||||
* @see ResourceHandlerRegistry
|
||||
*/
|
||||
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
|
||||
RequestMappingHandlerAdapter adapter = createRequestMappingHandlerAdapter();
|
||||
adapter.setMessageReaders(getMessageReaders());
|
||||
adapter.setWebBindingInitializer(getConfigurableWebBindingInitializer());
|
||||
adapter.setReactiveAdapterRegistry(webReactiveAdapterRegistry());
|
||||
|
||||
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();
|
||||
addArgumentResolvers(resolvers);
|
||||
if (!resolvers.isEmpty()) {
|
||||
adapter.setCustomArgumentResolvers(resolvers);
|
||||
}
|
||||
|
||||
return adapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override to plug a sub-class of {@link RequestMappingHandlerAdapter}.
|
||||
*/
|
||||
protected RequestMappingHandlerAdapter createRequestMappingHandlerAdapter() {
|
||||
return new RequestMappingHandlerAdapter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide custom argument resolvers without overriding the built-in ones.
|
||||
*/
|
||||
protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Main method to access message readers to use for decoding
|
||||
* controller method arguments with.
|
||||
* <p>Use {@link #configureMessageReaders} to configure the list or
|
||||
* {@link #extendMessageReaders} to add in addition to the default ones.
|
||||
*/
|
||||
protected final List<HttpMessageReader<?>> getMessageReaders() {
|
||||
if (this.messageReaders == null) {
|
||||
this.messageReaders = new ArrayList<>();
|
||||
configureMessageReaders(this.messageReaders);
|
||||
if (this.messageReaders.isEmpty()) {
|
||||
addDefaultHttpMessageReaders(this.messageReaders);
|
||||
}
|
||||
extendMessageReaders(this.messageReaders);
|
||||
}
|
||||
return this.messageReaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override to configure the message readers to use for decoding
|
||||
* controller method arguments.
|
||||
* <p>If no message readres are specified, default will be added via
|
||||
* {@link #addDefaultHttpMessageReaders}.
|
||||
* @param messageReaders a list to add message readers to, initially an empty
|
||||
*/
|
||||
protected void configureMessageReaders(List<HttpMessageReader<?>> messageReaders) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds default converters that sub-classes can call from
|
||||
* {@link #configureMessageReaders(List)} for {@code byte[]},
|
||||
* {@code ByteBuffer}, {@code String}, {@code Resource}, JAXB2, and Jackson
|
||||
* (if present on the classpath).
|
||||
*/
|
||||
protected final void addDefaultHttpMessageReaders(List<HttpMessageReader<?>> readers) {
|
||||
readers.add(new DecoderHttpMessageReader<>(new ByteArrayDecoder()));
|
||||
readers.add(new DecoderHttpMessageReader<>(new ByteBufferDecoder()));
|
||||
readers.add(new DecoderHttpMessageReader<>(new DataBufferDecoder()));
|
||||
readers.add(new DecoderHttpMessageReader<>(new StringDecoder()));
|
||||
readers.add(new DecoderHttpMessageReader<>(new ResourceDecoder()));
|
||||
if (jaxb2Present) {
|
||||
readers.add(new DecoderHttpMessageReader<>(new Jaxb2XmlDecoder()));
|
||||
}
|
||||
if (jackson2Present) {
|
||||
readers.add(new Jackson2ServerHttpMessageReader(
|
||||
new DecoderHttpMessageReader<>(new Jackson2JsonDecoder())));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Override this to modify the list of message readers after it has been
|
||||
* configured, for example to add some in addition to the default ones.
|
||||
*/
|
||||
protected void extendMessageReaders(List<HttpMessageReader<?>> messageReaders) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the {@link ConfigurableWebBindingInitializer} to use for
|
||||
* initializing all {@link WebDataBinder} instances.
|
||||
*/
|
||||
protected ConfigurableWebBindingInitializer getConfigurableWebBindingInitializer() {
|
||||
ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
|
||||
initializer.setConversionService(webReactiveConversionService());
|
||||
initializer.setValidator(webReactiveValidator());
|
||||
initializer.setMessageCodesResolver(getMessageCodesResolver());
|
||||
return initializer;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public FormattingConversionService webReactiveConversionService() {
|
||||
FormattingConversionService service = new DefaultFormattingConversionService();
|
||||
addFormatters(service);
|
||||
return service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override to add custom {@link Converter}s and {@link Formatter}s.
|
||||
*/
|
||||
protected void addFormatters(FormatterRegistry registry) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@link ReactiveAdapterRegistry} to adapting reactive types.
|
||||
*/
|
||||
@Bean
|
||||
public ReactiveAdapterRegistry webReactiveAdapterRegistry() {
|
||||
return new ReactiveAdapterRegistry();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a global {@link Validator} instance for example for validating
|
||||
* {@code @RequestBody} method arguments.
|
||||
* <p>Delegates to {@link #getValidator()} first. If that returns {@code null}
|
||||
* checks the classpath for the presence of a JSR-303 implementations
|
||||
* before creating a {@code OptionalValidatorFactoryBean}. If a JSR-303
|
||||
* implementation is not available, a "no-op" {@link Validator} is returned.
|
||||
*/
|
||||
@Bean
|
||||
public Validator webReactiveValidator() {
|
||||
Validator validator = getValidator();
|
||||
if (validator == null) {
|
||||
if (ClassUtils.isPresent("javax.validation.Validator", getClass().getClassLoader())) {
|
||||
Class<?> clazz;
|
||||
try {
|
||||
String name = "org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean";
|
||||
clazz = ClassUtils.forName(name, getClass().getClassLoader());
|
||||
}
|
||||
catch (ClassNotFoundException ex) {
|
||||
throw new BeanInitializationException("Could not find default validator class", ex);
|
||||
}
|
||||
catch (LinkageError ex) {
|
||||
throw new BeanInitializationException("Could not load default validator class", ex);
|
||||
}
|
||||
validator = (Validator) BeanUtils.instantiateClass(clazz);
|
||||
}
|
||||
else {
|
||||
validator = new NoOpValidator();
|
||||
}
|
||||
}
|
||||
return validator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override this method to provide a custom {@link Validator}.
|
||||
*/
|
||||
protected Validator getValidator() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override this method to provide a custom {@link MessageCodesResolver}.
|
||||
*/
|
||||
protected MessageCodesResolver getMessageCodesResolver() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SimpleHandlerAdapter simpleHandlerAdapter() {
|
||||
return new SimpleHandlerAdapter();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ResponseEntityResultHandler responseEntityResultHandler() {
|
||||
return new ResponseEntityResultHandler(
|
||||
getMessageWriters(), webReactiveContentTypeResolver(), webReactiveAdapterRegistry());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ResponseBodyResultHandler responseBodyResultHandler() {
|
||||
return new ResponseBodyResultHandler(
|
||||
getMessageWriters(), webReactiveContentTypeResolver(), webReactiveAdapterRegistry());
|
||||
}
|
||||
|
||||
/**
|
||||
* Main method to access message writers to use for encoding return values.
|
||||
* <p>Use {@link #configureMessageWriters(List)} to configure the list or
|
||||
* {@link #extendMessageWriters(List)} to add in addition to the default ones.
|
||||
*/
|
||||
protected final List<HttpMessageWriter<?>> getMessageWriters() {
|
||||
if (this.messageWriters == null) {
|
||||
this.messageWriters = new ArrayList<>();
|
||||
configureMessageWriters(this.messageWriters);
|
||||
if (this.messageWriters.isEmpty()) {
|
||||
addDefaultHttpMessageWriters(this.messageWriters);
|
||||
}
|
||||
extendMessageWriters(this.messageWriters);
|
||||
}
|
||||
return this.messageWriters;
|
||||
}
|
||||
/**
|
||||
* Override to configure the message writers to use for encoding
|
||||
* return values.
|
||||
* <p>If no message readers are specified, default will be added via
|
||||
* {@link #addDefaultHttpMessageWriters}.
|
||||
* @param messageWriters a list to add message writers to, initially an empty
|
||||
*/
|
||||
protected void configureMessageWriters(List<HttpMessageWriter<?>> messageWriters) {
|
||||
}
|
||||
/**
|
||||
* Adds default converters that sub-classes can call from
|
||||
* {@link #configureMessageWriters(List)}.
|
||||
*/
|
||||
protected final void addDefaultHttpMessageWriters(List<HttpMessageWriter<?>> writers) {
|
||||
List<Encoder<?>> sseDataEncoders = new ArrayList<>();
|
||||
writers.add(new EncoderHttpMessageWriter<>(new ByteArrayEncoder()));
|
||||
writers.add(new EncoderHttpMessageWriter<>(new ByteBufferEncoder()));
|
||||
writers.add(new EncoderHttpMessageWriter<>(new DataBufferEncoder()));
|
||||
writers.add(new EncoderHttpMessageWriter<>(new CharSequenceEncoder()));
|
||||
writers.add(new ResourceHttpMessageWriter());
|
||||
if (jaxb2Present) {
|
||||
writers.add(new EncoderHttpMessageWriter<>(new Jaxb2XmlEncoder()));
|
||||
}
|
||||
if (jackson2Present) {
|
||||
Jackson2JsonEncoder encoder = new Jackson2JsonEncoder();
|
||||
writers.add(new Jackson2ServerHttpMessageWriter(encoder));
|
||||
sseDataEncoders.add(encoder);
|
||||
HttpMessageWriter<Object> writer = new ServerSentEventHttpMessageWriter(sseDataEncoders);
|
||||
writers.add(new Jackson2ServerHttpMessageWriter(writer));
|
||||
}
|
||||
else {
|
||||
writers.add(new ServerSentEventHttpMessageWriter(sseDataEncoders));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Override this to modify the list of message writers after it has been
|
||||
* configured, for example to add some in addition to the default ones.
|
||||
*/
|
||||
protected void extendMessageWriters(List<HttpMessageWriter<?>> messageWriters) {
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ViewResolutionResultHandler viewResolutionResultHandler() {
|
||||
ViewResolverRegistry registry = new ViewResolverRegistry(getApplicationContext());
|
||||
configureViewResolvers(registry);
|
||||
List<ViewResolver> resolvers = registry.getViewResolvers();
|
||||
ViewResolutionResultHandler handler = new ViewResolutionResultHandler(
|
||||
resolvers, webReactiveContentTypeResolver(), webReactiveAdapterRegistry());
|
||||
handler.setDefaultViews(registry.getDefaultViews());
|
||||
handler.setOrder(registry.getOrder());
|
||||
return handler;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure view resolution for supporting template engines.
|
||||
* @see ViewResolverRegistry
|
||||
*/
|
||||
protected void configureViewResolvers(ViewResolverRegistry registry) {
|
||||
}
|
||||
|
||||
|
||||
private static final class EmptyHandlerMapping extends AbstractHandlerMapping {
|
||||
|
||||
@Override
|
||||
public Mono<Object> getHandlerInternal(ServerWebExchange exchange) {
|
||||
return Mono.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class NoOpValidator implements Validator {
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> clazz) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate(Object target, Errors errors) {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.config;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.format.Formatter;
|
||||
import org.springframework.format.FormatterRegistry;
|
||||
import org.springframework.http.codec.HttpMessageReader;
|
||||
import org.springframework.http.codec.HttpMessageWriter;
|
||||
import org.springframework.validation.MessageCodesResolver;
|
||||
import org.springframework.validation.Validator;
|
||||
import org.springframework.web.reactive.accept.CompositeContentTypeResolver;
|
||||
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
|
||||
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder;
|
||||
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver;
|
||||
|
||||
/**
|
||||
* Defines callback methods to customize the configuration for Web Reactive
|
||||
* applications enabled via {@code @EnableWebReactive}.
|
||||
*
|
||||
* <p>{@code @EnableWebReactive}-annotated configuration classes may implement
|
||||
* this interface to be called back and given a chance to customize the
|
||||
* default configuration. Consider implementing this interface and
|
||||
* overriding the relevant methods for your needs.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public interface WebReactiveConfigurer {
|
||||
|
||||
/**
|
||||
* Configure how the content type requested for the response is resolved.
|
||||
* <p>The given builder will create a composite of multiple
|
||||
* {@link RequestedContentTypeResolver}s, each defining a way to resolve
|
||||
* the the requested content type (accept HTTP header, path extension,
|
||||
* parameter, etc).
|
||||
* @param builder factory that creates a {@link CompositeContentTypeResolver}
|
||||
*/
|
||||
default void configureContentTypeResolver(RequestedContentTypeResolverBuilder builder) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure cross origin requests processing.
|
||||
* @see CorsRegistry
|
||||
*/
|
||||
default void addCorsMappings(CorsRegistry registry) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure path matching options.
|
||||
* <p>The given configurer assists with configuring
|
||||
* {@code HandlerMapping}s with path matching options.
|
||||
* @param configurer the {@link PathMatchConfigurer} instance
|
||||
*/
|
||||
default void configurePathMatching(PathMatchConfigurer configurer) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Add resource handlers for serving static resources.
|
||||
* @see ResourceHandlerRegistry
|
||||
*/
|
||||
default void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide custom controller method argument resolvers. Such resolvers do
|
||||
* not override and will be invoked after the built-in ones.
|
||||
* @param resolvers a list of resolvers to add
|
||||
*/
|
||||
default void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the message readers to use for decoding the request body where
|
||||
* {@code @RequestBody} and {@code HttpEntity} controller method arguments
|
||||
* are used. If none are specified, default ones are added based on
|
||||
* {@link WebReactiveConfigurationSupport#addDefaultHttpMessageReaders}.
|
||||
* <p>See {@link #extendMessageReaders(List)} for adding readers
|
||||
* in addition to the default ones.
|
||||
* @param readers an empty list to add message readers to
|
||||
*/
|
||||
default void configureMessageReaders(List<HttpMessageReader<?>> readers) {
|
||||
}
|
||||
|
||||
/**
|
||||
* An alternative to {@link #configureMessageReaders(List)} that allows
|
||||
* modifying the message readers to use after default ones have been added.
|
||||
*/
|
||||
default void extendMessageReaders(List<HttpMessageReader<?>> readers) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Add custom {@link Converter}s and {@link Formatter}s for performing type
|
||||
* conversion and formatting of controller method arguments.
|
||||
*/
|
||||
default void addFormatters(FormatterRegistry registry) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a custom {@link Validator}.
|
||||
* <p>By default a validator for standard bean validation is created if
|
||||
* bean validation api is present on the classpath.
|
||||
*/
|
||||
default Optional<Validator> getValidator() {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a custom {@link MessageCodesResolver} to use for data binding
|
||||
* instead of the one created by default in
|
||||
* {@link org.springframework.validation.DataBinder}.
|
||||
*/
|
||||
default Optional<MessageCodesResolver> getMessageCodesResolver() {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the message writers to use to encode the response body based on
|
||||
* the return values of {@code @ResponseBody}, and {@code ResponseEntity}
|
||||
* controller methods. If none are specified, default ones are added based on
|
||||
* {@link WebReactiveConfigurationSupport#addDefaultHttpMessageWriters(List)}.
|
||||
* <p>See {@link #extendMessageWriters(List)} for adding writers
|
||||
* in addition to the default ones.
|
||||
* @param writers a empty list to add message writers to
|
||||
*/
|
||||
default void configureMessageWriters(List<HttpMessageWriter<?>> writers) {
|
||||
}
|
||||
|
||||
/**
|
||||
* An alternative to {@link #configureMessageWriters(List)} that allows
|
||||
* modifying the message writers to use after default ones have been added.
|
||||
*/
|
||||
default void extendMessageWriters(List<HttpMessageWriter<?>> writers) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure view resolution for processing the return values of controller
|
||||
* methods that rely on resolving a
|
||||
* {@link org.springframework.web.reactive.result.view.View} to render
|
||||
* the response with. By default all controller methods rely on view
|
||||
* resolution unless annotated with {@code @ResponseBody} or explicitly
|
||||
* return {@code ResponseEntity}. A view may be specified explicitly with
|
||||
* a String return value or implicitly, e.g. {@code void} return value.
|
||||
* @see ViewResolverRegistry
|
||||
*/
|
||||
default void configureViewResolvers(ViewResolverRegistry registry) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.format.FormatterRegistry;
|
||||
import org.springframework.http.codec.HttpMessageReader;
|
||||
import org.springframework.http.codec.HttpMessageWriter;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.validation.MessageCodesResolver;
|
||||
import org.springframework.validation.Validator;
|
||||
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder;
|
||||
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver;
|
||||
|
||||
/**
|
||||
* A {@link WebReactiveConfigurer} that delegates to one or more others.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public class WebReactiveConfigurerComposite implements WebReactiveConfigurer {
|
||||
|
||||
private final List<WebReactiveConfigurer> delegates = new ArrayList<>();
|
||||
|
||||
|
||||
public void addWebReactiveConfigurers(List<WebReactiveConfigurer> configurers) {
|
||||
if (!CollectionUtils.isEmpty(configurers)) {
|
||||
this.delegates.addAll(configurers);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void configureContentTypeResolver(RequestedContentTypeResolverBuilder builder) {
|
||||
this.delegates.stream().forEach(delegate -> delegate.configureContentTypeResolver(builder));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
this.delegates.stream().forEach(delegate -> delegate.addCorsMappings(registry));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configurePathMatching(PathMatchConfigurer configurer) {
|
||||
this.delegates.stream().forEach(delegate -> delegate.configurePathMatching(configurer));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
this.delegates.stream().forEach(delegate -> delegate.addResourceHandlers(registry));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
|
||||
this.delegates.stream().forEach(delegate -> delegate.addArgumentResolvers(resolvers));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureMessageReaders(List<HttpMessageReader<?>> readers) {
|
||||
this.delegates.stream().forEach(delegate -> delegate.configureMessageReaders(readers));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void extendMessageReaders(List<HttpMessageReader<?>> readers) {
|
||||
this.delegates.stream().forEach(delegate -> delegate.extendMessageReaders(readers));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addFormatters(FormatterRegistry registry) {
|
||||
this.delegates.stream().forEach(delegate -> delegate.addFormatters(registry));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Validator> getValidator() {
|
||||
return createSingleBean(WebReactiveConfigurer::getValidator, Validator.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<MessageCodesResolver> getMessageCodesResolver() {
|
||||
return createSingleBean(WebReactiveConfigurer::getMessageCodesResolver, MessageCodesResolver.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureMessageWriters(List<HttpMessageWriter<?>> writers) {
|
||||
this.delegates.stream().forEach(delegate -> delegate.configureMessageWriters(writers));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void extendMessageWriters(List<HttpMessageWriter<?>> writers) {
|
||||
this.delegates.stream().forEach(delegate -> delegate.extendMessageWriters(writers));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureViewResolvers(ViewResolverRegistry registry) {
|
||||
this.delegates.stream().forEach(delegate -> delegate.configureViewResolvers(registry));
|
||||
}
|
||||
|
||||
private <T> Optional<T> createSingleBean(Function<WebReactiveConfigurer, Optional<T>> factory,
|
||||
Class<T> beanType) {
|
||||
|
||||
List<Optional<T>> result = this.delegates.stream()
|
||||
.map(factory).filter(Optional::isPresent).collect(Collectors.toList());
|
||||
|
||||
if (result.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
else if (result.size() == 1) {
|
||||
return result.get(0);
|
||||
}
|
||||
else {
|
||||
throw new IllegalStateException("More than one WebReactiveConfigurer implements " +
|
||||
beanType.getSimpleName() + " factory method.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Defines Spring Web Reactive configuration.
|
||||
*/
|
||||
package org.springframework.web.reactive.config;
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.function;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.http.ReactiveHttpInputMessage;
|
||||
import org.springframework.http.codec.HttpMessageReader;
|
||||
|
||||
/**
|
||||
* A function that can extract data from a {@link ReactiveHttpInputMessage} body.
|
||||
*
|
||||
* @param <T> the type of data to extract
|
||||
* @author Arjen Poutsma
|
||||
* @since 5.0
|
||||
* @see BodyExtractors
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface BodyExtractor<T, M extends ReactiveHttpInputMessage> {
|
||||
|
||||
/**
|
||||
* Extract from the given input message.
|
||||
* @param inputMessage request to extract from
|
||||
* @param context the configuration to use
|
||||
* @return the extracted data
|
||||
*/
|
||||
T extract(M inputMessage, Context context);
|
||||
|
||||
/**
|
||||
* Defines the context used during the extraction.
|
||||
*/
|
||||
interface Context {
|
||||
|
||||
/**
|
||||
* Supply a {@linkplain Stream stream} of {@link HttpMessageReader}s to be used for body
|
||||
* extraction.
|
||||
* @return the stream of message readers
|
||||
*/
|
||||
Supplier<Stream<HttpMessageReader<?>>> messageReaders();
|
||||
|
||||
/**
|
||||
* Return the map of hints to use to customize body extraction.
|
||||
*/
|
||||
Map<String, Object> hints();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.function;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.http.HttpMessage;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ReactiveHttpInputMessage;
|
||||
import org.springframework.http.codec.HttpMessageReader;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
/**
|
||||
* Implementations of {@link BodyExtractor} that read various bodies, such a reactive streams.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @since 5.0
|
||||
*/
|
||||
public abstract class BodyExtractors {
|
||||
|
||||
private static final ResolvableType FORM_TYPE =
|
||||
ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class);
|
||||
|
||||
|
||||
/**
|
||||
* Return a {@code BodyExtractor} that reads into a Reactor {@link Mono}.
|
||||
* @param elementClass the class of element in the {@code Mono}
|
||||
* @param <T> the element type
|
||||
* @return a {@code BodyExtractor} that reads a mono
|
||||
*/
|
||||
public static <T> BodyExtractor<Mono<T>, ReactiveHttpInputMessage> toMono(Class<? extends T> elementClass) {
|
||||
Assert.notNull(elementClass, "'elementClass' must not be null");
|
||||
return toMono(ResolvableType.forClass(elementClass));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@code BodyExtractor} that reads into a Reactor {@link Mono}.
|
||||
* @param elementType the type of element in the {@code Mono}
|
||||
* @param <T> the element type
|
||||
* @return a {@code BodyExtractor} that reads a mono
|
||||
*/
|
||||
public static <T> BodyExtractor<Mono<T>, ReactiveHttpInputMessage> toMono(ResolvableType elementType) {
|
||||
Assert.notNull(elementType, "'elementType' must not be null");
|
||||
return (request, context) -> readWithMessageReaders(request, context,
|
||||
elementType,
|
||||
reader -> reader.readMono(elementType, request, context.hints()),
|
||||
Mono::error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@code BodyExtractor} that reads into a Reactor {@link Flux}.
|
||||
* @param elementClass the class of element in the {@code Flux}
|
||||
* @param <T> the element type
|
||||
* @return a {@code BodyExtractor} that reads a mono
|
||||
*/
|
||||
public static <T> BodyExtractor<Flux<T>, ReactiveHttpInputMessage> toFlux(Class<? extends T> elementClass) {
|
||||
Assert.notNull(elementClass, "'elementClass' must not be null");
|
||||
return toFlux(ResolvableType.forClass(elementClass));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@code BodyExtractor} that reads into a Reactor {@link Flux}.
|
||||
* @param elementType the type of element in the {@code Flux}
|
||||
* @param <T> the element type
|
||||
* @return a {@code BodyExtractor} that reads a mono
|
||||
*/
|
||||
public static <T> BodyExtractor<Flux<T>, ReactiveHttpInputMessage> toFlux(ResolvableType elementType) {
|
||||
Assert.notNull(elementType, "'elementType' must not be null");
|
||||
return (inputMessage, context) -> readWithMessageReaders(inputMessage, context,
|
||||
elementType,
|
||||
reader -> reader.read(elementType, inputMessage, context.hints()),
|
||||
Flux::error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@code BodyExtractor} that reads form data into a {@link MultiValueMap}.
|
||||
* @return a {@code BodyExtractor} that reads form data
|
||||
*/
|
||||
public static BodyExtractor<Mono<MultiValueMap<String, String>>, ServerHttpRequest> toFormData() {
|
||||
return (serverRequest, context) -> {
|
||||
HttpMessageReader<MultiValueMap<String, String>> messageReader = formMessageReader(context);
|
||||
return messageReader.readMono(FORM_TYPE, serverRequest, context.hints());
|
||||
};
|
||||
}
|
||||
|
||||
private static HttpMessageReader<MultiValueMap<String, String>> formMessageReader(BodyExtractor.Context context) {
|
||||
return context.messageReaders().get()
|
||||
.filter(messageReader -> messageReader
|
||||
.canRead(FORM_TYPE, MediaType.APPLICATION_FORM_URLENCODED))
|
||||
.findFirst()
|
||||
.map(BodyExtractors::<MultiValueMap<String, String>>cast)
|
||||
.orElseThrow(() -> new IllegalStateException(
|
||||
"Could not find HttpMessageReader that supports " +
|
||||
MediaType.APPLICATION_FORM_URLENCODED_VALUE));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return a {@code BodyExtractor} that returns the body of the message as a {@link Flux} of
|
||||
* {@link DataBuffer}s.
|
||||
* <p><strong>Note</strong> that the returned buffers should be released after usage by calling
|
||||
* {@link org.springframework.core.io.buffer.DataBufferUtils#release(DataBuffer)}
|
||||
* @return a {@code BodyExtractor} that returns the body
|
||||
* @see ReactiveHttpInputMessage#getBody()
|
||||
*/
|
||||
public static BodyExtractor<Flux<DataBuffer>, ReactiveHttpInputMessage> toDataBuffers() {
|
||||
return (inputMessage, context) -> inputMessage.getBody();
|
||||
}
|
||||
|
||||
private static <T, S extends Publisher<T>> S readWithMessageReaders(
|
||||
ReactiveHttpInputMessage inputMessage,
|
||||
BodyExtractor.Context context,
|
||||
ResolvableType elementType,
|
||||
Function<HttpMessageReader<T>, S> readerFunction,
|
||||
Function<Throwable, S> unsupportedError) {
|
||||
|
||||
MediaType contentType = contentType(inputMessage);
|
||||
Supplier<Stream<HttpMessageReader<?>>> messageReaders = context.messageReaders();
|
||||
return messageReaders.get()
|
||||
.filter(r -> r.canRead(elementType, contentType))
|
||||
.findFirst()
|
||||
.map(BodyExtractors::<T>cast)
|
||||
.map(readerFunction)
|
||||
.orElseGet(() -> {
|
||||
List<MediaType> supportedMediaTypes = messageReaders.get()
|
||||
.flatMap(reader -> reader.getReadableMediaTypes().stream())
|
||||
.collect(Collectors.toList());
|
||||
UnsupportedMediaTypeException error =
|
||||
new UnsupportedMediaTypeException(contentType, supportedMediaTypes);
|
||||
return unsupportedError.apply(error);
|
||||
});
|
||||
}
|
||||
|
||||
private static MediaType contentType(HttpMessage message) {
|
||||
MediaType result = message.getHeaders().getContentType();
|
||||
return result != null ? result : MediaType.APPLICATION_OCTET_STREAM;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> HttpMessageReader<T> cast(HttpMessageReader<?> messageReader) {
|
||||
return (HttpMessageReader<T>) messageReader;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.function;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.http.ReactiveHttpOutputMessage;
|
||||
import org.springframework.http.codec.HttpMessageWriter;
|
||||
|
||||
/**
|
||||
* A combination of functions that can populate a {@link ReactiveHttpOutputMessage} body.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @since 5.0
|
||||
* @see BodyInserters
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface BodyInserter<T, M extends ReactiveHttpOutputMessage> {
|
||||
|
||||
/**
|
||||
* Insert into the given output message.
|
||||
* @param outputMessage the response to insert into
|
||||
* @param context the context to use
|
||||
* @return a {@code Mono} that indicates completion or error
|
||||
*/
|
||||
Mono<Void> insert(M outputMessage, Context context);
|
||||
|
||||
/**
|
||||
* Defines the context used during the insertion.
|
||||
*/
|
||||
interface Context {
|
||||
|
||||
/**
|
||||
* Supply a {@linkplain Stream stream} of {@link HttpMessageWriter}s to be used for response
|
||||
* body conversion.
|
||||
* @return the stream of message writers
|
||||
*/
|
||||
Supplier<Stream<HttpMessageWriter<?>>> messageWriters();
|
||||
|
||||
/**
|
||||
* Return the map of hints to use for response body conversion.
|
||||
*/
|
||||
Map<String, Object> hints();
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.function;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ReactiveHttpOutputMessage;
|
||||
import org.springframework.http.client.reactive.ClientHttpRequest;
|
||||
import org.springframework.http.codec.HttpMessageWriter;
|
||||
import org.springframework.http.codec.ServerSentEvent;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
/**
|
||||
* Implementations of {@link BodyInserter} that write various bodies, such a reactive streams,
|
||||
* server-sent events, resources, etc.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @since 5.0
|
||||
*/
|
||||
public abstract class BodyInserters {
|
||||
|
||||
private static final ResolvableType RESOURCE_TYPE = ResolvableType.forClass(Resource.class);
|
||||
|
||||
private static final ResolvableType SERVER_SIDE_EVENT_TYPE =
|
||||
ResolvableType.forClass(ServerSentEvent.class);
|
||||
|
||||
private static final ResolvableType FORM_TYPE =
|
||||
ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class);
|
||||
|
||||
|
||||
private static final BodyInserter<Void, ReactiveHttpOutputMessage> EMPTY =
|
||||
(response, context) -> response.setComplete();
|
||||
|
||||
|
||||
/**
|
||||
* Return an empty {@code BodyInserter} that writes nothing.
|
||||
* @return an empty {@code BodyInserter}
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> BodyInserter<T, ReactiveHttpOutputMessage> empty() {
|
||||
return (BodyInserter<T, ReactiveHttpOutputMessage>)EMPTY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@code BodyInserter} that writes the given single object.
|
||||
* @param body the body of the response
|
||||
* @return a {@code BodyInserter} that writes a single object
|
||||
*/
|
||||
public static <T> BodyInserter<T, ReactiveHttpOutputMessage> fromObject(T body) {
|
||||
Assert.notNull(body, "'body' must not be null");
|
||||
return bodyInserterFor(Mono.just(body), ResolvableType.forInstance(body));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@code BodyInserter} that writes the given {@link Publisher}.
|
||||
* @param publisher the publisher to stream to the response body
|
||||
* @param elementClass the class of elements contained in the publisher
|
||||
* @param <T> the type of the elements contained in the publisher
|
||||
* @param <P> the type of the {@code Publisher}
|
||||
* @return a {@code BodyInserter} that writes a {@code Publisher}
|
||||
*/
|
||||
public static <T, P extends Publisher<T>> BodyInserter<P, ReactiveHttpOutputMessage> fromPublisher(P publisher,
|
||||
Class<T> elementClass) {
|
||||
|
||||
Assert.notNull(publisher, "'publisher' must not be null");
|
||||
Assert.notNull(elementClass, "'elementClass' must not be null");
|
||||
return bodyInserterFor(publisher, ResolvableType.forClass(elementClass));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@code BodyInserter} that writes the given {@link Publisher}.
|
||||
* @param publisher the publisher to stream to the response body
|
||||
* @param elementType the type of elements contained in the publisher
|
||||
* @param <T> the type of the elements contained in the publisher
|
||||
* @param <P> the type of the {@code Publisher}
|
||||
* @return a {@code BodyInserter} that writes a {@code Publisher}
|
||||
*/
|
||||
public static <T, P extends Publisher<T>> BodyInserter<P, ReactiveHttpOutputMessage> fromPublisher(P publisher,
|
||||
ResolvableType elementType) {
|
||||
|
||||
Assert.notNull(publisher, "'publisher' must not be null");
|
||||
Assert.notNull(elementType, "'elementType' must not be null");
|
||||
return bodyInserterFor(publisher, elementType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@code BodyInserter} that writes the given {@code Resource}.
|
||||
* If the resource can be resolved to a {@linkplain Resource#getFile() file}, it will be copied
|
||||
* using
|
||||
* <a href="https://en.wikipedia.org/wiki/Zero-copy">zero-copy</a>
|
||||
* @param resource the resource to write to the output message
|
||||
* @param <T> the type of the {@code Resource}
|
||||
* @return a {@code BodyInserter} that writes a {@code Publisher}
|
||||
*/
|
||||
public static <T extends Resource> BodyInserter<T, ReactiveHttpOutputMessage> fromResource(T resource) {
|
||||
Assert.notNull(resource, "'resource' must not be null");
|
||||
return (outputMessage, context) -> {
|
||||
HttpMessageWriter<Resource> messageWriter = resourceHttpMessageWriter(context);
|
||||
return messageWriter.write(Mono.just(resource), RESOURCE_TYPE, null,
|
||||
outputMessage, context.hints());
|
||||
};
|
||||
}
|
||||
|
||||
private static HttpMessageWriter<Resource> resourceHttpMessageWriter(BodyInserter.Context context) {
|
||||
return context.messageWriters().get()
|
||||
.filter(messageWriter -> messageWriter.canWrite(RESOURCE_TYPE, null))
|
||||
.findFirst()
|
||||
.map(BodyInserters::<Resource>cast)
|
||||
.orElseThrow(() -> new IllegalStateException(
|
||||
"Could not find HttpMessageWriter that supports Resources."));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@code BodyInserter} that writes the given {@code ServerSentEvent} publisher.
|
||||
* @param eventsPublisher the {@code ServerSentEvent} publisher to write to the response body
|
||||
* @param <T> the type of the elements contained in the {@link ServerSentEvent}
|
||||
* @return a {@code BodyInserter} that writes a {@code ServerSentEvent} publisher
|
||||
* @see <a href="https://www.w3.org/TR/eventsource/">Server-Sent Events W3C recommendation</a>
|
||||
*/
|
||||
public static <T, S extends Publisher<ServerSentEvent<T>>> BodyInserter<S, ServerHttpResponse> fromServerSentEvents(
|
||||
S eventsPublisher) {
|
||||
|
||||
Assert.notNull(eventsPublisher, "'eventsPublisher' must not be null");
|
||||
return (response, context) -> {
|
||||
HttpMessageWriter<ServerSentEvent<T>> messageWriter =
|
||||
findMessageWriter(context, SERVER_SIDE_EVENT_TYPE, MediaType.TEXT_EVENT_STREAM);
|
||||
return messageWriter.write(eventsPublisher, SERVER_SIDE_EVENT_TYPE,
|
||||
MediaType.TEXT_EVENT_STREAM, response, context.hints());
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@code BodyInserter} that writes the given {@code Publisher} publisher as
|
||||
* Server-Sent Events.
|
||||
* @param eventsPublisher the publisher to write to the response body as Server-Sent Events
|
||||
* @param eventClass the class of event contained in the publisher
|
||||
* @param <T> the type of the elements contained in the publisher
|
||||
* @return a {@code BodyInserter} that writes the given {@code Publisher} publisher as
|
||||
* Server-Sent Events
|
||||
* @see <a href="https://www.w3.org/TR/eventsource/">Server-Sent Events W3C recommendation</a>
|
||||
*/
|
||||
public static <T, S extends Publisher<T>> BodyInserter<S, ServerHttpResponse> fromServerSentEvents(S eventsPublisher,
|
||||
Class<T> eventClass) {
|
||||
|
||||
Assert.notNull(eventsPublisher, "'eventsPublisher' must not be null");
|
||||
Assert.notNull(eventClass, "'eventClass' must not be null");
|
||||
return fromServerSentEvents(eventsPublisher, ResolvableType.forClass(eventClass));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@code BodyInserter} that writes the given {@code Publisher} publisher as
|
||||
* Server-Sent Events.
|
||||
* @param eventsPublisher the publisher to write to the response body as Server-Sent Events
|
||||
* @param eventType the type of event contained in the publisher
|
||||
* @param <T> the type of the elements contained in the publisher
|
||||
* @return a {@code BodyInserter} that writes the given {@code Publisher} publisher as
|
||||
* Server-Sent Events
|
||||
* @see <a href="https://www.w3.org/TR/eventsource/">Server-Sent Events W3C recommendation</a>
|
||||
*/
|
||||
public static <T, S extends Publisher<T>> BodyInserter<S, ServerHttpResponse> fromServerSentEvents(S eventsPublisher,
|
||||
ResolvableType eventType) {
|
||||
|
||||
Assert.notNull(eventsPublisher, "'eventsPublisher' must not be null");
|
||||
Assert.notNull(eventType, "'eventType' must not be null");
|
||||
return (outputMessage, context) -> {
|
||||
HttpMessageWriter<T> messageWriter =
|
||||
findMessageWriter(context, SERVER_SIDE_EVENT_TYPE, MediaType.TEXT_EVENT_STREAM);
|
||||
return messageWriter.write(eventsPublisher, eventType,
|
||||
MediaType.TEXT_EVENT_STREAM, outputMessage, context.hints());
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@code BodyInserter} that writes the given {@code MultiValueMap} as URL-encoded
|
||||
* form data.
|
||||
* @param formData the form data to write to the output message
|
||||
* @return a {@code BodyInserter} that writes form data
|
||||
*/
|
||||
public static BodyInserter<MultiValueMap<String, String>, ClientHttpRequest> fromFormData(MultiValueMap<String, String> formData) {
|
||||
Assert.notNull(formData, "'formData' must not be null");
|
||||
|
||||
return (outputMessage, context) -> {
|
||||
HttpMessageWriter<MultiValueMap<String, String>> messageWriter =
|
||||
findMessageWriter(context, FORM_TYPE, MediaType.APPLICATION_FORM_URLENCODED);
|
||||
return messageWriter.write(Mono.just(formData), FORM_TYPE,
|
||||
MediaType.APPLICATION_FORM_URLENCODED, outputMessage, context.hints());
|
||||
};
|
||||
}
|
||||
|
||||
private static <T> HttpMessageWriter<T> findMessageWriter(BodyInserter.Context context,
|
||||
ResolvableType type,
|
||||
MediaType mediaType) {
|
||||
|
||||
return context.messageWriters().get()
|
||||
.filter(messageWriter -> messageWriter.canWrite(type, mediaType))
|
||||
.findFirst()
|
||||
.map(BodyInserters::<T>cast)
|
||||
.orElseThrow(() -> new IllegalStateException(
|
||||
"Could not find HttpMessageWriter that supports " + mediaType));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Return a {@code BodyInserter} that writes the given {@code Publisher<DataBuffer>} to the
|
||||
* body.
|
||||
* @param publisher the data buffer publisher to write
|
||||
* @param <T> the type of the publisher
|
||||
* @return a {@code BodyInserter} that writes directly to the body
|
||||
* @see ReactiveHttpOutputMessage#writeWith(Publisher)
|
||||
*/
|
||||
public static <T extends Publisher<DataBuffer>> BodyInserter<T, ReactiveHttpOutputMessage> fromDataBuffers(T publisher) {
|
||||
Assert.notNull(publisher, "'publisher' must not be null");
|
||||
|
||||
return (outputMessage, context) -> outputMessage.writeWith(publisher);
|
||||
}
|
||||
|
||||
|
||||
private static <T, P extends Publisher<?>, M extends ReactiveHttpOutputMessage> BodyInserter<T, M> bodyInserterFor(P body, ResolvableType bodyType) {
|
||||
|
||||
return (m, context) -> {
|
||||
|
||||
MediaType contentType = m.getHeaders().getContentType();
|
||||
Supplier<Stream<HttpMessageWriter<?>>> messageWriters = context.messageWriters();
|
||||
return messageWriters.get()
|
||||
.filter(messageWriter -> messageWriter.canWrite(bodyType, contentType))
|
||||
.findFirst()
|
||||
.map(BodyInserters::cast)
|
||||
.map(messageWriter -> messageWriter
|
||||
.write(body, bodyType, contentType, m, context.hints()))
|
||||
.orElseGet(() -> {
|
||||
List<MediaType> supportedMediaTypes = messageWriters.get()
|
||||
.flatMap(reader -> reader.getWritableMediaTypes().stream())
|
||||
.collect(Collectors.toList());
|
||||
UnsupportedMediaTypeException error =
|
||||
new UnsupportedMediaTypeException(contentType, supportedMediaTypes);
|
||||
return Mono.error(error);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> HttpMessageWriter<T> cast(HttpMessageWriter<?> messageWriter) {
|
||||
return (HttpMessageWriter<T>) messageWriter;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.function;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.core.NestedRuntimeException;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
/**
|
||||
* Exception thrown to indicate that a {@code Content-Type} is not supported.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @since 5.0
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class UnsupportedMediaTypeException extends NestedRuntimeException {
|
||||
|
||||
private final MediaType contentType;
|
||||
|
||||
private final List<MediaType> supportedMediaTypes;
|
||||
|
||||
|
||||
/**
|
||||
* Constructor for when the specified Content-Type is invalid.
|
||||
*/
|
||||
public UnsupportedMediaTypeException(String reason) {
|
||||
super(reason);
|
||||
this.contentType = null;
|
||||
this.supportedMediaTypes = Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for when the Content-Type can be parsed but is not supported.
|
||||
*/
|
||||
public UnsupportedMediaTypeException(MediaType contentType, List<MediaType> supportedMediaTypes) {
|
||||
super("Content type '" + contentType + "' not supported");
|
||||
this.contentType = contentType;
|
||||
this.supportedMediaTypes = Collections.unmodifiableList(supportedMediaTypes);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the request Content-Type header if it was parsed successfully.
|
||||
*/
|
||||
public Optional<MediaType> getContentType() {
|
||||
return Optional.ofNullable(this.contentType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of supported content types in cases when the Content-Type
|
||||
* header is parsed but not supported, or an empty list otherwise.
|
||||
*/
|
||||
public List<MediaType> getSupportedMediaTypes() {
|
||||
return this.supportedMediaTypes;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.function.client;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.client.reactive.ClientHttpRequest;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.reactive.function.BodyInserter;
|
||||
|
||||
/**
|
||||
* Represents a typed, immutable, client-side HTTP request, as executed by the
|
||||
* {@link ExchangeFunction}. Instances of this interface can be created via static
|
||||
* builder methods.
|
||||
*
|
||||
* <p>Note that applications are more likely to perform requests through
|
||||
* {@link WebClient} rather than using this directly.
|
||||
*
|
||||
* @param <T> the type of the body that this request contains
|
||||
* @author Brian Clozel
|
||||
* @author Arjen Poutsma
|
||||
* @since 5.0
|
||||
*/
|
||||
public interface ClientRequest<T> {
|
||||
|
||||
/**
|
||||
* Return the HTTP method.
|
||||
*/
|
||||
HttpMethod method();
|
||||
|
||||
/**
|
||||
* Return the request URI.
|
||||
*/
|
||||
URI url();
|
||||
|
||||
/**
|
||||
* Return the headers of this request.
|
||||
*/
|
||||
HttpHeaders headers();
|
||||
|
||||
/**
|
||||
* Return the cookies of this request.
|
||||
*/
|
||||
MultiValueMap<String, String> cookies();
|
||||
|
||||
/**
|
||||
* Return the body inserter of this request.
|
||||
*/
|
||||
BodyInserter<T, ? super ClientHttpRequest> inserter();
|
||||
|
||||
/**
|
||||
* Writes this request to the given {@link ClientHttpRequest}.
|
||||
*
|
||||
* @param request the client http request to write to
|
||||
* @param strategies the strategies to use when writing
|
||||
* @return {@code Mono<Void>} to indicate when writing is complete
|
||||
*/
|
||||
Mono<Void> writeTo(ClientHttpRequest request, ExchangeStrategies strategies);
|
||||
|
||||
|
||||
// Static builder methods
|
||||
|
||||
/**
|
||||
* Create a builder with the method, URI, headers, and cookies of the given request.
|
||||
*
|
||||
* @param other the request to copy the method, URI, headers, and cookies from
|
||||
* @return the created builder
|
||||
*/
|
||||
static Builder from(ClientRequest<?> other) {
|
||||
Assert.notNull(other, "'other' must not be null");
|
||||
return new DefaultClientRequestBuilder(other.method(), other.url())
|
||||
.headers(other.headers())
|
||||
.cookies(other.cookies());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a builder with the given method and url.
|
||||
* @param method the HTTP method (GET, POST, etc)
|
||||
* @param url the URL
|
||||
* @return the created builder
|
||||
*/
|
||||
static Builder method(HttpMethod method, URI url) {
|
||||
return new DefaultClientRequestBuilder(method, url);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Defines a builder for a request.
|
||||
*/
|
||||
interface Builder {
|
||||
|
||||
/**
|
||||
* Add the given, single header value under the given name.
|
||||
* @param headerName the header name
|
||||
* @param headerValues the header value(s)
|
||||
* @return this builder
|
||||
* @see HttpHeaders#add(String, String)
|
||||
*/
|
||||
Builder header(String headerName, String... headerValues);
|
||||
|
||||
/**
|
||||
* Copy the given headers into the entity's headers map.
|
||||
*
|
||||
* @param headers the existing HttpHeaders to copy from
|
||||
* @return this builder
|
||||
*/
|
||||
Builder headers(HttpHeaders headers);
|
||||
|
||||
/**
|
||||
* Add a cookie with the given name and value.
|
||||
* @param name the cookie name
|
||||
* @param value the cookie value
|
||||
* @return this builder
|
||||
*/
|
||||
Builder cookie(String name, String value);
|
||||
|
||||
/**
|
||||
* Copy the given cookies into the entity's cookies map.
|
||||
*
|
||||
* @param cookies the existing cookies to copy from
|
||||
* @return this builder
|
||||
*/
|
||||
Builder cookies(MultiValueMap<String, String> cookies);
|
||||
|
||||
/**
|
||||
* Builds the request entity with no body.
|
||||
* @return the request entity
|
||||
*/
|
||||
ClientRequest<Void> build();
|
||||
|
||||
/**
|
||||
* Set the body of the request to the given {@code BodyInserter} and return it.
|
||||
* @param inserter the {@code BodyInserter} that writes to the request
|
||||
* @param <T> the type contained in the body
|
||||
* @return the built request
|
||||
*/
|
||||
<T> ClientRequest<T> body(BodyInserter<T, ? super ClientHttpRequest> inserter);
|
||||
|
||||
/**
|
||||
* Set the body of the request to the given {@code Publisher} and return it.
|
||||
* @param publisher the {@code Publisher} to write to the request
|
||||
* @param elementClass the class of elements contained in the publisher
|
||||
* @param <T> the type of the elements contained in the publisher
|
||||
* @param <S> the type of the {@code Publisher}
|
||||
* @return the built request
|
||||
*/
|
||||
<T, S extends Publisher<T>> ClientRequest<S> body(S publisher, Class<T> elementClass);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.function.client;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalLong;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.reactive.ClientHttpResponse;
|
||||
import org.springframework.web.reactive.function.BodyExtractor;
|
||||
|
||||
/**
|
||||
* Represents an HTTP response, as returned by the {@link ExchangeFunction}.
|
||||
* Access to headers and body is offered by {@link Headers} and
|
||||
* {@link #body(BodyExtractor)}, {@link #bodyToMono(Class)}, {@link #bodyToFlux(Class)}
|
||||
* respectively.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Arjen Poutsma
|
||||
* @since 5.0
|
||||
*/
|
||||
public interface ClientResponse {
|
||||
|
||||
/**
|
||||
* Return the status code of this response.
|
||||
*/
|
||||
HttpStatus statusCode();
|
||||
|
||||
/**
|
||||
* Return the headers of this response.
|
||||
*/
|
||||
Headers headers();
|
||||
|
||||
/**
|
||||
* Extract the body with the given {@code BodyExtractor}. Unlike {@link #bodyToMono(Class)} and
|
||||
* {@link #bodyToFlux(Class)}; this method does not check for a 4xx or 5xx status code before
|
||||
* extracting the body.
|
||||
* @param extractor the {@code BodyExtractor} that reads from the response
|
||||
* @param <T> the type of the body returned
|
||||
* @return the extracted body
|
||||
*/
|
||||
<T> T body(BodyExtractor<T, ? super ClientHttpResponse> extractor);
|
||||
|
||||
/**
|
||||
* Extract the body to a {@code Mono}. If the response has status code 4xx or 5xx, the
|
||||
* {@code Mono} will contain a {@link WebClientException}.
|
||||
* @param elementClass the class of element in the {@code Mono}
|
||||
* @param <T> the element type
|
||||
* @return a mono containing the body, or a {@link WebClientException} if the status code is
|
||||
* 4xx or 5xx
|
||||
*/
|
||||
<T> Mono<T> bodyToMono(Class<? extends T> elementClass);
|
||||
|
||||
/**
|
||||
* Extract the body to a {@code Flux}. If the response has status code 4xx or 5xx, the
|
||||
* {@code Flux} will contain a {@link WebClientException}.
|
||||
* @param elementClass the class of element in the {@code Flux}
|
||||
* @param <T> the element type
|
||||
* @return a flux containing the body, or a {@link WebClientException} if the status code is
|
||||
* 4xx or 5xx
|
||||
*/
|
||||
<T> Flux<T> bodyToFlux(Class<? extends T> elementClass);
|
||||
|
||||
|
||||
/**
|
||||
* Represents the headers of the HTTP response.
|
||||
* @see ClientResponse#headers()
|
||||
*/
|
||||
interface Headers {
|
||||
|
||||
/**
|
||||
* Return the length of the body in bytes, as specified by the
|
||||
* {@code Content-Length} header.
|
||||
*/
|
||||
OptionalLong contentLength();
|
||||
|
||||
/**
|
||||
* Return the {@linkplain MediaType media type} of the body, as specified
|
||||
* by the {@code Content-Type} header.
|
||||
*/
|
||||
Optional<MediaType> contentType();
|
||||
|
||||
/**
|
||||
* Return the header value(s), if any, for the header of the given name.
|
||||
* <p>Return an empty list if no header values are found.
|
||||
*
|
||||
* @param headerName the header name
|
||||
*/
|
||||
List<String> header(String headerName);
|
||||
|
||||
/**
|
||||
* Return the headers as a {@link HttpHeaders} instance.
|
||||
*/
|
||||
HttpHeaders asHttpHeaders();
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.function.client;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.http.HttpCookie;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.client.reactive.ClientHttpRequest;
|
||||
import org.springframework.http.codec.HttpMessageWriter;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.reactive.function.BodyInserter;
|
||||
import org.springframework.web.reactive.function.BodyInserters;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link ClientRequest.Builder}.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @since 5.0
|
||||
*/
|
||||
class DefaultClientRequestBuilder implements ClientRequest.Builder {
|
||||
|
||||
private final HttpMethod method;
|
||||
|
||||
private final URI url;
|
||||
|
||||
private final HttpHeaders headers = new HttpHeaders();
|
||||
|
||||
private final MultiValueMap<String, String> cookies = new LinkedMultiValueMap<>();
|
||||
|
||||
|
||||
public DefaultClientRequestBuilder(HttpMethod method, URI url) {
|
||||
this.method = method;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientRequest.Builder header(String headerName, String... headerValues) {
|
||||
for (String headerValue : headerValues) {
|
||||
this.headers.add(headerName, headerValue);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientRequest.Builder headers(HttpHeaders headers) {
|
||||
if (headers != null) {
|
||||
this.headers.putAll(headers);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientRequest.Builder cookie(String name, String value) {
|
||||
this.cookies.add(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientRequest.Builder cookies(MultiValueMap<String, String> cookies) {
|
||||
if (cookies != null) {
|
||||
this.cookies.putAll(cookies);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientRequest<Void> build() {
|
||||
return body(BodyInserters.empty());
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> ClientRequest<T> body(BodyInserter<T, ? super ClientHttpRequest> inserter) {
|
||||
Assert.notNull(inserter, "'inserter' must not be null");
|
||||
return new BodyInserterRequest<T>(this.method, this.url, this.headers, this.cookies,
|
||||
inserter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T, S extends Publisher<T>> ClientRequest<S> body(S publisher, Class<T> elementClass) {
|
||||
return body(BodyInserters.fromPublisher(publisher, elementClass));
|
||||
}
|
||||
|
||||
private static class BodyInserterRequest<T> implements ClientRequest<T> {
|
||||
|
||||
private final HttpMethod method;
|
||||
|
||||
private final URI url;
|
||||
|
||||
private final HttpHeaders headers;
|
||||
|
||||
private final MultiValueMap<String, String> cookies;
|
||||
|
||||
private final BodyInserter<T, ? super ClientHttpRequest> inserter;
|
||||
|
||||
public BodyInserterRequest(HttpMethod method, URI url, HttpHeaders headers,
|
||||
MultiValueMap<String, String> cookies,
|
||||
BodyInserter<T, ? super ClientHttpRequest> inserter) {
|
||||
this.method = method;
|
||||
this.url = url;
|
||||
this.headers = HttpHeaders.readOnlyHttpHeaders(headers);
|
||||
this.cookies = CollectionUtils.unmodifiableMultiValueMap(cookies);
|
||||
this.inserter = inserter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpMethod method() {
|
||||
return this.method;
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI url() {
|
||||
return this.url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders headers() {
|
||||
return this.headers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MultiValueMap<String, String> cookies() {
|
||||
return this.cookies;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BodyInserter<T, ? super ClientHttpRequest> inserter() {
|
||||
return this.inserter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> writeTo(ClientHttpRequest request, ExchangeStrategies strategies) {
|
||||
HttpHeaders requestHeaders = request.getHeaders();
|
||||
if (!this.headers.isEmpty()) {
|
||||
this.headers.entrySet().stream()
|
||||
.filter(entry -> !requestHeaders.containsKey(entry.getKey()))
|
||||
.forEach(entry -> requestHeaders
|
||||
.put(entry.getKey(), entry.getValue()));
|
||||
}
|
||||
MultiValueMap<String, HttpCookie> requestCookies = request.getCookies();
|
||||
if (!this.cookies.isEmpty()) {
|
||||
this.cookies.entrySet().forEach(entry -> {
|
||||
String name = entry.getKey();
|
||||
entry.getValue().forEach(value -> {
|
||||
HttpCookie cookie = new HttpCookie(name, value);
|
||||
requestCookies.add(name, cookie);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return this.inserter.insert(request, new BodyInserter.Context() {
|
||||
@Override
|
||||
public Supplier<Stream<HttpMessageWriter<?>>> messageWriters() {
|
||||
return strategies.messageWriters();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> hints() {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.function.client;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalLong;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.reactive.ClientHttpResponse;
|
||||
import org.springframework.http.codec.HttpMessageReader;
|
||||
import org.springframework.web.reactive.function.BodyExtractor;
|
||||
import org.springframework.web.reactive.function.BodyExtractors;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link ClientResponse}.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @since 5.0
|
||||
*/
|
||||
class DefaultClientResponse implements ClientResponse {
|
||||
|
||||
private final ClientHttpResponse response;
|
||||
|
||||
private final Headers headers;
|
||||
|
||||
private final ExchangeStrategies strategies;
|
||||
|
||||
|
||||
public DefaultClientResponse(ClientHttpResponse response, ExchangeStrategies strategies) {
|
||||
this.response = response;
|
||||
this.strategies = strategies;
|
||||
this.headers = new DefaultHeaders();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpStatus statusCode() {
|
||||
return this.response.getStatusCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Headers headers() {
|
||||
return this.headers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T body(BodyExtractor<T, ? super ClientHttpResponse> extractor) {
|
||||
return extractor.extract(this.response, new BodyExtractor.Context() {
|
||||
@Override
|
||||
public Supplier<Stream<HttpMessageReader<?>>> messageReaders() {
|
||||
return strategies.messageReaders();
|
||||
}
|
||||
@Override
|
||||
public Map<String, Object> hints() {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Mono<T> bodyToMono(Class<? extends T> elementClass) {
|
||||
return bodyToPublisher(BodyExtractors.toMono(elementClass), Mono::error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Flux<T> bodyToFlux(Class<? extends T> elementClass) {
|
||||
return bodyToPublisher(BodyExtractors.toFlux(elementClass), Flux::error);
|
||||
}
|
||||
|
||||
private <T extends Publisher<?>> T bodyToPublisher(
|
||||
BodyExtractor<T, ? super ClientHttpResponse> extractor,
|
||||
Function<WebClientException, T> errorFunction) {
|
||||
|
||||
HttpStatus status = statusCode();
|
||||
if (status.is4xxClientError() || status.is5xxServerError()) {
|
||||
WebClientException ex = new WebClientException(
|
||||
"ClientResponse has erroneous status code: " + status.value() +
|
||||
" " + status.getReasonPhrase());
|
||||
return errorFunction.apply(ex);
|
||||
}
|
||||
else {
|
||||
return body(extractor);
|
||||
}
|
||||
}
|
||||
|
||||
private class DefaultHeaders implements Headers {
|
||||
|
||||
private HttpHeaders delegate() {
|
||||
return response.getHeaders();
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionalLong contentLength() {
|
||||
return toOptionalLong(delegate().getContentLength());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<MediaType> contentType() {
|
||||
return Optional.ofNullable(delegate().getContentType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> header(String headerName) {
|
||||
List<String> headerValues = delegate().get(headerName);
|
||||
return headerValues != null ? headerValues : Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders asHttpHeaders() {
|
||||
return HttpHeaders.readOnlyHttpHeaders(delegate());
|
||||
}
|
||||
|
||||
private OptionalLong toOptionalLong(long value) {
|
||||
return value != -1 ? OptionalLong.of(value) : OptionalLong.empty();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.function.client;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.codec.ByteArrayDecoder;
|
||||
import org.springframework.core.codec.ByteArrayEncoder;
|
||||
import org.springframework.core.codec.ByteBufferDecoder;
|
||||
import org.springframework.core.codec.ByteBufferEncoder;
|
||||
import org.springframework.core.codec.CharSequenceEncoder;
|
||||
import org.springframework.core.codec.Decoder;
|
||||
import org.springframework.core.codec.Encoder;
|
||||
import org.springframework.core.codec.StringDecoder;
|
||||
import org.springframework.http.codec.DecoderHttpMessageReader;
|
||||
import org.springframework.http.codec.EncoderHttpMessageWriter;
|
||||
import org.springframework.http.codec.FormHttpMessageWriter;
|
||||
import org.springframework.http.codec.HttpMessageReader;
|
||||
import org.springframework.http.codec.HttpMessageWriter;
|
||||
import org.springframework.http.codec.ResourceHttpMessageWriter;
|
||||
import org.springframework.http.codec.ServerSentEventHttpMessageReader;
|
||||
import org.springframework.http.codec.json.Jackson2JsonDecoder;
|
||||
import org.springframework.http.codec.json.Jackson2JsonEncoder;
|
||||
import org.springframework.http.codec.xml.Jaxb2XmlDecoder;
|
||||
import org.springframework.http.codec.xml.Jaxb2XmlEncoder;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link ExchangeStrategies.Builder}.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @since 5.0
|
||||
*/
|
||||
class DefaultExchangeStrategiesBuilder implements ExchangeStrategies.Builder {
|
||||
|
||||
private static final boolean jackson2Present =
|
||||
ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper",
|
||||
DefaultExchangeStrategiesBuilder.class.getClassLoader()) &&
|
||||
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator",
|
||||
DefaultExchangeStrategiesBuilder.class.getClassLoader());
|
||||
|
||||
private static final boolean jaxb2Present =
|
||||
ClassUtils.isPresent("javax.xml.bind.Binder",
|
||||
DefaultExchangeStrategiesBuilder.class.getClassLoader());
|
||||
|
||||
|
||||
private final List<HttpMessageReader<?>> messageReaders = new ArrayList<>();
|
||||
|
||||
private final List<HttpMessageWriter<?>> messageWriters = new ArrayList<>();
|
||||
|
||||
|
||||
public void defaultConfiguration() {
|
||||
messageReader(new DecoderHttpMessageReader<>(new ByteArrayDecoder()));
|
||||
messageReader(new DecoderHttpMessageReader<>(new ByteBufferDecoder()));
|
||||
if (jackson2Present) {
|
||||
messageReader(new ServerSentEventHttpMessageReader(Collections.singletonList(new Jackson2JsonDecoder())));
|
||||
}
|
||||
else {
|
||||
messageReader(new ServerSentEventHttpMessageReader(Collections.emptyList()));
|
||||
}
|
||||
messageReader(new DecoderHttpMessageReader<>(new StringDecoder(false)));
|
||||
messageWriter(new EncoderHttpMessageWriter<>(new ByteArrayEncoder()));
|
||||
messageWriter(new EncoderHttpMessageWriter<>(new ByteBufferEncoder()));
|
||||
messageWriter(new EncoderHttpMessageWriter<>(new CharSequenceEncoder()));
|
||||
messageWriter(new ResourceHttpMessageWriter());
|
||||
messageWriter(new FormHttpMessageWriter());
|
||||
if (jaxb2Present) {
|
||||
messageReader(new DecoderHttpMessageReader<>(new Jaxb2XmlDecoder()));
|
||||
messageWriter(new EncoderHttpMessageWriter<>(new Jaxb2XmlEncoder()));
|
||||
}
|
||||
if (jackson2Present) {
|
||||
messageReader(new DecoderHttpMessageReader<>(new Jackson2JsonDecoder()));
|
||||
messageWriter(new EncoderHttpMessageWriter<>(new Jackson2JsonEncoder()));
|
||||
}
|
||||
}
|
||||
|
||||
public void applicationContext(ApplicationContext applicationContext) {
|
||||
applicationContext.getBeansOfType(HttpMessageReader.class).values().forEach(this::messageReader);
|
||||
applicationContext.getBeansOfType(HttpMessageWriter.class).values().forEach(this::messageWriter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExchangeStrategies.Builder messageReader(HttpMessageReader<?> messageReader) {
|
||||
Assert.notNull(messageReader, "'messageReader' must not be null");
|
||||
this.messageReaders.add(messageReader);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExchangeStrategies.Builder decoder(Decoder<?> decoder) {
|
||||
Assert.notNull(decoder, "'decoder' must not be null");
|
||||
return messageReader(new DecoderHttpMessageReader<>(decoder));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExchangeStrategies.Builder messageWriter(HttpMessageWriter<?> messageWriter) {
|
||||
Assert.notNull(messageWriter, "'messageWriter' must not be null");
|
||||
this.messageWriters.add(messageWriter);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExchangeStrategies.Builder encoder(Encoder<?> encoder) {
|
||||
Assert.notNull(encoder, "'encoder' must not be null");
|
||||
return messageWriter(new EncoderHttpMessageWriter<>(encoder));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExchangeStrategies build() {
|
||||
return new DefaultExchangeStrategies(this.messageReaders, this.messageWriters);
|
||||
}
|
||||
|
||||
private static class DefaultExchangeStrategies implements ExchangeStrategies {
|
||||
|
||||
private final List<HttpMessageReader<?>> messageReaders;
|
||||
|
||||
private final List<HttpMessageWriter<?>> messageWriters;
|
||||
|
||||
public DefaultExchangeStrategies(
|
||||
List<HttpMessageReader<?>> messageReaders,
|
||||
List<HttpMessageWriter<?>> messageWriters) {
|
||||
this.messageReaders = unmodifiableCopy(messageReaders);
|
||||
this.messageWriters = unmodifiableCopy(messageWriters);
|
||||
}
|
||||
|
||||
private static <T> List<T> unmodifiableCopy(List<? extends T> list) {
|
||||
return Collections.unmodifiableList(new ArrayList<>(list));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Supplier<Stream<HttpMessageReader<?>>> messageReaders() {
|
||||
return this.messageReaders::stream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Supplier<Stream<HttpMessageWriter<?>>> messageWriters() {
|
||||
return this.messageWriters::stream;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.function.client;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.charset.Charset;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Arrays;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.reactive.ClientHttpRequest;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.reactive.function.BodyInserter;
|
||||
import org.springframework.web.util.DefaultUriBuilderFactory;
|
||||
import org.springframework.web.util.UriBuilderFactory;
|
||||
|
||||
|
||||
/**
|
||||
* Default implementation of {@link WebClient}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
class DefaultWebClient implements WebClient {
|
||||
|
||||
private final ExchangeFunction exchangeFunction;
|
||||
|
||||
private final UriBuilderFactory uriBuilderFactory;
|
||||
|
||||
private final HttpHeaders defaultHeaders;
|
||||
|
||||
private final MultiValueMap<String, String> defaultCookies;
|
||||
|
||||
|
||||
DefaultWebClient(ExchangeFunction exchangeFunction, UriBuilderFactory factory,
|
||||
HttpHeaders defaultHeaders, MultiValueMap<String, String> defaultCookies) {
|
||||
|
||||
this.exchangeFunction = exchangeFunction;
|
||||
this.uriBuilderFactory = (factory != null ? factory : new DefaultUriBuilderFactory());
|
||||
|
||||
this.defaultHeaders = defaultHeaders != null ?
|
||||
HttpHeaders.readOnlyHttpHeaders(defaultHeaders) : null;
|
||||
|
||||
this.defaultCookies = defaultCookies != null ?
|
||||
CollectionUtils.unmodifiableMultiValueMap(defaultCookies) : null;
|
||||
}
|
||||
|
||||
|
||||
private ExchangeFunction getExchangeFunction() {
|
||||
return this.exchangeFunction;
|
||||
}
|
||||
|
||||
private UriBuilderFactory getUriBuilderFactory() {
|
||||
return this.uriBuilderFactory;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public UriSpec get() {
|
||||
return method(HttpMethod.GET);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UriSpec head() {
|
||||
return method(HttpMethod.HEAD);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UriSpec post() {
|
||||
return method(HttpMethod.POST);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UriSpec put() {
|
||||
return method(HttpMethod.PUT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UriSpec patch() {
|
||||
return method(HttpMethod.PATCH);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UriSpec delete() {
|
||||
return method(HttpMethod.DELETE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UriSpec options() {
|
||||
return method(HttpMethod.OPTIONS);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private UriSpec method(HttpMethod httpMethod) {
|
||||
return new DefaultUriSpec(httpMethod);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public WebClient filter(ExchangeFilterFunction filterFunction) {
|
||||
ExchangeFunction filteredExchangeFunction = this.exchangeFunction.filter(filterFunction);
|
||||
return new DefaultWebClient(filteredExchangeFunction,
|
||||
this.uriBuilderFactory, this.defaultHeaders, this.defaultCookies);
|
||||
}
|
||||
|
||||
|
||||
private class DefaultUriSpec implements UriSpec {
|
||||
|
||||
private final HttpMethod httpMethod;
|
||||
|
||||
|
||||
DefaultUriSpec(HttpMethod httpMethod) {
|
||||
this.httpMethod = httpMethod;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HeaderSpec uri(String uriTemplate, Object... uriVariables) {
|
||||
return uri(getUriBuilderFactory().expand(uriTemplate, uriVariables));
|
||||
}
|
||||
|
||||
@Override
|
||||
public HeaderSpec uri(Function<UriBuilderFactory, URI> uriFunction) {
|
||||
return uri(uriFunction.apply(getUriBuilderFactory()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public HeaderSpec uri(URI uri) {
|
||||
return new DefaultHeaderSpec(this.httpMethod, uri);
|
||||
}
|
||||
}
|
||||
|
||||
private class DefaultHeaderSpec implements HeaderSpec {
|
||||
|
||||
private final HttpMethod httpMethod;
|
||||
|
||||
private final URI uri;
|
||||
|
||||
private HttpHeaders headers;
|
||||
|
||||
private MultiValueMap<String, String> cookies;
|
||||
|
||||
|
||||
DefaultHeaderSpec(HttpMethod httpMethod, URI uri) {
|
||||
this.httpMethod = httpMethod;
|
||||
this.uri = uri;
|
||||
}
|
||||
|
||||
|
||||
private HttpHeaders getHeaders() {
|
||||
if (this.headers == null) {
|
||||
this.headers = new HttpHeaders();
|
||||
}
|
||||
return this.headers;
|
||||
}
|
||||
|
||||
private MultiValueMap<String, String> getCookies() {
|
||||
if (this.cookies == null) {
|
||||
this.cookies = new LinkedMultiValueMap<>(4);
|
||||
}
|
||||
return this.cookies;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultHeaderSpec header(String headerName, String... headerValues) {
|
||||
for (String headerValue : headerValues) {
|
||||
getHeaders().add(headerName, headerValue);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultHeaderSpec headers(HttpHeaders headers) {
|
||||
if (headers != null) {
|
||||
getHeaders().putAll(headers);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultHeaderSpec accept(MediaType... acceptableMediaTypes) {
|
||||
getHeaders().setAccept(Arrays.asList(acceptableMediaTypes));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultHeaderSpec acceptCharset(Charset... acceptableCharsets) {
|
||||
getHeaders().setAcceptCharset(Arrays.asList(acceptableCharsets));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultHeaderSpec contentType(MediaType contentType) {
|
||||
getHeaders().setContentType(contentType);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultHeaderSpec contentLength(long contentLength) {
|
||||
getHeaders().setContentLength(contentLength);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultHeaderSpec cookie(String name, String value) {
|
||||
getCookies().add(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultHeaderSpec cookies(MultiValueMap<String, String> cookies) {
|
||||
if (cookies != null) {
|
||||
getCookies().putAll(cookies);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultHeaderSpec ifModifiedSince(ZonedDateTime ifModifiedSince) {
|
||||
ZonedDateTime gmt = ifModifiedSince.withZoneSameInstant(ZoneId.of("GMT"));
|
||||
String headerValue = DateTimeFormatter.RFC_1123_DATE_TIME.format(gmt);
|
||||
getHeaders().set(HttpHeaders.IF_MODIFIED_SINCE, headerValue);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultHeaderSpec ifNoneMatch(String... ifNoneMatches) {
|
||||
getHeaders().setIfNoneMatch(Arrays.asList(ifNoneMatches));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ClientResponse> exchange() {
|
||||
ClientRequest<Void> request = initRequestBuilder().build();
|
||||
return getExchangeFunction().exchange(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Mono<ClientResponse> exchange(BodyInserter<T, ? super ClientHttpRequest> inserter) {
|
||||
ClientRequest<T> request = initRequestBuilder().body(inserter);
|
||||
return getExchangeFunction().exchange(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T, S extends Publisher<T>> Mono<ClientResponse> exchange(S publisher, Class<T> elementClass) {
|
||||
ClientRequest<S> request = initRequestBuilder().headers(this.headers).body(publisher, elementClass);
|
||||
return getExchangeFunction().exchange(request);
|
||||
}
|
||||
|
||||
private ClientRequest.Builder initRequestBuilder() {
|
||||
return ClientRequest.method(this.httpMethod, this.uri).headers(initHeaders()).cookies(initCookies());
|
||||
}
|
||||
|
||||
private HttpHeaders initHeaders() {
|
||||
if (CollectionUtils.isEmpty(defaultHeaders) && CollectionUtils.isEmpty(this.headers)) {
|
||||
return null;
|
||||
}
|
||||
else if (CollectionUtils.isEmpty(defaultHeaders)) {
|
||||
return this.headers;
|
||||
}
|
||||
else if (CollectionUtils.isEmpty(this.headers)) {
|
||||
return defaultHeaders;
|
||||
}
|
||||
else {
|
||||
HttpHeaders result = new HttpHeaders();
|
||||
result.putAll(this.headers);
|
||||
defaultHeaders.forEach((name, values) -> {
|
||||
if (!this.headers.containsKey(name)) {
|
||||
values.forEach(value -> result.add(name, value));
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private MultiValueMap<String, String> initCookies() {
|
||||
if (CollectionUtils.isEmpty(defaultCookies) && CollectionUtils.isEmpty(this.cookies)) {
|
||||
return null;
|
||||
}
|
||||
else if (CollectionUtils.isEmpty(defaultCookies)) {
|
||||
return this.cookies;
|
||||
}
|
||||
else if (CollectionUtils.isEmpty(this.cookies)) {
|
||||
return defaultCookies;
|
||||
}
|
||||
else {
|
||||
MultiValueMap<String, String> result = new LinkedMultiValueMap<>();
|
||||
result.putAll(this.cookies);
|
||||
defaultCookies.forEach(result::putIfAbsent);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.function.client;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.util.DefaultUriBuilderFactory;
|
||||
import org.springframework.web.util.UriBuilderFactory;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link WebClient.Builder}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
class DefaultWebClientBuilder implements WebClient.Builder {
|
||||
|
||||
private UriBuilderFactory uriBuilderFactory;
|
||||
|
||||
private ClientHttpConnector connector;
|
||||
|
||||
private ExchangeStrategies exchangeStrategies = ExchangeStrategies.withDefaults();
|
||||
|
||||
private ExchangeFunction exchangeFunction;
|
||||
|
||||
private HttpHeaders defaultHeaders;
|
||||
|
||||
private MultiValueMap<String, String> defaultCookies;
|
||||
|
||||
|
||||
public DefaultWebClientBuilder(String baseUrl) {
|
||||
this(new DefaultUriBuilderFactory(baseUrl));
|
||||
}
|
||||
|
||||
public DefaultWebClientBuilder(UriBuilderFactory uriBuilderFactory) {
|
||||
Assert.notNull(uriBuilderFactory, "UriBuilderFactory is required.");
|
||||
this.uriBuilderFactory = uriBuilderFactory;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public WebClient.Builder clientConnector(ClientHttpConnector connector) {
|
||||
this.connector = connector;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebClient.Builder exchangeStrategies(ExchangeStrategies strategies) {
|
||||
Assert.notNull(strategies, "ExchangeStrategies is required.");
|
||||
this.exchangeStrategies = strategies;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebClient.Builder exchangeFunction(ExchangeFunction exchangeFunction) {
|
||||
this.exchangeFunction = exchangeFunction;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebClient.Builder defaultHeader(String headerName, String... headerValues) {
|
||||
if (this.defaultHeaders == null) {
|
||||
this.defaultHeaders = new HttpHeaders();
|
||||
}
|
||||
for (String headerValue : headerValues) {
|
||||
this.defaultHeaders.add(headerName, headerValue);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebClient.Builder defaultCookie(String cookieName, String... cookieValues) {
|
||||
if (this.defaultCookies == null) {
|
||||
this.defaultCookies = new LinkedMultiValueMap<>(4);
|
||||
}
|
||||
this.defaultCookies.addAll(cookieName, Arrays.asList(cookieValues));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebClient build() {
|
||||
return new DefaultWebClient(initExchangeFunction(),
|
||||
this.uriBuilderFactory, this.defaultHeaders, this.defaultCookies);
|
||||
}
|
||||
|
||||
private ExchangeFunction initExchangeFunction() {
|
||||
if (this.exchangeFunction != null) {
|
||||
return this.exchangeFunction;
|
||||
}
|
||||
else if (this.connector != null) {
|
||||
return ExchangeFunctions.create(this.connector, this.exchangeStrategies);
|
||||
}
|
||||
|
||||
else {
|
||||
return ExchangeFunctions.create(new ReactorClientHttpConnector(), this.exchangeStrategies);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.function.client;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Represents a function that filters an {@linkplain ExchangeFunction exchange function}.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @since 5.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ExchangeFilterFunction {
|
||||
|
||||
/**
|
||||
* Apply this filter to the given request and exchange function. The given
|
||||
* {@linkplain ExchangeFunction exchange function} represents the next entity in the
|
||||
* chain, and can be {@linkplain ExchangeFunction#exchange(ClientRequest) invoked} in order
|
||||
* to proceed to the exchange, or not invoked to block the chain.
|
||||
*
|
||||
* @param request the request
|
||||
* @param next the next exchange function in the chain
|
||||
* @return the filtered response
|
||||
*/
|
||||
Mono<ClientResponse> filter(ClientRequest<?> request, ExchangeFunction next);
|
||||
|
||||
/**
|
||||
* Return a composed filter function that first applies this filter, and then applies the
|
||||
* {@code after} filter.
|
||||
* @param after the filter to apply after this filter is applied
|
||||
* @return a composed filter that first applies this function and then applies the
|
||||
* {@code after} function
|
||||
*/
|
||||
default ExchangeFilterFunction andThen(ExchangeFilterFunction after) {
|
||||
Assert.notNull(after, "'after' must not be null");
|
||||
return (request, next) -> {
|
||||
ExchangeFunction nextExchange = exchangeRequest -> after.filter(exchangeRequest, next);
|
||||
return filter(request, nextExchange);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply this filter to the given exchange function, resulting in a filtered exchange function.
|
||||
* @param exchange the exchange function to filter
|
||||
* @return the filtered exchange function
|
||||
*/
|
||||
default ExchangeFunction apply(ExchangeFunction exchange) {
|
||||
Assert.notNull(exchange, "'exchange' must not be null");
|
||||
return request -> this.filter(request, exchange);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapt the given request processor function to a filter function that only operates on the
|
||||
* {@code ClientRequest}.
|
||||
* @param requestProcessor the request processor
|
||||
* @return the filter adaptation of the request processor
|
||||
*/
|
||||
static ExchangeFilterFunction ofRequestProcessor(Function<ClientRequest<?>,
|
||||
Mono<ClientRequest<?>>> requestProcessor) {
|
||||
|
||||
Assert.notNull(requestProcessor, "'requestProcessor' must not be null");
|
||||
return (request, next) -> requestProcessor.apply(request).then(next::exchange);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapt the given response processor function to a filter function that only operates on the
|
||||
* {@code ClientResponse}.
|
||||
* @param responseProcessor the response processor
|
||||
* @return the filter adaptation of the request processor
|
||||
*/
|
||||
static ExchangeFilterFunction ofResponseProcessor(Function<ClientResponse,
|
||||
Mono<ClientResponse>> responseProcessor) {
|
||||
|
||||
Assert.notNull(responseProcessor, "'responseProcessor' must not be null");
|
||||
return (request, next) -> next.exchange(request).then(responseProcessor);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.function.client;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Implementations of {@link ExchangeFilterFunction} that provide various useful request filter
|
||||
* operations, such as basic authentication, error handling, etc.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @author Arjen Poutsma
|
||||
* @since 5.0
|
||||
*/
|
||||
public abstract class ExchangeFilterFunctions {
|
||||
|
||||
/**
|
||||
* Return a filter that adds an Authorization header for HTTP Basic Authentication.
|
||||
* @param username the username to use
|
||||
* @param password the password to use
|
||||
* @return the {@link ExchangeFilterFunction} that adds the Authorization header
|
||||
*/
|
||||
public static ExchangeFilterFunction basicAuthentication(String username, String password) {
|
||||
Assert.notNull(username, "'username' must not be null");
|
||||
Assert.notNull(password, "'password' must not be null");
|
||||
|
||||
return ExchangeFilterFunction.ofRequestProcessor(
|
||||
clientRequest -> {
|
||||
String authorization = authorization(username, password);
|
||||
ClientRequest<?> authorizedRequest = ClientRequest.from(clientRequest)
|
||||
.header(HttpHeaders.AUTHORIZATION, authorization)
|
||||
.body(clientRequest.inserter());
|
||||
return Mono.just(authorizedRequest);
|
||||
});
|
||||
}
|
||||
|
||||
private static String authorization(String username, String password) {
|
||||
String credentials = username + ":" + password;
|
||||
byte[] credentialBytes = credentials.getBytes(StandardCharsets.ISO_8859_1);
|
||||
byte[] encodedBytes = Base64.getEncoder().encode(credentialBytes);
|
||||
String encodedCredentials = new String(encodedBytes, StandardCharsets.ISO_8859_1);
|
||||
return "Basic " + encodedCredentials;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.function.client;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Represents a function that exchanges a {@linkplain ClientRequest request} for a (delayed)
|
||||
* {@linkplain ClientResponse}. Can be used as an alternative to {@link WebClient}.
|
||||
* <p>For example:
|
||||
* <pre class="code">
|
||||
* ExchangeFunction exchangeFunction = ExchangeFunctions.create(new ReactorClientHttpConnector());
|
||||
* ClientRequest<Void> request = ClientRequest.method(HttpMethod.GET,
|
||||
* "http://example.com/resource").build();
|
||||
*
|
||||
* Mono<String> result = exchangeFunction
|
||||
* .exchange(request)
|
||||
* .then(response -> response.bodyToMono(String.class));
|
||||
* </pre>
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @since 5.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ExchangeFunction {
|
||||
|
||||
/**
|
||||
* Exchange the given request for a response mono.
|
||||
* @param request the request to exchange
|
||||
* @return the delayed response
|
||||
*/
|
||||
Mono<ClientResponse> exchange(ClientRequest<?> request);
|
||||
|
||||
/**
|
||||
* Filters this exchange function with the given {@code ExchangeFilterFunction}, resulting in a
|
||||
* filtered {@code ExchangeFunction}.
|
||||
* @param filter the filter to apply to this exchange
|
||||
* @return the filtered exchange
|
||||
* @see ExchangeFilterFunction#apply(ExchangeFunction)
|
||||
*/
|
||||
default ExchangeFunction filter(ExchangeFilterFunction filter) {
|
||||
Assert.notNull(filter, "'filter' must not be null");
|
||||
|
||||
return filter.apply(this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.function.client;
|
||||
|
||||
import java.util.logging.Level;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Exposes request-response exchange functionality, such as to
|
||||
* {@linkplain #create(ClientHttpConnector) create} an {@code ExchangeFunction} given a
|
||||
* {@code ClientHttpConnector}.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @since 5.0
|
||||
*/
|
||||
public abstract class ExchangeFunctions {
|
||||
|
||||
/**
|
||||
* Create a new {@link ExchangeFunction} with the given connector. This method uses
|
||||
* {@linkplain ExchangeStrategies#withDefaults() default strategies}.
|
||||
* @param connector the connector to create connections
|
||||
* @return the created function
|
||||
*/
|
||||
public static ExchangeFunction create(ClientHttpConnector connector) {
|
||||
return create(connector, ExchangeStrategies.withDefaults());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link ExchangeFunction} with the given connector and strategies.
|
||||
* @param connector the connector to create connections
|
||||
* @param strategies the strategies to use
|
||||
* @return the created function
|
||||
*/
|
||||
public static ExchangeFunction create(ClientHttpConnector connector,
|
||||
ExchangeStrategies strategies) {
|
||||
Assert.notNull(connector, "'connector' must not be null");
|
||||
Assert.notNull(strategies, "'strategies' must not be null");
|
||||
|
||||
return new DefaultExchangeFunction(connector, strategies);
|
||||
}
|
||||
|
||||
|
||||
private static class DefaultExchangeFunction implements ExchangeFunction {
|
||||
|
||||
private final ClientHttpConnector connector;
|
||||
|
||||
private final ExchangeStrategies strategies;
|
||||
|
||||
public DefaultExchangeFunction(
|
||||
ClientHttpConnector connector,
|
||||
ExchangeStrategies strategies) {
|
||||
this.connector = connector;
|
||||
this.strategies = strategies;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ClientResponse> exchange(ClientRequest<?> request) {
|
||||
Assert.notNull(request, "'request' must not be null");
|
||||
|
||||
return this.connector
|
||||
.connect(request.method(), request.url(),
|
||||
clientHttpRequest -> request.writeTo(clientHttpRequest, this.strategies))
|
||||
.log("org.springframework.web.reactive.function.client", Level.FINE)
|
||||
.map(clientHttpResponse -> new DefaultClientResponse(clientHttpResponse,
|
||||
this.strategies));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.function.client;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.codec.Decoder;
|
||||
import org.springframework.core.codec.Encoder;
|
||||
import org.springframework.http.codec.HttpMessageReader;
|
||||
import org.springframework.http.codec.HttpMessageWriter;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Defines the strategies for invoking {@link ExchangeFunction}s. An instance of
|
||||
* this class is immutable; instances are typically created through the mutable {@link Builder}:
|
||||
* either through {@link #builder()} to set up default strategies, or {@link #empty()} to start from
|
||||
* scratch. Alternatively, {@code ExchangeStrategies} instances can be created through
|
||||
* {@link #of(Supplier, Supplier)}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Arjen Poutsma
|
||||
* @since 5.0
|
||||
*/
|
||||
public interface ExchangeStrategies {
|
||||
|
||||
// Instance methods
|
||||
|
||||
/**
|
||||
* Supply a {@linkplain Stream stream} of {@link HttpMessageReader}s to be used for request
|
||||
* body conversion.
|
||||
* @return the stream of message readers
|
||||
*/
|
||||
Supplier<Stream<HttpMessageReader<?>>> messageReaders();
|
||||
|
||||
/**
|
||||
* Supply a {@linkplain Stream stream} of {@link HttpMessageWriter}s to be used for response
|
||||
* body conversion.
|
||||
* @return the stream of message writers
|
||||
*/
|
||||
Supplier<Stream<HttpMessageWriter<?>>> messageWriters();
|
||||
|
||||
|
||||
// Static methods
|
||||
|
||||
/**
|
||||
* Return a new {@code ExchangeStrategies} with default initialization.
|
||||
* @return the new {@code ExchangeStrategies}
|
||||
*/
|
||||
static ExchangeStrategies withDefaults() {
|
||||
return builder().build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@code ExchangeStrategies} based on the given
|
||||
* {@linkplain ApplicationContext application context}.
|
||||
* The returned supplier will search for all {@link HttpMessageReader}, and
|
||||
* {@link HttpMessageWriter} instances in the given application context and return them for
|
||||
* {@link #messageReaders()}, and {@link #messageWriters()} respectively.
|
||||
* @param applicationContext the application context to base the strategies on
|
||||
* @return the new {@code ExchangeStrategies}
|
||||
*/
|
||||
static ExchangeStrategies of(ApplicationContext applicationContext) {
|
||||
return builder(applicationContext).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@code ExchangeStrategies} described by the given supplier functions.
|
||||
* All provided supplier function parameters can be {@code null} to indicate an empty
|
||||
* stream is to be returned.
|
||||
* @param messageReaders the supplier function for {@link HttpMessageReader} instances (can be {@code null})
|
||||
* @param messageWriters the supplier function for {@link HttpMessageWriter} instances (can be {@code null})
|
||||
* @return the new {@code ExchangeStrategies}
|
||||
*/
|
||||
static ExchangeStrategies of(Supplier<Stream<HttpMessageReader<?>>> messageReaders,
|
||||
Supplier<Stream<HttpMessageWriter<?>>> messageWriters) {
|
||||
|
||||
return new ExchangeStrategies() {
|
||||
@Override
|
||||
public Supplier<Stream<HttpMessageReader<?>>> messageReaders() {
|
||||
return checkForNull(messageReaders);
|
||||
}
|
||||
@Override
|
||||
public Supplier<Stream<HttpMessageWriter<?>>> messageWriters() {
|
||||
return checkForNull(messageWriters);
|
||||
}
|
||||
private <T> Supplier<Stream<T>> checkForNull(Supplier<Stream<T>> supplier) {
|
||||
return supplier != null ? supplier : Stream::empty;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// Builder methods
|
||||
|
||||
/**
|
||||
* Return a mutable builder for a {@code ExchangeStrategies} with default initialization.
|
||||
* @return the builder
|
||||
*/
|
||||
static Builder builder() {
|
||||
DefaultExchangeStrategiesBuilder builder = new DefaultExchangeStrategiesBuilder();
|
||||
builder.defaultConfiguration();
|
||||
return builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a mutable builder based on the given {@linkplain ApplicationContext application context}.
|
||||
* The returned builder will search for all {@link HttpMessageReader}, and
|
||||
* {@link HttpMessageWriter} instances in the given application context and return them for
|
||||
* {@link #messageReaders()}, and {@link #messageWriters()}.
|
||||
* @param applicationContext the application context to base the strategies on
|
||||
* @return the builder
|
||||
*/
|
||||
static Builder builder(ApplicationContext applicationContext) {
|
||||
Assert.notNull(applicationContext, "ApplicationContext must not be null");
|
||||
DefaultExchangeStrategiesBuilder builder = new DefaultExchangeStrategiesBuilder();
|
||||
builder.applicationContext(applicationContext);
|
||||
return builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a mutable, empty builder for a {@code ExchangeStrategies}.
|
||||
* @return the builder
|
||||
*/
|
||||
static Builder empty() {
|
||||
return new DefaultExchangeStrategiesBuilder();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A mutable builder for a {@link ExchangeStrategies}.
|
||||
*/
|
||||
interface Builder {
|
||||
|
||||
/**
|
||||
* Add the given message reader to this builder.
|
||||
* @param messageReader the message reader to add
|
||||
* @return this builder
|
||||
*/
|
||||
Builder messageReader(HttpMessageReader<?> messageReader);
|
||||
|
||||
/**
|
||||
* Add the given decoder to this builder. This is a convenient alternative to adding a
|
||||
* {@link org.springframework.http.codec.DecoderHttpMessageReader} that wraps the given
|
||||
* decoder.
|
||||
* @param decoder the decoder to add
|
||||
* @return this builder
|
||||
*/
|
||||
Builder decoder(Decoder<?> decoder);
|
||||
|
||||
/**
|
||||
* Add the given message writer to this builder.
|
||||
* @param messageWriter the message writer to add
|
||||
* @return this builder
|
||||
*/
|
||||
Builder messageWriter(HttpMessageWriter<?> messageWriter);
|
||||
|
||||
/**
|
||||
* Add the given encoder to this builder. This is a convenient alternative to adding a
|
||||
* {@link org.springframework.http.codec.EncoderHttpMessageWriter} that wraps the given
|
||||
* encoder.
|
||||
* @param encoder the encoder to add
|
||||
* @return this builder
|
||||
*/
|
||||
Builder encoder(Encoder<?> encoder);
|
||||
|
||||
/**
|
||||
* Builds the {@link ExchangeStrategies}.
|
||||
* @return the built strategies
|
||||
*/
|
||||
ExchangeStrategies build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.function.client;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.charset.Charset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
import org.springframework.http.client.reactive.ClientHttpRequest;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.reactive.function.BodyInserter;
|
||||
import org.springframework.web.util.DefaultUriBuilderFactory;
|
||||
import org.springframework.web.util.UriBuilderFactory;
|
||||
|
||||
/**
|
||||
* The main class for performing Web requests.
|
||||
*
|
||||
* <pre class="code">
|
||||
*
|
||||
* // Create ExchangeFunction (application-wide)
|
||||
*
|
||||
* ClientHttpConnector connector = new ReactorClientHttpConnector();
|
||||
* ExchangeFunction exchangeFunction = ExchangeFunctions.create(connector);
|
||||
*
|
||||
* // Create WebClient (per base URI)
|
||||
*
|
||||
* String baseUri = "http://abc.com";
|
||||
* UriBuilderFactory factory = new DefaultUriBuilderFactory(baseUri);
|
||||
* WebClient operations = WebClient.create(exchangeFunction, factory);
|
||||
*
|
||||
* // Perform requests...
|
||||
*
|
||||
* Mono<String> result = operations.get()
|
||||
* .uri("/foo")
|
||||
* .exchange()
|
||||
* .then(response -> response.bodyToMono(String.class));
|
||||
* </pre>
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Arjen Poutsma
|
||||
* @since 5.0
|
||||
*/
|
||||
public interface WebClient {
|
||||
|
||||
/**
|
||||
* Prepare an HTTP GET request.
|
||||
* @return a spec for specifying the target URL
|
||||
*/
|
||||
UriSpec get();
|
||||
|
||||
/**
|
||||
* Prepare an HTTP HEAD request.
|
||||
* @return a spec for specifying the target URL
|
||||
*/
|
||||
UriSpec head();
|
||||
|
||||
/**
|
||||
* Prepare an HTTP POST request.
|
||||
* @return a spec for specifying the target URL
|
||||
*/
|
||||
UriSpec post();
|
||||
|
||||
/**
|
||||
* Prepare an HTTP PUT request.
|
||||
* @return a spec for specifying the target URL
|
||||
*/
|
||||
UriSpec put();
|
||||
|
||||
/**
|
||||
* Prepare an HTTP PATCH request.
|
||||
* @return a spec for specifying the target URL
|
||||
*/
|
||||
UriSpec patch();
|
||||
|
||||
/**
|
||||
* Prepare an HTTP DELETE request.
|
||||
* @return a spec for specifying the target URL
|
||||
*/
|
||||
UriSpec delete();
|
||||
|
||||
/**
|
||||
* Prepare an HTTP OPTIONS request.
|
||||
* @return a spec for specifying the target URL
|
||||
*/
|
||||
UriSpec options();
|
||||
|
||||
|
||||
/**
|
||||
* Filter the client with the given {@code ExchangeFilterFunction}.
|
||||
* @param filterFunction the filter to apply to this client
|
||||
* @return the filtered client
|
||||
* @see ExchangeFilterFunction#apply(ExchangeFunction)
|
||||
*/
|
||||
WebClient filter(ExchangeFilterFunction filterFunction);
|
||||
|
||||
|
||||
// Static, factory methods
|
||||
|
||||
/**
|
||||
* Shortcut for:
|
||||
* <pre class="code">
|
||||
* WebClient client = builder(baseUrl).build();
|
||||
* </pre>
|
||||
* @param baseUrl the base URI for all requests
|
||||
*/
|
||||
static WebClient create(String baseUrl) {
|
||||
return new DefaultWebClientBuilder(baseUrl).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain a {@code WebClient} builder with a base URI to be used as the
|
||||
* base for expanding URI templates during exchanges. The given String
|
||||
* is used to create an instance of {@link DefaultUriBuilderFactory} whose
|
||||
* {@link DefaultUriBuilderFactory#DefaultUriBuilderFactory(String)
|
||||
* constructor} provides more details on how the base URI is applied.
|
||||
* @param baseUrl the base URI for all requests
|
||||
*/
|
||||
static WebClient.Builder builder(String baseUrl) {
|
||||
return new DefaultWebClientBuilder(baseUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain a {@code WebClient} builder with the {@link UriBuilderFactory}
|
||||
* to use for expanding URI templates during exchanges.
|
||||
* @param uriBuilderFactory the factory to use
|
||||
*/
|
||||
static WebClient.Builder builder(UriBuilderFactory uriBuilderFactory) {
|
||||
return new DefaultWebClientBuilder(uriBuilderFactory);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A mutable builder for a {@link WebClient}.
|
||||
*/
|
||||
interface Builder {
|
||||
|
||||
/**
|
||||
* Configure the {@link ClientHttpConnector} to use.
|
||||
* <p>By default an instance of
|
||||
* {@link org.springframework.http.client.reactive.ReactorClientHttpConnector
|
||||
* ReactorClientHttpConnector} is created if this is not set. However a
|
||||
* shared instance may be passed instead, e.g. for use with multiple
|
||||
* {@code WebClient}'s targeting different base URIs.
|
||||
* @param connector the connector to use
|
||||
*/
|
||||
Builder clientConnector(ClientHttpConnector connector);
|
||||
|
||||
/**
|
||||
* Configure the {@link ExchangeStrategies} to use.
|
||||
* <p>By default {@link ExchangeStrategies#withDefaults()} is used.
|
||||
* @param strategies the strategies to use
|
||||
*/
|
||||
Builder exchangeStrategies(ExchangeStrategies strategies);
|
||||
|
||||
/**
|
||||
* Configure directly an {@link ExchangeFunction} instead of separately
|
||||
* providing a {@link ClientHttpConnector} and/or
|
||||
* {@link ExchangeStrategies}.
|
||||
* @param exchangeFunction the exchange function to use
|
||||
*/
|
||||
Builder exchangeFunction(ExchangeFunction exchangeFunction);
|
||||
|
||||
/**
|
||||
* Add the given header to all requests that haven't added it.
|
||||
* @param headerName the header name
|
||||
* @param headerValues the header values
|
||||
*/
|
||||
Builder defaultHeader(String headerName, String... headerValues);
|
||||
|
||||
/**
|
||||
* Add the given header to all requests that haven't added it.
|
||||
* @param cookieName the cookie name
|
||||
* @param cookieValues the cookie values
|
||||
*/
|
||||
Builder defaultCookie(String cookieName, String... cookieValues);
|
||||
|
||||
/**
|
||||
* Builder the {@link WebClient} instance.
|
||||
*/
|
||||
WebClient build();
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Contract for specifying the URI for a request.
|
||||
*/
|
||||
interface UriSpec {
|
||||
|
||||
/**
|
||||
* Specify the URI using an absolute, fully constructed {@link URI}.
|
||||
*/
|
||||
HeaderSpec uri(URI uri);
|
||||
|
||||
/**
|
||||
* Specify the URI for the request using a URI template and URI variables.
|
||||
* If a {@link UriBuilderFactory} was configured for the client (e.g.
|
||||
* with a base URI) it will be used to expand the URI template.
|
||||
* @see #builder(String)
|
||||
*/
|
||||
HeaderSpec uri(String uri, Object... uriVariables);
|
||||
|
||||
/**
|
||||
* Build the URI for the request using the {@link UriBuilderFactory}
|
||||
* configured for this client.
|
||||
* @see #builder(String)
|
||||
*/
|
||||
HeaderSpec uri(Function<UriBuilderFactory, URI> uriFunction);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Contract for specifying request headers leading up to the exchange.
|
||||
*/
|
||||
interface HeaderSpec {
|
||||
|
||||
/**
|
||||
* Set the list of acceptable {@linkplain MediaType media types}, as
|
||||
* specified by the {@code Accept} header.
|
||||
* @param acceptableMediaTypes the acceptable media types
|
||||
* @return this builder
|
||||
*/
|
||||
HeaderSpec accept(MediaType... acceptableMediaTypes);
|
||||
|
||||
/**
|
||||
* Set the list of acceptable {@linkplain Charset charsets}, as specified
|
||||
* by the {@code Accept-Charset} header.
|
||||
* @param acceptableCharsets the acceptable charsets
|
||||
* @return this builder
|
||||
*/
|
||||
HeaderSpec acceptCharset(Charset... acceptableCharsets);
|
||||
|
||||
/**
|
||||
* Set the length of the body in bytes, as specified by the
|
||||
* {@code Content-Length} header.
|
||||
* @param contentLength the content length
|
||||
* @return this builder
|
||||
* @see HttpHeaders#setContentLength(long)
|
||||
*/
|
||||
HeaderSpec contentLength(long contentLength);
|
||||
|
||||
/**
|
||||
* Set the {@linkplain MediaType media type} of the body, as specified
|
||||
* by the {@code Content-Type} header.
|
||||
* @param contentType the content type
|
||||
* @return this builder
|
||||
* @see HttpHeaders#setContentType(MediaType)
|
||||
*/
|
||||
HeaderSpec contentType(MediaType contentType);
|
||||
|
||||
/**
|
||||
* Add a cookie with the given name and value.
|
||||
* @param name the cookie name
|
||||
* @param value the cookie value
|
||||
* @return this builder
|
||||
*/
|
||||
HeaderSpec cookie(String name, String value);
|
||||
|
||||
/**
|
||||
* Copy the given cookies into the entity's cookies map.
|
||||
*
|
||||
* @param cookies the existing cookies to copy from
|
||||
* @return this builder
|
||||
*/
|
||||
HeaderSpec cookies(MultiValueMap<String, String> cookies);
|
||||
|
||||
/**
|
||||
* Set the value of the {@code If-Modified-Since} header.
|
||||
* <p>The date should be specified as the number of milliseconds since
|
||||
* January 1, 1970 GMT.
|
||||
* @param ifModifiedSince the new value of the header
|
||||
* @return this builder
|
||||
*/
|
||||
HeaderSpec ifModifiedSince(ZonedDateTime ifModifiedSince);
|
||||
|
||||
/**
|
||||
* Set the values of the {@code If-None-Match} header.
|
||||
* @param ifNoneMatches the new value of the header
|
||||
* @return this builder
|
||||
*/
|
||||
HeaderSpec ifNoneMatch(String... ifNoneMatches);
|
||||
|
||||
/**
|
||||
* Add the given, single header value under the given name.
|
||||
* @param headerName the header name
|
||||
* @param headerValues the header value(s)
|
||||
* @return this builder
|
||||
*/
|
||||
HeaderSpec header(String headerName, String... headerValues);
|
||||
|
||||
/**
|
||||
* Copy the given headers into the entity's headers map.
|
||||
* @param headers the existing headers to copy from
|
||||
* @return this builder
|
||||
*/
|
||||
HeaderSpec headers(HttpHeaders headers);
|
||||
|
||||
/**
|
||||
* Perform the request without a request body.
|
||||
* @return a {@code Mono} with the response
|
||||
*/
|
||||
Mono<ClientResponse> exchange();
|
||||
|
||||
/**
|
||||
* Set the body of the request to the given {@code BodyInserter} and
|
||||
* perform the request.
|
||||
* @param inserter the {@code BodyInserter} that writes to the request
|
||||
* @param <T> the type contained in the body
|
||||
* @return a {@code Mono} with the response
|
||||
*/
|
||||
<T> Mono<ClientResponse> exchange(BodyInserter<T, ? super ClientHttpRequest> inserter);
|
||||
|
||||
/**
|
||||
* Set the body of the request to the given {@code Publisher} and
|
||||
* perform the request.
|
||||
* @param publisher the {@code Publisher} to write to the request
|
||||
* @param elementClass the class of elements contained in the publisher
|
||||
* @param <T> the type of the elements contained in the publisher
|
||||
* @param <S> the type of the {@code Publisher}
|
||||
* @return a {@code Mono} with the response
|
||||
*/
|
||||
<T, S extends Publisher<T>> Mono<ClientResponse> exchange(S publisher, Class<T> elementClass);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.function.client;
|
||||
|
||||
import org.springframework.core.NestedRuntimeException;
|
||||
|
||||
/**
|
||||
* Exception published by {@link WebClient} in case of errors.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @since 5.0
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class WebClientException extends NestedRuntimeException {
|
||||
|
||||
/**
|
||||
* Construct a new instance of {@code WebClientException} with the given message.
|
||||
* @param msg the message
|
||||
*/
|
||||
public WebClientException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new instance of {@code WebClientException} with the given message and
|
||||
* exception.
|
||||
* @param msg the message
|
||||
* @param ex the exception
|
||||
*/
|
||||
public WebClientException(String msg, Throwable ex) {
|
||||
super(msg, ex);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Provides a reactive {@link org.springframework.web.reactive.function.client.WebClient}
|
||||
* that builds on top of the
|
||||
* {@code org.springframework.http.client.reactive} reactive HTTP adapter layer.
|
||||
*/
|
||||
package org.springframework.web.reactive.function.client;
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Provides a foundation for both the reactive client and server subpackages.
|
||||
*/
|
||||
package org.springframework.web.reactive.function;
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.function.server;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.codec.ByteArrayDecoder;
|
||||
import org.springframework.core.codec.ByteArrayEncoder;
|
||||
import org.springframework.core.codec.ByteBufferDecoder;
|
||||
import org.springframework.core.codec.ByteBufferEncoder;
|
||||
import org.springframework.core.codec.CharSequenceEncoder;
|
||||
import org.springframework.core.codec.StringDecoder;
|
||||
import org.springframework.http.codec.DecoderHttpMessageReader;
|
||||
import org.springframework.http.codec.EncoderHttpMessageWriter;
|
||||
import org.springframework.http.codec.FormHttpMessageReader;
|
||||
import org.springframework.http.codec.HttpMessageReader;
|
||||
import org.springframework.http.codec.HttpMessageWriter;
|
||||
import org.springframework.http.codec.ResourceHttpMessageWriter;
|
||||
import org.springframework.http.codec.ServerSentEventHttpMessageWriter;
|
||||
import org.springframework.http.codec.json.Jackson2JsonDecoder;
|
||||
import org.springframework.http.codec.json.Jackson2JsonEncoder;
|
||||
import org.springframework.http.codec.xml.Jaxb2XmlDecoder;
|
||||
import org.springframework.http.codec.xml.Jaxb2XmlEncoder;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.web.reactive.result.view.ViewResolver;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link HandlerStrategies.Builder}.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @since 5.0
|
||||
*/
|
||||
class DefaultHandlerStrategiesBuilder implements HandlerStrategies.Builder {
|
||||
|
||||
private static final boolean jackson2Present =
|
||||
ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper",
|
||||
DefaultHandlerStrategiesBuilder.class.getClassLoader()) &&
|
||||
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator",
|
||||
DefaultHandlerStrategiesBuilder.class.getClassLoader());
|
||||
|
||||
private static final boolean jaxb2Present =
|
||||
ClassUtils.isPresent("javax.xml.bind.Binder",
|
||||
DefaultHandlerStrategiesBuilder.class.getClassLoader());
|
||||
|
||||
|
||||
private final List<HttpMessageReader<?>> messageReaders = new ArrayList<>();
|
||||
|
||||
private final List<HttpMessageWriter<?>> messageWriters = new ArrayList<>();
|
||||
|
||||
private final List<ViewResolver> viewResolvers = new ArrayList<>();
|
||||
|
||||
public void defaultConfiguration() {
|
||||
messageReader(new DecoderHttpMessageReader<>(new ByteArrayDecoder()));
|
||||
messageReader(new DecoderHttpMessageReader<>(new ByteBufferDecoder()));
|
||||
messageReader(new DecoderHttpMessageReader<>(new StringDecoder()));
|
||||
messageReader(new FormHttpMessageReader());
|
||||
|
||||
messageWriter(new EncoderHttpMessageWriter<>(new ByteArrayEncoder()));
|
||||
messageWriter(new EncoderHttpMessageWriter<>(new ByteBufferEncoder()));
|
||||
messageWriter(new EncoderHttpMessageWriter<>(new CharSequenceEncoder()));
|
||||
messageWriter(new ResourceHttpMessageWriter());
|
||||
|
||||
if (jaxb2Present) {
|
||||
messageReader(new DecoderHttpMessageReader<>(new Jaxb2XmlDecoder()));
|
||||
messageWriter(new EncoderHttpMessageWriter<>(new Jaxb2XmlEncoder()));
|
||||
}
|
||||
if (jackson2Present) {
|
||||
messageReader(new DecoderHttpMessageReader<>(new Jackson2JsonDecoder()));
|
||||
Jackson2JsonEncoder jsonEncoder = new Jackson2JsonEncoder();
|
||||
messageWriter(new EncoderHttpMessageWriter<>(jsonEncoder));
|
||||
messageWriter(
|
||||
new ServerSentEventHttpMessageWriter(Collections.singletonList(jsonEncoder)));
|
||||
}
|
||||
else {
|
||||
messageWriter(new ServerSentEventHttpMessageWriter());
|
||||
}
|
||||
}
|
||||
|
||||
public void applicationContext(ApplicationContext applicationContext) {
|
||||
applicationContext.getBeansOfType(HttpMessageReader.class).values().forEach(this::messageReader);
|
||||
applicationContext.getBeansOfType(HttpMessageWriter.class).values().forEach(this::messageWriter);
|
||||
applicationContext.getBeansOfType(ViewResolver.class).values().forEach(this::viewResolver);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandlerStrategies.Builder messageReader(HttpMessageReader<?> messageReader) {
|
||||
Assert.notNull(messageReader, "'messageReader' must not be null");
|
||||
this.messageReaders.add(messageReader);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandlerStrategies.Builder messageWriter(HttpMessageWriter<?> messageWriter) {
|
||||
Assert.notNull(messageWriter, "'messageWriter' must not be null");
|
||||
this.messageWriters.add(messageWriter);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandlerStrategies.Builder viewResolver(ViewResolver viewResolver) {
|
||||
Assert.notNull(viewResolver, "'viewResolver' must not be null");
|
||||
this.viewResolvers.add(viewResolver);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandlerStrategies build() {
|
||||
return new DefaultHandlerStrategies(this.messageReaders, this.messageWriters,
|
||||
this.viewResolvers);
|
||||
}
|
||||
|
||||
private static class DefaultHandlerStrategies implements HandlerStrategies {
|
||||
|
||||
private final List<HttpMessageReader<?>> messageReaders;
|
||||
|
||||
private final List<HttpMessageWriter<?>> messageWriters;
|
||||
|
||||
private final List<ViewResolver> viewResolvers;
|
||||
|
||||
public DefaultHandlerStrategies(
|
||||
List<HttpMessageReader<?>> messageReaders,
|
||||
List<HttpMessageWriter<?>> messageWriters,
|
||||
List<ViewResolver> viewResolvers) {
|
||||
this.messageReaders = unmodifiableCopy(messageReaders);
|
||||
this.messageWriters = unmodifiableCopy(messageWriters);
|
||||
this.viewResolvers = unmodifiableCopy(viewResolvers);
|
||||
}
|
||||
|
||||
private static <T> List<T> unmodifiableCopy(List<? extends T> list) {
|
||||
return Collections.unmodifiableList(new ArrayList<>(list));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Supplier<Stream<HttpMessageReader<?>>> messageReaders() {
|
||||
return this.messageReaders::stream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Supplier<Stream<HttpMessageWriter<?>>> messageWriters() {
|
||||
return this.messageWriters::stream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Supplier<Stream<ViewResolver>> viewResolvers() {
|
||||
return this.viewResolvers::stream;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.function.server;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalLong;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpRange;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.HttpMessageReader;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.reactive.function.BodyExtractor;
|
||||
import org.springframework.web.reactive.function.BodyExtractors;
|
||||
import org.springframework.web.reactive.function.UnsupportedMediaTypeException;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.UnsupportedMediaTypeStatusException;
|
||||
import org.springframework.web.server.WebSession;
|
||||
|
||||
/**
|
||||
* {@code ServerRequest} implementation based on a {@link ServerWebExchange}.
|
||||
* @author Arjen Poutsma
|
||||
*/
|
||||
class DefaultServerRequest implements ServerRequest {
|
||||
|
||||
private static final Function<UnsupportedMediaTypeException, UnsupportedMediaTypeStatusException> ERROR_MAPPER =
|
||||
ex -> ex.getContentType()
|
||||
.map(contentType -> new UnsupportedMediaTypeStatusException(contentType,
|
||||
ex.getSupportedMediaTypes()))
|
||||
.orElseGet(() -> new UnsupportedMediaTypeStatusException(ex.getMessage()));
|
||||
|
||||
private final ServerWebExchange exchange;
|
||||
|
||||
private final Headers headers;
|
||||
|
||||
private final HandlerStrategies strategies;
|
||||
|
||||
|
||||
DefaultServerRequest(ServerWebExchange exchange, HandlerStrategies strategies) {
|
||||
this.exchange = exchange;
|
||||
this.strategies = strategies;
|
||||
this.headers = new DefaultHeaders();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public HttpMethod method() {
|
||||
return request().getMethod();
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI uri() {
|
||||
return request().getURI();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Headers headers() {
|
||||
return this.headers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T body(BodyExtractor<T, ? super ServerHttpRequest> extractor) {
|
||||
return body(extractor, Collections.emptyMap());
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T body(BodyExtractor<T, ? super ServerHttpRequest> extractor, Map<String, Object> hints) {
|
||||
Assert.notNull(extractor, "'extractor' must not be null");
|
||||
return extractor.extract(request(),
|
||||
new BodyExtractor.Context() {
|
||||
@Override
|
||||
public Supplier<Stream<HttpMessageReader<?>>> messageReaders() {
|
||||
return DefaultServerRequest.this.strategies.messageReaders();
|
||||
}
|
||||
@Override
|
||||
public Map<String, Object> hints() {
|
||||
return hints;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Mono<T> bodyToMono(Class<? extends T> elementClass) {
|
||||
Mono<T> mono = body(BodyExtractors.toMono(elementClass));
|
||||
return mono.mapError(UnsupportedMediaTypeException.class, ERROR_MAPPER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Flux<T> bodyToFlux(Class<? extends T> elementClass) {
|
||||
Flux<T> flux = body(BodyExtractors.toFlux(elementClass));
|
||||
return flux.mapError(UnsupportedMediaTypeException.class, ERROR_MAPPER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Optional<T> attribute(String name) {
|
||||
return this.exchange.getAttribute(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> queryParams(String name) {
|
||||
List<String> queryParams = request().getQueryParams().get(name);
|
||||
return queryParams != null ? queryParams : Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> pathVariables() {
|
||||
return this.exchange.<Map<String, String>>getAttribute(RouterFunctions.URI_TEMPLATE_VARIABLES_ATTRIBUTE).
|
||||
orElseGet(Collections::emptyMap);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<WebSession> session() {
|
||||
return this.exchange.getSession();
|
||||
}
|
||||
|
||||
private ServerHttpRequest request() {
|
||||
return this.exchange.getRequest();
|
||||
}
|
||||
|
||||
ServerWebExchange exchange() {
|
||||
return this.exchange;
|
||||
}
|
||||
|
||||
|
||||
private class DefaultHeaders implements Headers {
|
||||
|
||||
private HttpHeaders delegate() {
|
||||
return request().getHeaders();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MediaType> accept() {
|
||||
return delegate().getAccept();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Charset> acceptCharset() {
|
||||
return delegate().getAcceptCharset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionalLong contentLength() {
|
||||
long value = delegate().getContentLength();
|
||||
return (value != -1 ? OptionalLong.of(value) : OptionalLong.empty());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<MediaType> contentType() {
|
||||
return Optional.ofNullable(delegate().getContentType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public InetSocketAddress host() {
|
||||
return delegate().getHost();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<HttpRange> range() {
|
||||
return delegate().getRange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> header(String headerName) {
|
||||
List<String> headerValues = delegate().get(headerName);
|
||||
return (headerValues != null ? headerValues : Collections.emptyList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders asHttpHeaders() {
|
||||
return HttpHeaders.readOnlyHttpHeaders(delegate());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.function.server;
|
||||
|
||||
import java.net.URI;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.Conventions;
|
||||
import org.springframework.http.CacheControl;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.HttpMessageWriter;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.web.reactive.function.BodyInserter;
|
||||
import org.springframework.web.reactive.function.BodyInserters;
|
||||
import org.springframework.web.reactive.result.view.ViewResolver;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* Default {@link ServerResponse.BodyBuilder} implementation.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
*/
|
||||
class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder {
|
||||
|
||||
private final HttpStatus statusCode;
|
||||
|
||||
private final HttpHeaders headers = new HttpHeaders();
|
||||
|
||||
private final Map<String, Object> hints = new HashMap<>();
|
||||
|
||||
|
||||
public DefaultServerResponseBuilder(HttpStatus statusCode) {
|
||||
this.statusCode = statusCode;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public ServerResponse.BodyBuilder header(String headerName, String... headerValues) {
|
||||
for (String headerValue : headerValues) {
|
||||
this.headers.add(headerName, headerValue);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerResponse.BodyBuilder headers(HttpHeaders headers) {
|
||||
if (headers != null) {
|
||||
this.headers.putAll(headers);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerResponse.BodyBuilder allow(HttpMethod... allowedMethods) {
|
||||
this.headers.setAllow(new LinkedHashSet<>(Arrays.asList(allowedMethods)));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerResponse.BodyBuilder allow(Set<HttpMethod> allowedMethods) {
|
||||
this.headers.setAllow(allowedMethods);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerResponse.BodyBuilder contentLength(long contentLength) {
|
||||
this.headers.setContentLength(contentLength);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerResponse.BodyBuilder contentType(MediaType contentType) {
|
||||
this.headers.setContentType(contentType);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerResponse.BodyBuilder eTag(String eTag) {
|
||||
if (eTag != null) {
|
||||
if (!eTag.startsWith("\"") && !eTag.startsWith("W/\"")) {
|
||||
eTag = "\"" + eTag;
|
||||
}
|
||||
if (!eTag.endsWith("\"")) {
|
||||
eTag = eTag + "\"";
|
||||
}
|
||||
}
|
||||
this.headers.setETag(eTag);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerResponse.BodyBuilder hint(String key, Object value) {
|
||||
this.hints.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerResponse.BodyBuilder lastModified(ZonedDateTime lastModified) {
|
||||
ZonedDateTime gmt = lastModified.withZoneSameInstant(ZoneId.of("GMT"));
|
||||
String headerValue = DateTimeFormatter.RFC_1123_DATE_TIME.format(gmt);
|
||||
this.headers.set(HttpHeaders.LAST_MODIFIED, headerValue);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerResponse.BodyBuilder location(URI location) {
|
||||
this.headers.setLocation(location);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerResponse.BodyBuilder cacheControl(CacheControl cacheControl) {
|
||||
String ccValue = cacheControl.getHeaderValue();
|
||||
if (ccValue != null) {
|
||||
this.headers.setCacheControl(cacheControl.getHeaderValue());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerResponse.BodyBuilder varyBy(String... requestHeaders) {
|
||||
this.headers.setVary(Arrays.asList(requestHeaders));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ServerResponse> build() {
|
||||
return build((exchange, handlerStrategies) -> exchange.getResponse().setComplete());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ServerResponse> build(Publisher<Void> voidPublisher) {
|
||||
Assert.notNull(voidPublisher, "'voidPublisher' must not be null");
|
||||
return build((exchange, handlerStrategies) ->
|
||||
Mono.from(voidPublisher).then(exchange.getResponse().setComplete()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ServerResponse> build(
|
||||
BiFunction<ServerWebExchange, HandlerStrategies, Mono<Void>> writeFunction) {
|
||||
|
||||
Assert.notNull(writeFunction, "'writeFunction' must not be null");
|
||||
return Mono.just(new WriterFunctionServerResponse(this.statusCode, this.headers,
|
||||
writeFunction));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T, P extends Publisher<T>> Mono<ServerResponse> body(P publisher,
|
||||
Class<T> elementClass) {
|
||||
return body(BodyInserters.fromPublisher(publisher, elementClass));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Mono<ServerResponse> body(BodyInserter<T, ? super ServerHttpResponse> inserter) {
|
||||
Assert.notNull(inserter, "'inserter' must not be null");
|
||||
return Mono
|
||||
.just(new BodyInserterServerResponse<T>(this.statusCode, this.headers, inserter, this.hints));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ServerResponse> render(String name, Object... modelAttributes) {
|
||||
Assert.hasLength(name, "'name' must not be empty");
|
||||
return render(name, toModelMap(modelAttributes));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ServerResponse> render(String name, Map<String, ?> model) {
|
||||
Assert.hasLength(name, "'name' must not be empty");
|
||||
Map<String, Object> modelMap = new LinkedHashMap<>();
|
||||
if (model != null) {
|
||||
modelMap.putAll(model);
|
||||
}
|
||||
return Mono
|
||||
.just(new RenderingServerResponse(this.statusCode, this.headers, name, modelMap));
|
||||
}
|
||||
|
||||
private Map<String, Object> toModelMap(Object[] modelAttributes) {
|
||||
if (ObjectUtils.isEmpty(modelAttributes)) {
|
||||
return null;
|
||||
}
|
||||
return Arrays.stream(modelAttributes)
|
||||
.filter(val -> !ObjectUtils.isEmpty(val))
|
||||
.collect(Collectors.toMap(Conventions::getVariableName, val -> val));
|
||||
}
|
||||
|
||||
|
||||
private static abstract class AbstractServerResponse implements ServerResponse {
|
||||
|
||||
private final HttpStatus statusCode;
|
||||
|
||||
private final HttpHeaders headers;
|
||||
|
||||
protected AbstractServerResponse(HttpStatus statusCode, HttpHeaders headers) {
|
||||
this.statusCode = statusCode;
|
||||
this.headers = readOnlyCopy(headers);
|
||||
}
|
||||
|
||||
private static HttpHeaders readOnlyCopy(HttpHeaders headers) {
|
||||
HttpHeaders copy = new HttpHeaders();
|
||||
copy.putAll(headers);
|
||||
return HttpHeaders.readOnlyHttpHeaders(copy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final HttpStatus statusCode() {
|
||||
return this.statusCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final HttpHeaders headers() {
|
||||
return this.headers;
|
||||
}
|
||||
|
||||
protected void writeStatusAndHeaders(ServerHttpResponse response) {
|
||||
response.setStatusCode(this.statusCode);
|
||||
HttpHeaders responseHeaders = response.getHeaders();
|
||||
|
||||
if (!this.headers.isEmpty()) {
|
||||
this.headers.entrySet().stream()
|
||||
.filter(entry -> !responseHeaders.containsKey(entry.getKey()))
|
||||
.forEach(entry -> responseHeaders
|
||||
.put(entry.getKey(), entry.getValue()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class WriterFunctionServerResponse extends AbstractServerResponse {
|
||||
|
||||
private final BiFunction<ServerWebExchange, HandlerStrategies, Mono<Void>> writeFunction;
|
||||
|
||||
public WriterFunctionServerResponse(HttpStatus statusCode,
|
||||
HttpHeaders headers,
|
||||
BiFunction<ServerWebExchange, HandlerStrategies, Mono<Void>> writeFunction) {
|
||||
super(statusCode, headers);
|
||||
this.writeFunction = writeFunction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> writeTo(ServerWebExchange exchange, HandlerStrategies strategies) {
|
||||
writeStatusAndHeaders(exchange.getResponse());
|
||||
return this.writeFunction.apply(exchange, strategies);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static final class BodyInserterServerResponse<T> extends AbstractServerResponse {
|
||||
|
||||
private final BodyInserter<T, ? super ServerHttpResponse> inserter;
|
||||
|
||||
private final Map<String, Object> hints;
|
||||
|
||||
public BodyInserterServerResponse(HttpStatus statusCode, HttpHeaders headers,
|
||||
BodyInserter<T, ? super ServerHttpResponse> inserter, Map<String, Object> hints) {
|
||||
|
||||
super(statusCode, headers);
|
||||
this.inserter = inserter;
|
||||
this.hints = hints;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> writeTo(ServerWebExchange exchange, HandlerStrategies strategies) {
|
||||
ServerHttpResponse response = exchange.getResponse();
|
||||
writeStatusAndHeaders(response);
|
||||
return this.inserter.insert(response, new BodyInserter.Context() {
|
||||
@Override
|
||||
public Supplier<Stream<HttpMessageWriter<?>>> messageWriters() {
|
||||
return strategies.messageWriters();
|
||||
}
|
||||
@Override
|
||||
public Map<String, Object> hints() {
|
||||
return hints;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static final class RenderingServerResponse extends AbstractServerResponse {
|
||||
|
||||
private final String name;
|
||||
|
||||
private final Map<String, Object> model;
|
||||
|
||||
|
||||
public RenderingServerResponse(HttpStatus statusCode, HttpHeaders headers, String name,
|
||||
Map<String, Object> model) {
|
||||
|
||||
super(statusCode, headers);
|
||||
this.name = name;
|
||||
this.model = Collections.unmodifiableMap(model);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> writeTo(ServerWebExchange exchange, HandlerStrategies strategies) {
|
||||
ServerHttpResponse response = exchange.getResponse();
|
||||
writeStatusAndHeaders(response);
|
||||
MediaType contentType = exchange.getResponse().getHeaders().getContentType();
|
||||
Locale acceptLocale = exchange.getRequest().getHeaders().getAcceptLanguageAsLocale();
|
||||
Locale locale = (acceptLocale != null ? acceptLocale : Locale.getDefault());
|
||||
Stream<ViewResolver> viewResolverStream = strategies.viewResolvers().get();
|
||||
return Flux.fromStream(viewResolverStream)
|
||||
.concatMap(viewResolver -> viewResolver.resolveViewName(this.name, locale))
|
||||
.next()
|
||||
.otherwiseIfEmpty(Mono.error(new IllegalArgumentException("Could not resolve view with name '" +
|
||||
this.name +"'")))
|
||||
.then(view -> view.render(this.model, contentType, exchange));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.function.server;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.reactive.function.server.support.ServerRequestWrapper;
|
||||
|
||||
/**
|
||||
* Represents a function that filters a {@linkplain HandlerFunction handler function}.
|
||||
*
|
||||
* @param <T> the type of the {@linkplain HandlerFunction handler function} to filter
|
||||
* @param <R> the type of the response of the function
|
||||
* @author Arjen Poutsma
|
||||
* @since 5.0
|
||||
* @see RouterFunction#filter(HandlerFilterFunction)
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface HandlerFilterFunction<T extends ServerResponse, R extends ServerResponse> {
|
||||
|
||||
/**
|
||||
* Apply this filter to the given handler function. The given
|
||||
* {@linkplain HandlerFunction handler function} represents the next entity in the
|
||||
* chain, and can be {@linkplain HandlerFunction#handle(ServerRequest) invoked} in order
|
||||
* to proceed to this entity, or not invoked to block the chain.
|
||||
*
|
||||
* @param request the request
|
||||
* @param next the next handler or filter function in the chain
|
||||
* @return the filtered response
|
||||
* @see ServerRequestWrapper
|
||||
*/
|
||||
Mono<R> filter(ServerRequest request, HandlerFunction<T> next);
|
||||
|
||||
/**
|
||||
* Return a composed filter function that first applies this filter, and then applies the
|
||||
* {@code after} filter.
|
||||
* @param after the filter to apply after this filter is applied
|
||||
* @return a composed filter that first applies this function and then applies the
|
||||
* {@code after} function
|
||||
*/
|
||||
default HandlerFilterFunction<T, R> andThen(HandlerFilterFunction<T, T> after) {
|
||||
Assert.notNull(after, "'after' must not be null");
|
||||
return (request, next) -> {
|
||||
HandlerFunction<T> nextHandler =
|
||||
handlerRequest -> after.filter(handlerRequest, next);
|
||||
return filter(request, nextHandler);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply this filter to the given handler function, resulting in a filtered handler function.
|
||||
* @param handler the handler function to filter
|
||||
* @return the filtered handler function
|
||||
*/
|
||||
default HandlerFunction<R> apply(HandlerFunction<T> handler) {
|
||||
Assert.notNull(handler, "'handler' must not be null");
|
||||
return request -> this.filter(request, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapt the given request processor function to a filter function that only operates on the
|
||||
* {@code ClientRequest}.
|
||||
* @param requestProcessor the request processor
|
||||
* @return the filter adaptation of the request processor
|
||||
*/
|
||||
static HandlerFilterFunction<?, ?> ofRequestProcessor(Function<ServerRequest,
|
||||
Mono<ServerRequest>> requestProcessor) {
|
||||
|
||||
Assert.notNull(requestProcessor, "'requestProcessor' must not be null");
|
||||
return (request, next) -> requestProcessor.apply(request).then(next::handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapt the given response processor function to a filter function that only operates on the
|
||||
* {@code ClientResponse}.
|
||||
* @param responseProcessor the response processor
|
||||
* @return the filter adaptation of the request processor
|
||||
*/
|
||||
static <T extends ServerResponse, R extends ServerResponse> HandlerFilterFunction<T, R> ofResponseProcessor(Function<T,
|
||||
R> responseProcessor) {
|
||||
|
||||
Assert.notNull(responseProcessor, "'responseProcessor' must not be null");
|
||||
return (request, next) -> next.handle(request).map(responseProcessor);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.function.server;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* Represents a function that handles a {@linkplain ServerRequest request}.
|
||||
*
|
||||
* @param <T> the type of the response of the function
|
||||
* @author Arjen Poutsma
|
||||
* @since 5.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface HandlerFunction<T extends ServerResponse> {
|
||||
|
||||
/**
|
||||
* Handle the given request.
|
||||
* @param request the request to handle
|
||||
* @return the response
|
||||
*/
|
||||
Mono<T> handle(ServerRequest request);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.function.server;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.http.codec.HttpMessageReader;
|
||||
import org.springframework.http.codec.HttpMessageWriter;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.reactive.result.view.ViewResolver;
|
||||
|
||||
/**
|
||||
* Defines the strategies to be used for processing {@link HandlerFunction}s. An instance of
|
||||
* this class is immutable; instances are typically created through the mutable {@link Builder}:
|
||||
* either through {@link #builder()} to set up default strategies, or {@link #empty()} to start from
|
||||
* scratch. Alternatively, {@code HandlerStrategies} instances can be created through
|
||||
* {@link #of(Supplier, Supplier, Supplier)}.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @author Juergen Hoeller
|
||||
* @since 5.0
|
||||
* @see RouterFunctions#toHttpHandler(RouterFunction, HandlerStrategies)
|
||||
* @see RouterFunctions#toHandlerMapping(RouterFunction, HandlerStrategies)
|
||||
*/
|
||||
public interface HandlerStrategies {
|
||||
|
||||
// Instance methods
|
||||
|
||||
/**
|
||||
* Supply a {@linkplain Stream stream} of {@link HttpMessageReader}s to be used for request
|
||||
* body conversion.
|
||||
* @return the stream of message readers
|
||||
*/
|
||||
Supplier<Stream<HttpMessageReader<?>>> messageReaders();
|
||||
|
||||
/**
|
||||
* Supply a {@linkplain Stream stream} of {@link HttpMessageWriter}s to be used for response
|
||||
* body conversion.
|
||||
* @return the stream of message writers
|
||||
*/
|
||||
Supplier<Stream<HttpMessageWriter<?>>> messageWriters();
|
||||
|
||||
/**
|
||||
* Supply a {@linkplain Stream stream} of {@link ViewResolver}s to be used for view name
|
||||
* resolution.
|
||||
* @return the stream of view resolvers
|
||||
*/
|
||||
Supplier<Stream<ViewResolver>> viewResolvers();
|
||||
|
||||
|
||||
// Static methods
|
||||
|
||||
/**
|
||||
* Return a new {@code HandlerStrategies} with default initialization.
|
||||
* @return the new {@code HandlerStrategies}
|
||||
*/
|
||||
static HandlerStrategies withDefaults() {
|
||||
return builder().build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@code HandlerStrategies} based on the given
|
||||
* {@linkplain ApplicationContext application context}.
|
||||
* The returned supplier will search for all {@link HttpMessageReader}, {@link HttpMessageWriter},
|
||||
* and {@link ViewResolver} instances in the given application context and return them for
|
||||
* {@link #messageReaders()}, {@link #messageWriters()}, and {@link #viewResolvers()}
|
||||
* respectively.
|
||||
* @param applicationContext the application context to base the strategies on
|
||||
* @return the new {@code HandlerStrategies}
|
||||
*/
|
||||
static HandlerStrategies of(ApplicationContext applicationContext) {
|
||||
return builder(applicationContext).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@code HandlerStrategies} described by the given supplier functions.
|
||||
* All provided supplier function parameters can be {@code null} to indicate an empty
|
||||
* stream is to be returned.
|
||||
* @param messageReaders the supplier function for {@link HttpMessageReader} instances (can be {@code null})
|
||||
* @param messageWriters the supplier function for {@link HttpMessageWriter} instances (can be {@code null})
|
||||
* @param viewResolvers the supplier function for {@link ViewResolver} instances (can be {@code null})
|
||||
* @return the new {@code HandlerStrategies}
|
||||
*/
|
||||
static HandlerStrategies of(Supplier<Stream<HttpMessageReader<?>>> messageReaders,
|
||||
Supplier<Stream<HttpMessageWriter<?>>> messageWriters,
|
||||
Supplier<Stream<ViewResolver>> viewResolvers) {
|
||||
|
||||
return new HandlerStrategies() {
|
||||
@Override
|
||||
public Supplier<Stream<HttpMessageReader<?>>> messageReaders() {
|
||||
return checkForNull(messageReaders);
|
||||
}
|
||||
@Override
|
||||
public Supplier<Stream<HttpMessageWriter<?>>> messageWriters() {
|
||||
return checkForNull(messageWriters);
|
||||
}
|
||||
@Override
|
||||
public Supplier<Stream<ViewResolver>> viewResolvers() {
|
||||
return checkForNull(viewResolvers);
|
||||
}
|
||||
private <T> Supplier<Stream<T>> checkForNull(Supplier<Stream<T>> supplier) {
|
||||
return supplier != null ? supplier : Stream::empty;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// Builder methods
|
||||
|
||||
/**
|
||||
* Return a mutable builder for a {@code HandlerStrategies} with default initialization.
|
||||
* @return the builder
|
||||
*/
|
||||
static Builder builder() {
|
||||
DefaultHandlerStrategiesBuilder builder = new DefaultHandlerStrategiesBuilder();
|
||||
builder.defaultConfiguration();
|
||||
return builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a mutable builder based on the given {@linkplain ApplicationContext application context}.
|
||||
* The returned builder will search for all {@link HttpMessageReader}, {@link HttpMessageWriter},
|
||||
* and {@link ViewResolver} instances in the given application context and return them for
|
||||
* {@link #messageReaders()}, {@link #messageWriters()}, and {@link #viewResolvers()}
|
||||
* respectively.
|
||||
* @param applicationContext the application context to base the strategies on
|
||||
* @return the builder
|
||||
*/
|
||||
static Builder builder(ApplicationContext applicationContext) {
|
||||
Assert.notNull(applicationContext, "ApplicationContext must not be null");
|
||||
DefaultHandlerStrategiesBuilder builder = new DefaultHandlerStrategiesBuilder();
|
||||
builder.applicationContext(applicationContext);
|
||||
return builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a mutable, empty builder for a {@code HandlerStrategies}.
|
||||
* @return the builder
|
||||
*/
|
||||
static Builder empty() {
|
||||
return new DefaultHandlerStrategiesBuilder();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A mutable builder for a {@link HandlerStrategies}.
|
||||
*/
|
||||
interface Builder {
|
||||
|
||||
/**
|
||||
* Add the given message reader to this builder.
|
||||
* @param messageReader the message reader to add
|
||||
* @return this builder
|
||||
*/
|
||||
Builder messageReader(HttpMessageReader<?> messageReader);
|
||||
|
||||
/**
|
||||
* Add the given message writer to this builder.
|
||||
* @param messageWriter the message writer to add
|
||||
* @return this builder
|
||||
*/
|
||||
Builder messageWriter(HttpMessageWriter<?> messageWriter);
|
||||
|
||||
/**
|
||||
* Add the given view resolver to this builder.
|
||||
* @param viewResolver the view resolver to add
|
||||
* @return this builder
|
||||
*/
|
||||
Builder viewResolver(ViewResolver viewResolver);
|
||||
|
||||
/**
|
||||
* Builds the {@link HandlerStrategies}.
|
||||
* @return the built strategies
|
||||
*/
|
||||
HandlerStrategies build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.function.server;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.function.Function;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.UrlResource;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.util.PathMatcher;
|
||||
import org.springframework.util.ResourceUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Lookup function used by {@link RouterFunctions#resources(String, Resource)}.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @since 5.0
|
||||
*/
|
||||
class PathResourceLookupFunction implements Function<ServerRequest, Mono<Resource>> {
|
||||
|
||||
private static final PathMatcher PATH_MATCHER = new AntPathMatcher();
|
||||
|
||||
private final String pattern;
|
||||
|
||||
private final Resource location;
|
||||
|
||||
public PathResourceLookupFunction(String pattern, Resource location) {
|
||||
this.pattern = pattern;
|
||||
this.location = location;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Resource> apply(ServerRequest request) {
|
||||
String path = processPath(request.path());
|
||||
if (path.contains("%")) {
|
||||
path = StringUtils.uriDecode(path, StandardCharsets.UTF_8);
|
||||
}
|
||||
if (!StringUtils.hasLength(path) || isInvalidPath(path)) {
|
||||
return Mono.empty();
|
||||
}
|
||||
if (!PATH_MATCHER.match(this.pattern, path)) {
|
||||
return Mono.empty();
|
||||
}
|
||||
else {
|
||||
path = PATH_MATCHER.extractPathWithinPattern(this.pattern, path);
|
||||
}
|
||||
try {
|
||||
Resource resource = this.location.createRelative(path);
|
||||
if (resource.exists() && resource.isReadable() && isResourceUnderLocation(resource)) {
|
||||
return Mono.just(resource);
|
||||
}
|
||||
else {
|
||||
return Mono.empty();
|
||||
}
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new UncheckedIOException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static String processPath(String path) {
|
||||
boolean slash = false;
|
||||
for (int i = 0; i < path.length(); i++) {
|
||||
if (path.charAt(i) == '/') {
|
||||
slash = true;
|
||||
}
|
||||
else if (path.charAt(i) > ' ' && path.charAt(i) != 127) {
|
||||
if (i == 0 || (i == 1 && slash)) {
|
||||
return path;
|
||||
}
|
||||
path = slash ? "/" + path.substring(i) : path.substring(i);
|
||||
return path;
|
||||
}
|
||||
}
|
||||
return (slash ? "/" : "");
|
||||
}
|
||||
|
||||
private static boolean isInvalidPath(String path) {
|
||||
if (path.contains("WEB-INF") || path.contains("META-INF")) {
|
||||
return true;
|
||||
}
|
||||
if (path.contains(":/")) {
|
||||
String relativePath = (path.charAt(0) == '/' ? path.substring(1) : path);
|
||||
if (ResourceUtils.isUrl(relativePath) || relativePath.startsWith("url:")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (path.contains("")) {
|
||||
path = StringUtils.cleanPath(path);
|
||||
if (path.contains("../")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isResourceUnderLocation(Resource resource) throws IOException {
|
||||
if (resource.getClass() != this.location.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String resourcePath;
|
||||
String locationPath;
|
||||
|
||||
if (resource instanceof UrlResource) {
|
||||
resourcePath = resource.getURL().toExternalForm();
|
||||
locationPath = StringUtils.cleanPath(this.location.getURL().toString());
|
||||
}
|
||||
else if (resource instanceof ClassPathResource) {
|
||||
resourcePath = ((ClassPathResource) resource).getPath();
|
||||
locationPath = StringUtils.cleanPath(((ClassPathResource) this.location).getPath());
|
||||
}
|
||||
else {
|
||||
resourcePath = resource.getURL().getPath();
|
||||
locationPath = StringUtils.cleanPath(this.location.getURL().getPath());
|
||||
}
|
||||
|
||||
if (locationPath.equals(resourcePath)) {
|
||||
return true;
|
||||
}
|
||||
locationPath = (locationPath.endsWith("/") || locationPath.isEmpty() ? locationPath :
|
||||
locationPath + "/");
|
||||
if (!resourcePath.startsWith(locationPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (resourcePath.contains("%")) {
|
||||
if (StringUtils.uriDecode(resourcePath, StandardCharsets.UTF_8).contains("../")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.function.server;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Represents a function that evaluates on a given {@link ServerRequest}.
|
||||
* Instances of this function that evaluate on common request properties can be found in {@link RequestPredicates}.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @since 5.0
|
||||
* @see RequestPredicates
|
||||
* @see RouterFunctions#route(RequestPredicate, HandlerFunction)
|
||||
* @see RouterFunctions#subroute(RequestPredicate, RouterFunction)
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface RequestPredicate {
|
||||
|
||||
/**
|
||||
* Evaluates this predicate on the given request.
|
||||
*
|
||||
*
|
||||
* @param request the request to match against
|
||||
* @return {@code true} if the request matches the predicate; {@code false} otherwise
|
||||
*/
|
||||
boolean test(ServerRequest request);
|
||||
|
||||
/**
|
||||
* Returns a composed request predicate that tests against both this predicate AND the {@code other} predicate.
|
||||
* When evaluating the composed predicate, if this predicate is {@code false}, then the {@code other}
|
||||
* predicate is not evaluated.
|
||||
*
|
||||
* @param other a predicate that will be logically-ANDed with this predicate
|
||||
* @return a predicate composed of this predicate AND the {@code other} predicate
|
||||
*/
|
||||
default RequestPredicate and(RequestPredicate other) {
|
||||
Assert.notNull(other, "'other' must not be null");
|
||||
return new RequestPredicate() {
|
||||
@Override
|
||||
public boolean test(ServerRequest t) {
|
||||
return RequestPredicate.this.test(t) && other.test(t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerRequest subRequest(ServerRequest request) {
|
||||
return other.subRequest(RequestPredicate.this.subRequest(request));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a predicate that represents the logical negation of this predicate.
|
||||
*
|
||||
* @return a predicate that represents the logical negation of this predicate
|
||||
*/
|
||||
default RequestPredicate negate() {
|
||||
return (t) -> !test(t);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a composed request predicate that tests against both this predicate OR the {@code other} predicate.
|
||||
* When evaluating the composed predicate, if this predicate is {@code true}, then the {@code other} predicate
|
||||
* is not evaluated.
|
||||
|
||||
* @param other a predicate that will be logically-ORed with this predicate
|
||||
* @return a predicate composed of this predicate OR the {@code other} predicate
|
||||
*/
|
||||
default RequestPredicate or(RequestPredicate other) {
|
||||
Assert.notNull(other, "'other' must not be null");
|
||||
return (t) -> test(t) || other.test(t);
|
||||
}
|
||||
|
||||
default ServerRequest subRequest(ServerRequest request) {
|
||||
return request;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,372 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.function.server;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.PathMatcher;
|
||||
import org.springframework.web.reactive.function.BodyExtractor;
|
||||
import org.springframework.web.server.WebSession;
|
||||
|
||||
/**
|
||||
* Implementations of {@link RequestPredicate} that implement various useful request matching operations, such as
|
||||
* matching based on path, HTTP method, etc.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @since 5.0
|
||||
*/
|
||||
public abstract class RequestPredicates {
|
||||
|
||||
private static final PathMatcher DEFAULT_PATH_MATCHER = new AntPathMatcher();
|
||||
|
||||
/**
|
||||
* Returns a {@code RequestPredicate} that always matches.
|
||||
*
|
||||
* @return a predicate that always matches
|
||||
*/
|
||||
public static RequestPredicate all() {
|
||||
return request -> true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@code RequestPredicate} that tests against the given HTTP method.
|
||||
*
|
||||
* @param httpMethod the HTTP method to match to
|
||||
* @return a predicate that tests against the given HTTP method
|
||||
*/
|
||||
public static RequestPredicate method(HttpMethod httpMethod) {
|
||||
return new HttpMethodPredicate(httpMethod);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@code RequestPredicate} that tests against the given path pattern.
|
||||
*
|
||||
* @param pattern the pattern to match to
|
||||
* @return a predicate that tests against the given path pattern
|
||||
*/
|
||||
public static RequestPredicate path(String pattern) {
|
||||
return path(pattern, DEFAULT_PATH_MATCHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@code RequestPredicate} that tests against the given path pattern using the given matcher.
|
||||
*
|
||||
* @param pattern the pattern to match to
|
||||
* @param pathMatcher the path matcher to use
|
||||
* @return a predicate that tests against the given path pattern
|
||||
*/
|
||||
public static RequestPredicate path(String pattern, PathMatcher pathMatcher) {
|
||||
return new PathPredicate(pattern, pathMatcher);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@code RequestPredicate} that tests the request's headers against the given headers predicate.
|
||||
*
|
||||
* @param headersPredicate a predicate that tests against the request headers
|
||||
* @return a predicate that tests against the given header predicate
|
||||
*/
|
||||
public static RequestPredicate headers(Predicate<ServerRequest.Headers> headersPredicate) {
|
||||
return new HeaderPredicates(headersPredicate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@code RequestPredicate} that tests if the request's
|
||||
* {@linkplain ServerRequest.Headers#contentType() content type} is {@linkplain MediaType#includes(MediaType) included}
|
||||
* by any of the given media types.
|
||||
*
|
||||
* @param mediaTypes the media types to match the request's content type against
|
||||
* @return a predicate that tests the request's content type against the given media types
|
||||
*/
|
||||
public static RequestPredicate contentType(MediaType... mediaTypes) {
|
||||
Assert.notEmpty(mediaTypes, "'mediaTypes' must not be empty");
|
||||
Set<MediaType> mediaTypeSet = new HashSet<>(Arrays.asList(mediaTypes));
|
||||
return headers(headers -> {
|
||||
MediaType contentType = headers.contentType().orElse(MediaType.APPLICATION_OCTET_STREAM);
|
||||
return mediaTypeSet.stream()
|
||||
.anyMatch(mediaType -> mediaType.includes(contentType));
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@code RequestPredicate} that tests if the request's
|
||||
* {@linkplain ServerRequest.Headers#accept() accept} header is
|
||||
* {@linkplain MediaType#isCompatibleWith(MediaType) compatible} with any of the given media types.
|
||||
*
|
||||
* @param mediaTypes the media types to match the request's accept header against
|
||||
* @return a predicate that tests the request's accept header against the given media types
|
||||
*/
|
||||
public static RequestPredicate accept(MediaType... mediaTypes) {
|
||||
Assert.notEmpty(mediaTypes, "'mediaTypes' must not be empty");
|
||||
Set<MediaType> mediaTypeSet = new HashSet<>(Arrays.asList(mediaTypes));
|
||||
return headers(headers -> {
|
||||
List<MediaType> acceptedMediaTypes = headers.accept();
|
||||
MediaType.sortBySpecificityAndQuality(acceptedMediaTypes);
|
||||
return acceptedMediaTypes.stream()
|
||||
.anyMatch(acceptedMediaType -> mediaTypeSet.stream()
|
||||
.anyMatch(acceptedMediaType::isCompatibleWith));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@code RequestPredicate} that matches if request's HTTP method is {@code GET} and the given
|
||||
* {@code pattern} matches against the request path.
|
||||
*
|
||||
* @param pattern the path pattern to match against
|
||||
* @return a predicate that matches if the request method is GET and if the given pattern matches against the
|
||||
* request path
|
||||
*/
|
||||
public static RequestPredicate GET(String pattern) {
|
||||
return method(HttpMethod.GET).and(path(pattern));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@code RequestPredicate} that matches if request's HTTP method is {@code HEAD} and the given
|
||||
* {@code pattern} matches against the request path.
|
||||
*
|
||||
* @param pattern the path pattern to match against
|
||||
* @return a predicate that matches if the request method is HEAD and if the given pattern matches against the
|
||||
* request path
|
||||
*/
|
||||
public static RequestPredicate HEAD(String pattern) {
|
||||
return method(HttpMethod.HEAD).and(path(pattern));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@code RequestPredicate} that matches if request's HTTP method is {@code POST} and the given
|
||||
* {@code pattern} matches against the request path.
|
||||
*
|
||||
* @param pattern the path pattern to match against
|
||||
* @return a predicate that matches if the request method is POST and if the given pattern matches against the
|
||||
* request path
|
||||
*/
|
||||
public static RequestPredicate POST(String pattern) {
|
||||
return method(HttpMethod.POST).and(path(pattern));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@code RequestPredicate} that matches if request's HTTP method is {@code PUT} and the given
|
||||
* {@code pattern} matches against the request path.
|
||||
*
|
||||
* @param pattern the path pattern to match against
|
||||
* @return a predicate that matches if the request method is PUT and if the given pattern matches against the
|
||||
* request path
|
||||
*/
|
||||
public static RequestPredicate PUT(String pattern) {
|
||||
return method(HttpMethod.PUT).and(path(pattern));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@code RequestPredicate} that matches if request's HTTP method is {@code PATCH} and the given
|
||||
* {@code pattern} matches against the request path.
|
||||
*
|
||||
* @param pattern the path pattern to match against
|
||||
* @return a predicate that matches if the request method is PATCH and if the given pattern matches against the
|
||||
* request path
|
||||
*/
|
||||
public static RequestPredicate PATCH(String pattern) {
|
||||
return method(HttpMethod.PATCH).and(path(pattern));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@code RequestPredicate} that matches if request's HTTP method is {@code DELETE} and the given
|
||||
* {@code pattern} matches against the request path.
|
||||
*
|
||||
* @param pattern the path pattern to match against
|
||||
* @return a predicate that matches if the request method is DELETE and if the given pattern matches against the
|
||||
* request path
|
||||
*/
|
||||
public static RequestPredicate DELETE(String pattern) {
|
||||
return method(HttpMethod.DELETE).and(path(pattern));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@code RequestPredicate} that matches if request's HTTP method is {@code OPTIONS} and the given
|
||||
* {@code pattern} matches against the request path.
|
||||
*
|
||||
* @param pattern the path pattern to match against
|
||||
* @return a predicate that matches if the request method is OPTIONS and if the given pattern matches against the
|
||||
* request path
|
||||
*/
|
||||
public static RequestPredicate OPTIONS(String pattern) {
|
||||
return method(HttpMethod.OPTIONS).and(path(pattern));
|
||||
}
|
||||
|
||||
private static class HttpMethodPredicate implements RequestPredicate {
|
||||
|
||||
private final HttpMethod httpMethod;
|
||||
|
||||
public HttpMethodPredicate(HttpMethod httpMethod) {
|
||||
Assert.notNull(httpMethod, "'httpMethod' must not be null");
|
||||
this.httpMethod = httpMethod;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean test(ServerRequest request) {
|
||||
return this.httpMethod == request.method();
|
||||
}
|
||||
}
|
||||
|
||||
private static class PathPredicate implements RequestPredicate {
|
||||
|
||||
private final String pattern;
|
||||
|
||||
private final PathMatcher pathMatcher;
|
||||
|
||||
public PathPredicate(String pattern, PathMatcher pathMatcher) {
|
||||
Assert.notNull(pattern, "'pattern' must not be null");
|
||||
Assert.notNull(pathMatcher, "'pathMatcher' must not be null");
|
||||
this.pattern = pattern;
|
||||
this.pathMatcher = pathMatcher;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean test(ServerRequest request) {
|
||||
String path = request.path();
|
||||
if (this.pathMatcher.match(this.pattern, path)) {
|
||||
if (request instanceof DefaultServerRequest) {
|
||||
DefaultServerRequest defaultRequest = (DefaultServerRequest) request;
|
||||
Map<String, String> uriTemplateVariables = this.pathMatcher.extractUriTemplateVariables(this.pattern, path);
|
||||
defaultRequest.exchange().getAttributes().put(RouterFunctions.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVariables);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerRequest subRequest(ServerRequest request) {
|
||||
String requestPath = request.path();
|
||||
String subPath = this.pathMatcher.extractPathWithinPattern(this.pattern, requestPath);
|
||||
return new SubPathServerRequestWrapper(request, subPath);
|
||||
}
|
||||
}
|
||||
|
||||
private static class HeaderPredicates implements RequestPredicate {
|
||||
|
||||
private final Predicate<ServerRequest.Headers> headersPredicate;
|
||||
|
||||
public HeaderPredicates(Predicate<ServerRequest.Headers> headersPredicate) {
|
||||
Assert.notNull(headersPredicate, "'headersPredicate' must not be null");
|
||||
this.headersPredicate = headersPredicate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean test(ServerRequest request) {
|
||||
return this.headersPredicate.test(request.headers());
|
||||
}
|
||||
}
|
||||
|
||||
private static class SubPathServerRequestWrapper implements ServerRequest {
|
||||
|
||||
private final ServerRequest request;
|
||||
|
||||
private final String subPath;
|
||||
|
||||
public SubPathServerRequestWrapper(ServerRequest request, String subPath) {
|
||||
this.request = request;
|
||||
this.subPath = subPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpMethod method() {
|
||||
return this.request.method();
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI uri() {
|
||||
return this.request.uri();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String path() {
|
||||
return this.subPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Headers headers() {
|
||||
return this.request.headers();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T body(BodyExtractor<T, ? super ServerHttpRequest> extractor) {
|
||||
return this.request.body(extractor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T body(BodyExtractor<T, ? super ServerHttpRequest> extractor, Map<String, Object> hints) {
|
||||
return this.request.body(extractor, hints);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Mono<T> bodyToMono(Class<? extends T> elementClass) {
|
||||
return this.request.bodyToMono(elementClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Flux<T> bodyToFlux(Class<? extends T> elementClass) {
|
||||
return this.request.bodyToFlux(elementClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Optional<T> attribute(String name) {
|
||||
return this.request.attribute(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> queryParam(String name) {
|
||||
return this.request.queryParam(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> queryParams(String name) {
|
||||
return this.request.queryParams(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String pathVariable(String name) {
|
||||
return this.request.pathVariable(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> pathVariables() {
|
||||
return this.request.pathVariables();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<WebSession> session() {
|
||||
return this.request.session();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.function.server;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Set;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.reactive.function.BodyInserters;
|
||||
|
||||
/**
|
||||
* @author Arjen Poutsma
|
||||
* @since 5.0
|
||||
*/
|
||||
class ResourceHandlerFunction implements HandlerFunction<ServerResponse> {
|
||||
|
||||
|
||||
private static final Set<HttpMethod> SUPPORTED_METHODS =
|
||||
EnumSet.of(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.OPTIONS);
|
||||
|
||||
|
||||
private final Resource resource;
|
||||
|
||||
public ResourceHandlerFunction(Resource resource) {
|
||||
this.resource = resource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ServerResponse> handle(ServerRequest request) {
|
||||
switch (request.method()) {
|
||||
case GET:
|
||||
return ServerResponse.ok()
|
||||
.body(BodyInserters.fromResource(this.resource));
|
||||
case HEAD:
|
||||
Resource headResource = new HeadMethodResource(this.resource);
|
||||
return ServerResponse.ok()
|
||||
.body(BodyInserters.fromResource(headResource));
|
||||
case OPTIONS:
|
||||
return ServerResponse.ok()
|
||||
.allow(SUPPORTED_METHODS)
|
||||
.body(BodyInserters.empty());
|
||||
default:
|
||||
return ServerResponse.status(HttpStatus.METHOD_NOT_ALLOWED)
|
||||
.allow(SUPPORTED_METHODS)
|
||||
.body(BodyInserters.empty());
|
||||
}
|
||||
}
|
||||
|
||||
private static class HeadMethodResource implements Resource {
|
||||
|
||||
private static final byte[] EMPTY = new byte[0];
|
||||
|
||||
private final Resource delegate;
|
||||
|
||||
public HeadMethodResource(Resource delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException {
|
||||
return new ByteArrayInputStream(EMPTY);
|
||||
}
|
||||
|
||||
// delegation
|
||||
|
||||
@Override
|
||||
public boolean exists() {
|
||||
return this.delegate.exists();
|
||||
}
|
||||
|
||||
@Override
|
||||
public URL getURL() throws IOException {
|
||||
return this.delegate.getURL();
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI getURI() throws IOException {
|
||||
return this.delegate.getURI();
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getFile() throws IOException {
|
||||
return this.delegate.getFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long contentLength() throws IOException {
|
||||
return this.delegate.contentLength();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long lastModified() throws IOException {
|
||||
return this.delegate.lastModified();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Resource createRelative(String relativePath) throws IOException {
|
||||
return this.delegate.createRelative(relativePath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return this.delegate.getFilename();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return this.delegate.getDescription();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.function.server;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* Represents a function that routes to a {@linkplain HandlerFunction handler function}.
|
||||
*
|
||||
* @param <T> the type of the {@linkplain HandlerFunction handler function} to route to
|
||||
* @author Arjen Poutsma
|
||||
* @since 5.0
|
||||
* @see RouterFunctions
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface RouterFunction<T extends ServerResponse> {
|
||||
|
||||
/**
|
||||
* Return the {@linkplain HandlerFunction handler function} that matches the given request.
|
||||
* @param request the request to route to
|
||||
* @return an {@code Mono} describing the {@code HandlerFunction} that matches this request,
|
||||
* or an empty {@code Mono} if there is no match
|
||||
*/
|
||||
Mono<HandlerFunction<T>> route(ServerRequest request);
|
||||
|
||||
/**
|
||||
* Return a composed routing function that first invokes this function,
|
||||
* and then invokes the {@code other} function (of the same type {@code T}) if this route had
|
||||
* {@linkplain Mono#empty() no result}.
|
||||
*
|
||||
* @param other the function of type {@code T} to apply when this function has no result
|
||||
* @return a composed function that first routes with this function and then the {@code other} function if this
|
||||
* function has no result
|
||||
*/
|
||||
default RouterFunction<T> andSame(RouterFunction<T> other) {
|
||||
return request -> this.route(request).otherwiseIfEmpty(other.route(request));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a composed routing function that first invokes this function,
|
||||
* and then invokes the {@code other} function (of a different type) if this route had
|
||||
* {@linkplain Optional#empty() no result}.
|
||||
*
|
||||
* @param other the function to apply when this function has no result
|
||||
* @return a composed function that first routes with this function and then the {@code other} function if this
|
||||
* function has no result
|
||||
*/
|
||||
default RouterFunction<?> and(RouterFunction<?> other) {
|
||||
return request -> this.route(request)
|
||||
.map(RouterFunctions::cast)
|
||||
.otherwiseIfEmpty(other.route(request).map(RouterFunctions::cast));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a composed routing function that first invokes this function,
|
||||
* and then routes to the given handler function if the given request predicate applies. This
|
||||
* method is a convenient combination of {@link #and(RouterFunction)} and
|
||||
* {@link RouterFunctions#route(RequestPredicate, HandlerFunction)}.
|
||||
* @param predicate the predicate to test
|
||||
* @param handlerFunction the handler function to route to
|
||||
* @param <S> the handler function type
|
||||
* @return a composed function that first routes with this function and then the function
|
||||
* created from {@code predicate} and {@code handlerFunction} if this
|
||||
* function has no result
|
||||
*/
|
||||
default <S extends ServerResponse> RouterFunction<?> andRoute(RequestPredicate predicate,
|
||||
HandlerFunction<S> handlerFunction) {
|
||||
return and(RouterFunctions.route(predicate, handlerFunction));
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter all {@linkplain HandlerFunction handler functions} routed by this function with the given
|
||||
* {@linkplain HandlerFilterFunction filter function}.
|
||||
*
|
||||
* @param filterFunction the filter to apply
|
||||
* @param <S> the filter return type
|
||||
* @return the filtered routing function
|
||||
*/
|
||||
default <S extends ServerResponse> RouterFunction<S> filter(HandlerFilterFunction<T, S> filterFunction) {
|
||||
return request -> this.route(request).map(filterFunction::apply);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.function.server;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.server.reactive.HttpHandler;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.reactive.HandlerMapping;
|
||||
import org.springframework.web.reactive.function.server.support.HandlerFunctionAdapter;
|
||||
import org.springframework.web.reactive.function.server.support.ServerResponseResultHandler;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebHandler;
|
||||
import org.springframework.web.server.adapter.HttpWebHandlerAdapter;
|
||||
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
|
||||
|
||||
/**
|
||||
* <strong>Central entry point to Spring's functional web framework.</strong>
|
||||
* Exposes routing functionality, such as to
|
||||
* {@linkplain #route(RequestPredicate, HandlerFunction) create} a {@code RouterFunction} given a
|
||||
* {@code RequestPredicate} and {@code HandlerFunction}, and to do further
|
||||
* {@linkplain #subroute(RequestPredicate, RouterFunction) subrouting} on an existing routing
|
||||
* function.
|
||||
*
|
||||
* <p>Additionally, this class can {@linkplain #toHttpHandler(RouterFunction) transform} a
|
||||
* {@code RouterFunction} into an {@code HttpHandler}, which can be run in Servlet 3.1+,
|
||||
* Reactor, RxNetty, or Undertow.
|
||||
* And it can {@linkplain #toHandlerMapping(RouterFunction, HandlerStrategies) transform} a
|
||||
* {@code RouterFunction} into an {@code HandlerMapping}, which can be run in a
|
||||
* {@code DispatcherHandler}.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @since 5.0
|
||||
*/
|
||||
public abstract class RouterFunctions {
|
||||
|
||||
/**
|
||||
* Name of the {@link ServerWebExchange} attribute that contains the {@link ServerRequest}.
|
||||
*/
|
||||
public static final String REQUEST_ATTRIBUTE = RouterFunctions.class.getName() + ".request";
|
||||
|
||||
/**
|
||||
* Name of the {@link ServerWebExchange} attribute that contains the URI
|
||||
* templates map, mapping variable names to values.
|
||||
*/
|
||||
public static final String URI_TEMPLATE_VARIABLES_ATTRIBUTE =
|
||||
RouterFunctions.class.getName() + ".uriTemplateVariables";
|
||||
|
||||
private static final HandlerFunction<ServerResponse> NOT_FOUND_HANDLER = request -> ServerResponse.notFound().build();
|
||||
|
||||
|
||||
/**
|
||||
* Route to the given handler function if the given request predicate applies.
|
||||
* @param predicate the predicate to test
|
||||
* @param handlerFunction the handler function to route to
|
||||
* @param <T> the type of the handler function
|
||||
* @return a router function that routes to {@code handlerFunction} if
|
||||
* {@code predicate} evaluates to {@code true}
|
||||
* @see RequestPredicates
|
||||
*/
|
||||
public static <T extends ServerResponse> RouterFunction<T> route(RequestPredicate predicate,
|
||||
HandlerFunction<T> handlerFunction) {
|
||||
|
||||
Assert.notNull(predicate, "'predicate' must not be null");
|
||||
Assert.notNull(handlerFunction, "'handlerFunction' must not be null");
|
||||
|
||||
return request -> predicate.test(request) ? Mono.just(handlerFunction) : Mono.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Route to the given router function if the given request predicate applies.
|
||||
* @param predicate the predicate to test
|
||||
* @param routerFunction the router function to route to
|
||||
* @param <T> the type of the handler function
|
||||
* @return a router function that routes to {@code routerFunction} if
|
||||
* {@code predicate} evaluates to {@code true}
|
||||
* @see RequestPredicates
|
||||
*/
|
||||
public static <T extends ServerResponse> RouterFunction<T> subroute(RequestPredicate predicate,
|
||||
RouterFunction<T> routerFunction) {
|
||||
|
||||
Assert.notNull(predicate, "'predicate' must not be null");
|
||||
Assert.notNull(routerFunction, "'routerFunction' must not be null");
|
||||
|
||||
return request -> {
|
||||
if (predicate.test(request)) {
|
||||
ServerRequest subRequest = predicate.subRequest(request);
|
||||
return routerFunction.route(subRequest);
|
||||
}
|
||||
else {
|
||||
return Mono.empty();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Route requests that match the given pattern to resources relative to the given root location.
|
||||
* For instance
|
||||
* <pre class="code">
|
||||
* Resource location = new FileSystemResource("public-resources/");
|
||||
* RoutingFunction<Resource> resources = RouterFunctions.resources("/resources/**", location);
|
||||
* </pre>
|
||||
* @param pattern the pattern to match
|
||||
* @param location the location directory relative to which resources should be resolved
|
||||
* @return a router function that routes to resources
|
||||
*/
|
||||
public static RouterFunction<ServerResponse> resources(String pattern, Resource location) {
|
||||
Assert.hasLength(pattern, "'pattern' must not be empty");
|
||||
Assert.notNull(location, "'location' must not be null");
|
||||
|
||||
return resources(new PathResourceLookupFunction(pattern, location));
|
||||
}
|
||||
|
||||
/**
|
||||
* Route to resources using the provided lookup function. If the lookup function provides a
|
||||
* {@link Resource} for the given request, it will be it will be exposed using a
|
||||
* {@link HandlerFunction} that handles GET, HEAD, and OPTIONS requests.
|
||||
* @param lookupFunction the function to provide a {@link Resource} given the {@link ServerRequest}
|
||||
* @return a router function that routes to resources
|
||||
*/
|
||||
public static RouterFunction<ServerResponse> resources(Function<ServerRequest, Mono<Resource>> lookupFunction) {
|
||||
Assert.notNull(lookupFunction, "'lookupFunction' must not be null");
|
||||
|
||||
return request -> lookupFunction.apply(request).map(ResourceHandlerFunction::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the given {@linkplain RouterFunction router function} into a {@link HttpHandler}.
|
||||
* This conversion uses {@linkplain HandlerStrategies#builder() default strategies}.
|
||||
* <p>The returned handler can be adapted to run in
|
||||
* <ul>
|
||||
* <li>Servlet 3.1+ using the
|
||||
* {@link org.springframework.http.server.reactive.ServletHttpHandlerAdapter},</li>
|
||||
* <li>Reactor using the
|
||||
* {@link org.springframework.http.server.reactive.ReactorHttpHandlerAdapter},</li>
|
||||
* <li>RxNetty using the
|
||||
* {@link org.springframework.http.server.reactive.RxNettyHttpHandlerAdapter}, or </li>
|
||||
* <li>Undertow using the
|
||||
* {@link org.springframework.http.server.reactive.UndertowHttpHandlerAdapter}.</li>
|
||||
* </ul>
|
||||
* <p>Note that {@code HttpWebHandlerAdapter} also implements {@link WebHandler}, allowing
|
||||
* for additional filter and exception handler registration through
|
||||
* {@link WebHttpHandlerBuilder}.
|
||||
* @param routerFunction the router function to convert
|
||||
* @return an http handler that handles HTTP request using the given router function
|
||||
*/
|
||||
public static HttpWebHandlerAdapter toHttpHandler(RouterFunction<?> routerFunction) {
|
||||
return toHttpHandler(routerFunction, HandlerStrategies.withDefaults());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the given {@linkplain RouterFunction router function} into a {@link HttpHandler},
|
||||
* using the given strategies.
|
||||
* <p>The returned {@code HttpHandler} can be adapted to run in
|
||||
* <ul>
|
||||
* <li>Servlet 3.1+ using the
|
||||
* {@link org.springframework.http.server.reactive.ServletHttpHandlerAdapter},</li>
|
||||
* <li>Reactor using the
|
||||
* {@link org.springframework.http.server.reactive.ReactorHttpHandlerAdapter},</li>
|
||||
* <li>RxNetty using the
|
||||
* {@link org.springframework.http.server.reactive.RxNettyHttpHandlerAdapter}, or </li>
|
||||
* <li>Undertow using the
|
||||
* {@link org.springframework.http.server.reactive.UndertowHttpHandlerAdapter}.</li>
|
||||
* </ul>
|
||||
* <p>Note that {@code HttpWebHandlerAdapter} also implements {@link WebHandler}, allowing
|
||||
* for additional filter and exception handler registration through
|
||||
* @param routerFunction the router function to convert
|
||||
* @param strategies the strategies to use
|
||||
* @return an http handler that handles HTTP request using the given router function
|
||||
*/
|
||||
public static HttpWebHandlerAdapter toHttpHandler(RouterFunction<?> routerFunction, HandlerStrategies strategies) {
|
||||
Assert.notNull(routerFunction, "RouterFunction must not be null");
|
||||
Assert.notNull(strategies, "HandlerStrategies must not be null");
|
||||
|
||||
return new HttpWebHandlerAdapter(exchange -> {
|
||||
ServerRequest request = new DefaultServerRequest(exchange, strategies);
|
||||
addAttributes(exchange, request);
|
||||
return routerFunction.route(request)
|
||||
.defaultIfEmpty(notFound())
|
||||
.then(handlerFunction -> handlerFunction.handle(request))
|
||||
.then(response -> response.writeTo(exchange, strategies));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the given {@code RouterFunction} into a {@code HandlerMapping}.
|
||||
* This conversion uses {@linkplain HandlerStrategies#builder() default strategies}.
|
||||
* <p>The returned {@code HandlerMapping} can be run in a
|
||||
* {@link org.springframework.web.reactive.DispatcherHandler}.
|
||||
* @param routerFunction the router function to convert
|
||||
* @return an handler mapping that maps HTTP request to a handler using the given router function
|
||||
* @see HandlerFunctionAdapter
|
||||
* @see ServerResponseResultHandler
|
||||
*/
|
||||
public static HandlerMapping toHandlerMapping(RouterFunction<?> routerFunction) {
|
||||
return toHandlerMapping(routerFunction, HandlerStrategies.withDefaults());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the given {@linkplain RouterFunction router function} into a {@link HandlerMapping},
|
||||
* using the given strategies.
|
||||
* <p>The returned {@code HandlerMapping} can be run in a
|
||||
* {@link org.springframework.web.reactive.DispatcherHandler}.
|
||||
* @param routerFunction the router function to convert
|
||||
* @param strategies the strategies to use
|
||||
* @return an handler mapping that maps HTTP request to a handler using the given router function
|
||||
* @see HandlerFunctionAdapter
|
||||
* @see ServerResponseResultHandler
|
||||
*/
|
||||
public static HandlerMapping toHandlerMapping(RouterFunction<?> routerFunction, HandlerStrategies strategies) {
|
||||
Assert.notNull(routerFunction, "RouterFunction must not be null");
|
||||
Assert.notNull(strategies, "HandlerStrategies must not be null");
|
||||
|
||||
return new HandlerMapping() {
|
||||
@Override
|
||||
public Mono<Object> getHandler(ServerWebExchange exchange) {
|
||||
ServerRequest request = new DefaultServerRequest(exchange, strategies);
|
||||
addAttributes(exchange, request);
|
||||
return routerFunction.route(request).map(handlerFunction -> (Object)handlerFunction);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private static void addAttributes(ServerWebExchange exchange, ServerRequest request) {
|
||||
Map<String, Object> attributes = exchange.getAttributes();
|
||||
attributes.put(REQUEST_ATTRIBUTE, request);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T extends ServerResponse> HandlerFunction<T> notFound() {
|
||||
return (HandlerFunction<T>) NOT_FOUND_HANDLER;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static <T extends ServerResponse> HandlerFunction<T> cast(HandlerFunction<?> handlerFunction) {
|
||||
return (HandlerFunction<T>) handlerFunction;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.function.server;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalLong;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpRange;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.json.AbstractJackson2Codec;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.web.reactive.function.BodyExtractor;
|
||||
import org.springframework.web.server.WebSession;
|
||||
|
||||
/**
|
||||
* Represents a server-side HTTP request, as handled by a {@code HandlerFunction}.
|
||||
* Access to headers and body is offered by {@link Headers} and
|
||||
* {@link #body(BodyExtractor)} respectively.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @author Sebastien Deleuze
|
||||
* @since 5.0
|
||||
*/
|
||||
public interface ServerRequest {
|
||||
|
||||
/**
|
||||
* Return the HTTP method.
|
||||
*/
|
||||
HttpMethod method();
|
||||
|
||||
/**
|
||||
* Return the request URI.
|
||||
*/
|
||||
URI uri();
|
||||
|
||||
/**
|
||||
* Return the request path.
|
||||
*/
|
||||
default String path() {
|
||||
return uri().getPath();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the headers of this request.
|
||||
*/
|
||||
Headers headers();
|
||||
|
||||
/**
|
||||
* Extract the body with the given {@code BodyExtractor}.
|
||||
* @param extractor the {@code BodyExtractor} that reads from the request
|
||||
* @param <T> the type of the body returned
|
||||
* @return the extracted body
|
||||
* @see #body(BodyExtractor, Map)
|
||||
*/
|
||||
<T> T body(BodyExtractor<T, ? super ServerHttpRequest> extractor);
|
||||
|
||||
/**
|
||||
* Extract the body with the given {@code BodyExtractor} and hints.
|
||||
* @param extractor the {@code BodyExtractor} that reads from the request
|
||||
* @param hints the map of hints like {@link AbstractJackson2Codec#JSON_VIEW_HINT}
|
||||
* to use to customize body extraction
|
||||
* @param <T> the type of the body returned
|
||||
* @return the extracted body
|
||||
*/
|
||||
<T> T body(BodyExtractor<T, ? super ServerHttpRequest> extractor, Map<String, Object> hints);
|
||||
|
||||
/**
|
||||
* Extract the body to a {@code Mono}.
|
||||
* @param elementClass the class of element in the {@code Mono}
|
||||
* @param <T> the element type
|
||||
* @return the body as a mono
|
||||
*/
|
||||
<T> Mono<T> bodyToMono(Class<? extends T> elementClass);
|
||||
|
||||
/**
|
||||
* Extract the body to a {@code Flux}.
|
||||
* @param elementClass the class of element in the {@code Flux}
|
||||
* @param <T> the element type
|
||||
* @return the body as a flux
|
||||
*/
|
||||
<T> Flux<T> bodyToFlux(Class<? extends T> elementClass);
|
||||
|
||||
/**
|
||||
* Return the request attribute value if present.
|
||||
* @param name the attribute name
|
||||
* @param <T> the attribute type
|
||||
* @return the attribute value
|
||||
*/
|
||||
<T> Optional<T> attribute(String name);
|
||||
|
||||
/**
|
||||
* Return the first query parameter with the given name, if present.
|
||||
* @param name the parameter name
|
||||
* @return the parameter value
|
||||
*/
|
||||
default Optional<String> queryParam(String name) {
|
||||
List<String> queryParams = this.queryParams(name);
|
||||
return !queryParams.isEmpty() ? Optional.of(queryParams.get(0)) : Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all query parameter with the given name. Returns an empty list if no values could
|
||||
* be found.
|
||||
* @param name the parameter name
|
||||
* @return the parameter values
|
||||
*/
|
||||
List<String> queryParams(String name);
|
||||
|
||||
/**
|
||||
* Return the path variable with the given name, if present.
|
||||
* @param name the variable name
|
||||
* @return the variable value
|
||||
* @throws IllegalArgumentException if there is no path variable with the given name
|
||||
*/
|
||||
default String pathVariable(String name) {
|
||||
Map<String, String> pathVariables = pathVariables();
|
||||
if (pathVariables.containsKey(name)) {
|
||||
return pathVariables().get(name);
|
||||
}
|
||||
else {
|
||||
throw new IllegalArgumentException(
|
||||
"No path variable with name \"" + name + "\" available");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all path variables.
|
||||
* @return the path variables
|
||||
*/
|
||||
Map<String, String> pathVariables();
|
||||
|
||||
/**
|
||||
* Return the web session for the current request. Always guaranteed to
|
||||
* return an instance either matching to the session id requested by the
|
||||
* client, or with a new session id either because the client did not
|
||||
* specify one or because the underlying session had expired. Use of this
|
||||
* method does not automatically create a session.
|
||||
*/
|
||||
Mono<WebSession> session();
|
||||
|
||||
|
||||
/**
|
||||
* Represents the headers of the HTTP request.
|
||||
* @see ServerRequest#headers()
|
||||
*/
|
||||
interface Headers {
|
||||
|
||||
/**
|
||||
* Return the list of acceptable {@linkplain MediaType media types},
|
||||
* as specified by the {@code Accept} header.
|
||||
* <p>Returns an empty list when the acceptable media types are unspecified.
|
||||
*/
|
||||
List<MediaType> accept();
|
||||
|
||||
/**
|
||||
* Return the list of acceptable {@linkplain Charset charsets},
|
||||
* as specified by the {@code Accept-Charset} header.
|
||||
*/
|
||||
List<Charset> acceptCharset();
|
||||
|
||||
/**
|
||||
* Return the length of the body in bytes, as specified by the
|
||||
* {@code Content-Length} header.
|
||||
*/
|
||||
OptionalLong contentLength();
|
||||
|
||||
/**
|
||||
* Return the {@linkplain MediaType media type} of the body, as specified
|
||||
* by the {@code Content-Type} header.
|
||||
*/
|
||||
Optional<MediaType> contentType();
|
||||
|
||||
/**
|
||||
* Return the value of the required {@code Host} header.
|
||||
* <p>If the header value does not contain a port, the returned
|
||||
* {@linkplain InetSocketAddress#getPort() port} will be {@code 0}.
|
||||
*/
|
||||
InetSocketAddress host();
|
||||
|
||||
/**
|
||||
* Return the value of the {@code Range} header.
|
||||
* <p>Returns an empty list when the range is unknown.
|
||||
*/
|
||||
List<HttpRange> range();
|
||||
|
||||
/**
|
||||
* Return the header value(s), if any, for the header of the given name.
|
||||
* <p>Return an empty list if no header values are found.
|
||||
*
|
||||
* @param headerName the header name
|
||||
*/
|
||||
List<String> header(String headerName);
|
||||
|
||||
/**
|
||||
* Return the headers as a {@link HttpHeaders} instance.
|
||||
*/
|
||||
HttpHeaders asHttpHeaders();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.function.server;
|
||||
|
||||
import java.net.URI;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.http.CacheControl;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.json.AbstractJackson2Codec;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.reactive.function.BodyInserter;
|
||||
import org.springframework.web.reactive.function.BodyInserters;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* Represents a typed, server-side HTTP response, as returned by a
|
||||
* {@linkplain HandlerFunction handler function} or {@linkplain HandlerFilterFunction filter function}.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @author Sebastien Deleuze
|
||||
* @since 5.0
|
||||
*/
|
||||
public interface ServerResponse {
|
||||
|
||||
// Instance methods
|
||||
|
||||
/**
|
||||
* Return the status code of this response.
|
||||
*/
|
||||
HttpStatus statusCode();
|
||||
|
||||
/**
|
||||
* Return the headers of this response.
|
||||
*/
|
||||
HttpHeaders headers();
|
||||
|
||||
/**
|
||||
* Writes this response to the given web exchange.
|
||||
*
|
||||
* @param exchange the web exchange to write to
|
||||
* @param strategies the strategies to use when writing
|
||||
* @return {@code Mono<Void>} to indicate when writing is complete
|
||||
*/
|
||||
Mono<Void> writeTo(ServerWebExchange exchange, HandlerStrategies strategies);
|
||||
|
||||
// Static builder methods
|
||||
|
||||
/**
|
||||
* Create a builder with the status code and headers of the given response.
|
||||
*
|
||||
* @param other the response to copy the status and headers from
|
||||
* @return the created builder
|
||||
*/
|
||||
static BodyBuilder from(ServerResponse other) {
|
||||
Assert.notNull(other, "'other' must not be null");
|
||||
DefaultServerResponseBuilder builder = new DefaultServerResponseBuilder(other.statusCode());
|
||||
return builder.headers(other.headers());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a builder with the given status.
|
||||
*
|
||||
* @param status the response status
|
||||
* @return the created builder
|
||||
*/
|
||||
static BodyBuilder status(HttpStatus status) {
|
||||
Assert.notNull(status, "HttpStatus must not be null");
|
||||
return new DefaultServerResponseBuilder(status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a builder with the status set to {@linkplain HttpStatus#OK OK}.
|
||||
*
|
||||
* @return the created builder
|
||||
*/
|
||||
static BodyBuilder ok() {
|
||||
return status(HttpStatus.OK);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new builder with a {@linkplain HttpStatus#CREATED CREATED} status
|
||||
* and a location header set to the given URI.
|
||||
*
|
||||
* @param location the location URI
|
||||
* @return the created builder
|
||||
*/
|
||||
static BodyBuilder created(URI location) {
|
||||
BodyBuilder builder = status(HttpStatus.CREATED);
|
||||
return builder.location(location);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a builder with an {@linkplain HttpStatus#ACCEPTED ACCEPTED} status.
|
||||
*
|
||||
* @return the created builder
|
||||
*/
|
||||
static BodyBuilder accepted() {
|
||||
return status(HttpStatus.ACCEPTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a builder with a {@linkplain HttpStatus#NO_CONTENT NO_CONTENT} status.
|
||||
*
|
||||
* @return the created builder
|
||||
*/
|
||||
static HeadersBuilder<?> noContent() {
|
||||
return status(HttpStatus.NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a builder with a {@linkplain HttpStatus#BAD_REQUEST BAD_REQUEST} status.
|
||||
*
|
||||
* @return the created builder
|
||||
*/
|
||||
static BodyBuilder badRequest() {
|
||||
return status(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a builder with a {@linkplain HttpStatus#NOT_FOUND NOT_FOUND} status.
|
||||
*
|
||||
* @return the created builder
|
||||
*/
|
||||
static HeadersBuilder<?> notFound() {
|
||||
return status(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a builder with an
|
||||
* {@linkplain HttpStatus#UNPROCESSABLE_ENTITY UNPROCESSABLE_ENTITY} status.
|
||||
*
|
||||
* @return the created builder
|
||||
*/
|
||||
static BodyBuilder unprocessableEntity() {
|
||||
return status(HttpStatus.UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Defines a builder that adds headers to the response.
|
||||
*
|
||||
* @param <B> the builder subclass
|
||||
*/
|
||||
interface HeadersBuilder<B extends HeadersBuilder<B>> {
|
||||
|
||||
/**
|
||||
* Add the given header value(s) under the given name.
|
||||
*
|
||||
* @param headerName the header name
|
||||
* @param headerValues the header value(s)
|
||||
* @return this builder
|
||||
* @see HttpHeaders#add(String, String)
|
||||
*/
|
||||
B header(String headerName, String... headerValues);
|
||||
|
||||
/**
|
||||
* Copy the given headers into the entity's headers map.
|
||||
*
|
||||
* @param headers the existing HttpHeaders to copy from
|
||||
* @return this builder
|
||||
* @see HttpHeaders#add(String, String)
|
||||
*/
|
||||
B headers(HttpHeaders headers);
|
||||
|
||||
/**
|
||||
* Set the set of allowed {@link HttpMethod HTTP methods}, as specified
|
||||
* by the {@code Allow} header.
|
||||
*
|
||||
* @param allowedMethods the allowed methods
|
||||
* @return this builder
|
||||
* @see HttpHeaders#setAllow(Set)
|
||||
*/
|
||||
B allow(HttpMethod... allowedMethods);
|
||||
|
||||
/**
|
||||
* Set the set of allowed {@link HttpMethod HTTP methods}, as specified
|
||||
* by the {@code Allow} header.
|
||||
*
|
||||
* @param allowedMethods the allowed methods
|
||||
* @return this builder
|
||||
* @see HttpHeaders#setAllow(Set)
|
||||
*/
|
||||
B allow(Set<HttpMethod> allowedMethods);
|
||||
|
||||
/**
|
||||
* Set the entity tag of the body, as specified by the {@code ETag} header.
|
||||
*
|
||||
* @param eTag the new entity tag
|
||||
* @return this builder
|
||||
* @see HttpHeaders#setETag(String)
|
||||
*/
|
||||
B eTag(String eTag);
|
||||
|
||||
/**
|
||||
* Set the time the resource was last changed, as specified by the
|
||||
* {@code Last-Modified} header.
|
||||
* <p>The date should be specified as the number of milliseconds since
|
||||
* January 1, 1970 GMT.
|
||||
*
|
||||
* @param lastModified the last modified date
|
||||
* @return this builder
|
||||
* @see HttpHeaders#setLastModified(long)
|
||||
*/
|
||||
B lastModified(ZonedDateTime lastModified);
|
||||
|
||||
/**
|
||||
* Set the location of a resource, as specified by the {@code Location} header.
|
||||
*
|
||||
* @param location the location
|
||||
* @return this builder
|
||||
* @see HttpHeaders#setLocation(URI)
|
||||
*/
|
||||
B location(URI location);
|
||||
|
||||
/**
|
||||
* Set the caching directives for the resource, as specified by the HTTP 1.1
|
||||
* {@code Cache-Control} header.
|
||||
* <p>A {@code CacheControl} instance can be built like
|
||||
* {@code CacheControl.maxAge(3600).cachePublic().noTransform()}.
|
||||
*
|
||||
* @param cacheControl a builder for cache-related HTTP response headers
|
||||
* @return this builder
|
||||
* @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2">RFC-7234 Section 5.2</a>
|
||||
*/
|
||||
B cacheControl(CacheControl cacheControl);
|
||||
|
||||
/**
|
||||
* Configure one or more request header names (e.g. "Accept-Language") to
|
||||
* add to the "Vary" response header to inform clients that the response is
|
||||
* subject to content negotiation and variances based on the value of the
|
||||
* given request headers. The configured request header names are added only
|
||||
* if not already present in the response "Vary" header.
|
||||
*
|
||||
* @param requestHeaders request header names
|
||||
* @return this builder
|
||||
*/
|
||||
B varyBy(String... requestHeaders);
|
||||
|
||||
/**
|
||||
* Build the response entity with no body.
|
||||
*
|
||||
* @return the built response
|
||||
*/
|
||||
Mono<ServerResponse> build();
|
||||
|
||||
/**
|
||||
* Build the response entity with no body.
|
||||
* The response will be committed when the given {@code voidPublisher} completes.
|
||||
*
|
||||
* @param voidPublisher publisher publisher to indicate when the response should be committed
|
||||
* @return the built response
|
||||
*/
|
||||
Mono<ServerResponse> build(Publisher<Void> voidPublisher);
|
||||
|
||||
/**
|
||||
* Build the response entity with a custom writer function.
|
||||
*
|
||||
* @param writeFunction the function used to write to the {@link ServerWebExchange}
|
||||
* @return the built response
|
||||
*/
|
||||
Mono<ServerResponse> build(BiFunction<ServerWebExchange, HandlerStrategies,
|
||||
Mono<Void>> writeFunction);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Defines a builder that adds a body to the response.
|
||||
*/
|
||||
interface BodyBuilder extends HeadersBuilder<BodyBuilder> {
|
||||
|
||||
/**
|
||||
* Set the length of the body in bytes, as specified by the
|
||||
* {@code Content-Length} header.
|
||||
*
|
||||
* @param contentLength the content length
|
||||
* @return this builder
|
||||
* @see HttpHeaders#setContentLength(long)
|
||||
*/
|
||||
BodyBuilder contentLength(long contentLength);
|
||||
|
||||
/**
|
||||
* Set the {@linkplain MediaType media type} of the body, as specified by the
|
||||
* {@code Content-Type} header.
|
||||
*
|
||||
* @param contentType the content type
|
||||
* @return this builder
|
||||
* @see HttpHeaders#setContentType(MediaType)
|
||||
*/
|
||||
BodyBuilder contentType(MediaType contentType);
|
||||
|
||||
/**
|
||||
* Add a serialization hint like {@link AbstractJackson2Codec#JSON_VIEW_HINT} to
|
||||
* customize how the body will be serialized.
|
||||
*/
|
||||
BodyBuilder hint(String key, Object value);
|
||||
|
||||
/**
|
||||
* Set the body of the response to the given {@code Publisher} and return it. This
|
||||
* convenience method combines {@link #body(BodyInserter)} and
|
||||
* {@link BodyInserters#fromPublisher(Publisher, Class)}.
|
||||
* @param publisher the {@code Publisher} to write to the response
|
||||
* @param elementClass the class of elements contained in the publisher
|
||||
* @param <T> the type of the elements contained in the publisher
|
||||
* @param <P> the type of the {@code Publisher}
|
||||
* @return the built request
|
||||
*/
|
||||
<T, P extends Publisher<T>> Mono<ServerResponse> body(P publisher, Class<T> elementClass);
|
||||
|
||||
/**
|
||||
* Set the body of the response to the given {@code BodyInserter} and return it.
|
||||
* @param inserter the {@code BodyInserter} that writes to the response
|
||||
* @param <T> the type contained in the body
|
||||
* @return the built response
|
||||
*/
|
||||
<T> Mono<ServerResponse> body(BodyInserter<T, ? super ServerHttpResponse> inserter);
|
||||
|
||||
/**
|
||||
* Render the template with the given {@code name} using the given {@code modelAttributes}.
|
||||
* The model attributes are mapped under a
|
||||
* {@linkplain org.springframework.core.Conventions#getVariableName generated name}.
|
||||
* <p><emphasis>Note: Empty {@link Collection Collections} are not added to
|
||||
* the model when using this method because we cannot correctly determine
|
||||
* the true convention name.</emphasis>
|
||||
* @param name the name of the template to be rendered
|
||||
* @param modelAttributes the modelAttributes used to render the template
|
||||
* @return the built response
|
||||
*/
|
||||
Mono<ServerResponse> render(String name, Object... modelAttributes);
|
||||
|
||||
/**
|
||||
* Render the template with the given {@code name} using the given {@code model}.
|
||||
* @param name the name of the template to be rendered
|
||||
* @param model the model used to render the template
|
||||
* @return the built response
|
||||
*/
|
||||
Mono<ServerResponse> render(String name, Map<String, ?> model);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Provides the types that make up Spring's functional web framework.
|
||||
*/
|
||||
package org.springframework.web.reactive.function.server;
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.function.server.support;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.web.reactive.HandlerAdapter;
|
||||
import org.springframework.web.reactive.HandlerResult;
|
||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* {@code HandlerAdapter} implementation that supports {@link HandlerFunction}s.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @since 5.0
|
||||
*/
|
||||
public class HandlerFunctionAdapter implements HandlerAdapter {
|
||||
|
||||
private static final MethodParameter HANDLER_FUNCTION_RETURN_TYPE;
|
||||
|
||||
static {
|
||||
try {
|
||||
Method method = HandlerFunction.class.getMethod("handle", ServerRequest.class);
|
||||
HANDLER_FUNCTION_RETURN_TYPE = new MethodParameter(method, -1);
|
||||
}
|
||||
catch (NoSuchMethodException ex) {
|
||||
throw new Error(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Object handler) {
|
||||
return handler instanceof HandlerFunction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<HandlerResult> handle(ServerWebExchange exchange, Object handler) {
|
||||
HandlerFunction<?> handlerFunction = (HandlerFunction<?>) handler;
|
||||
ServerRequest request =
|
||||
exchange.<ServerRequest>getAttribute(RouterFunctions.REQUEST_ATTRIBUTE)
|
||||
.orElseThrow(() -> new IllegalStateException(
|
||||
"Could not find ServerRequest in exchange attributes"));
|
||||
|
||||
return handlerFunction.handle(request)
|
||||
.map(response -> new HandlerResult(handlerFunction, response, HANDLER_FUNCTION_RETURN_TYPE));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.function.server.support;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalLong;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpRange;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.reactive.function.BodyExtractor;
|
||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.server.WebSession;
|
||||
|
||||
/**
|
||||
* Implementation of the {@link ServerRequest} interface that can be subclassed
|
||||
* to adapt the request to a {@link HandlerFunction handler function}.
|
||||
* All methods default to calling through to the wrapped request.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @since 5.0
|
||||
*/
|
||||
public class ServerRequestWrapper implements ServerRequest {
|
||||
|
||||
private final ServerRequest delegate;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new {@code RequestWrapper} that wraps the given request.
|
||||
* @param delegate the request to wrap
|
||||
*/
|
||||
public ServerRequestWrapper(ServerRequest delegate) {
|
||||
Assert.notNull(delegate, "Delegate must not be null");
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the wrapped request.
|
||||
*/
|
||||
public ServerRequest request() {
|
||||
return this.delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpMethod method() {
|
||||
return this.delegate.method();
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI uri() {
|
||||
return this.delegate.uri();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String path() {
|
||||
return this.delegate.path();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Headers headers() {
|
||||
return this.delegate.headers();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T body(BodyExtractor<T, ? super ServerHttpRequest> extractor) {
|
||||
return this.delegate.body(extractor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T body(BodyExtractor<T, ? super ServerHttpRequest> extractor, Map<String, Object> hints) {
|
||||
return this.delegate.body(extractor, hints);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Mono<T> bodyToMono(Class<? extends T> elementClass) {
|
||||
return this.delegate.bodyToMono(elementClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Flux<T> bodyToFlux(Class<? extends T> elementClass) {
|
||||
return this.delegate.bodyToFlux(elementClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Optional<T> attribute(String name) {
|
||||
return this.delegate.attribute(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> queryParam(String name) {
|
||||
return this.delegate.queryParam(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> queryParams(String name) {
|
||||
return this.delegate.queryParams(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String pathVariable(String name) {
|
||||
return this.delegate.pathVariable(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> pathVariables() {
|
||||
return this.delegate.pathVariables();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<WebSession> session() {
|
||||
return this.delegate.session();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Implementation of the {@code Headers} interface that can be subclassed
|
||||
* to adapt the headers to a {@link HandlerFunction handler function}.
|
||||
* All methods default to calling through to the wrapped headers.
|
||||
*/
|
||||
public static class HeadersWrapper implements ServerRequest.Headers {
|
||||
|
||||
private final Headers headers;
|
||||
|
||||
/**
|
||||
* Create a new {@code HeadersWrapper} that wraps the given request.
|
||||
* @param headers the headers to wrap
|
||||
*/
|
||||
public HeadersWrapper(Headers headers) {
|
||||
Assert.notNull(headers, "'headers' must not be null");
|
||||
this.headers = headers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MediaType> accept() {
|
||||
return this.headers.accept();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Charset> acceptCharset() {
|
||||
return this.headers.acceptCharset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionalLong contentLength() {
|
||||
return this.headers.contentLength();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<MediaType> contentType() {
|
||||
return this.headers.contentType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InetSocketAddress host() {
|
||||
return this.headers.host();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<HttpRange> range() {
|
||||
return this.headers.range();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> header(String headerName) {
|
||||
return this.headers.header(headerName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders asHttpHeaders() {
|
||||
return this.headers.asHttpHeaders();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.function.server.support;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.reactive.HandlerResult;
|
||||
import org.springframework.web.reactive.HandlerResultHandler;
|
||||
import org.springframework.web.reactive.function.server.HandlerStrategies;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* {@code HandlerResultHandler} implementation that supports {@link ServerResponse}s.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @since 5.0
|
||||
*/
|
||||
public class ServerResponseResultHandler implements HandlerResultHandler {
|
||||
|
||||
private final HandlerStrategies strategies;
|
||||
|
||||
/**
|
||||
* Create a {@code ResponseResultHandler} with default strategies.
|
||||
*/
|
||||
public ServerResponseResultHandler() {
|
||||
this(HandlerStrategies.builder().build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@code ResponseResultHandler} with the given strategies.
|
||||
*/
|
||||
public ServerResponseResultHandler(HandlerStrategies strategies) {
|
||||
Assert.notNull(strategies, "'strategies' must not be null");
|
||||
this.strategies = strategies;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(HandlerResult result) {
|
||||
return result.getReturnValue()
|
||||
.filter(o -> o instanceof ServerResponse)
|
||||
.isPresent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) {
|
||||
ServerResponse response = (ServerResponse) result.getReturnValue().orElseThrow(
|
||||
IllegalStateException::new);
|
||||
return response.writeTo(exchange, this.strategies);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Classes supporting the {@code org.springframework.web.reactive.function} package.
|
||||
* Contains a {@code HandlerAdapter} that supports {@code HandlerFunction}s,
|
||||
* a {@code HandlerResultHandler} that supports {@code ServerResponse}s, and
|
||||
* a {@code ServerRequest} wrapper to adapt a request.
|
||||
*/
|
||||
package org.springframework.web.reactive.function.server.support;
|
||||
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.handler;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.context.support.ApplicationObjectSupport;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.PathMatcher;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.reactive.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.reactive.CorsProcessor;
|
||||
import org.springframework.web.cors.reactive.CorsUtils;
|
||||
import org.springframework.web.cors.reactive.DefaultCorsProcessor;
|
||||
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
|
||||
import org.springframework.web.reactive.HandlerMapping;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebHandler;
|
||||
import org.springframework.web.server.support.HttpRequestPathHelper;
|
||||
|
||||
/**
|
||||
* Abstract base class for {@link org.springframework.web.reactive.HandlerMapping}
|
||||
* implementations.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Juergen Hoeller
|
||||
* @since 5.0
|
||||
*/
|
||||
public abstract class AbstractHandlerMapping extends ApplicationObjectSupport implements HandlerMapping, Ordered {
|
||||
|
||||
private static final WebHandler REQUEST_HANDLED_HANDLER = exchange -> Mono.empty();
|
||||
|
||||
|
||||
private int order = Integer.MAX_VALUE; // default: same as non-Ordered
|
||||
|
||||
private HttpRequestPathHelper pathHelper = new HttpRequestPathHelper();
|
||||
|
||||
private PathMatcher pathMatcher = new AntPathMatcher();
|
||||
|
||||
private final UrlBasedCorsConfigurationSource globalCorsConfigSource = new UrlBasedCorsConfigurationSource();
|
||||
|
||||
private CorsProcessor corsProcessor = new DefaultCorsProcessor();
|
||||
|
||||
|
||||
/**
|
||||
* Specify the order value for this HandlerMapping bean.
|
||||
* <p>Default value is {@code Integer.MAX_VALUE}, meaning that it's non-ordered.
|
||||
* @see org.springframework.core.Ordered#getOrder()
|
||||
*/
|
||||
public final void setOrder(int order) {
|
||||
this.order = order;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int getOrder() {
|
||||
return this.order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set if the path should be URL-decoded. This sets the same property on the
|
||||
* underlying path helper.
|
||||
* @see HttpRequestPathHelper#setUrlDecode(boolean)
|
||||
*/
|
||||
public void setUrlDecode(boolean urlDecode) {
|
||||
this.pathHelper.setUrlDecode(urlDecode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link HttpRequestPathHelper} to use for resolution of lookup
|
||||
* paths. Use this to override the default implementation with a custom
|
||||
* subclass or to share common path helper settings across multiple
|
||||
* HandlerMappings.
|
||||
*/
|
||||
public void setPathHelper(HttpRequestPathHelper pathHelper) {
|
||||
this.pathHelper = pathHelper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the {@link HttpRequestPathHelper} implementation to use for
|
||||
* resolution of lookup paths.
|
||||
*/
|
||||
public HttpRequestPathHelper getPathHelper() {
|
||||
return this.pathHelper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the PathMatcher implementation to use for matching URL paths
|
||||
* against registered URL patterns. Default is AntPathMatcher.
|
||||
* @see org.springframework.util.AntPathMatcher
|
||||
*/
|
||||
public void setPathMatcher(PathMatcher pathMatcher) {
|
||||
Assert.notNull(pathMatcher, "PathMatcher must not be null");
|
||||
this.pathMatcher = pathMatcher;
|
||||
this.globalCorsConfigSource.setPathMatcher(pathMatcher);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the PathMatcher implementation to use for matching URL paths
|
||||
* against registered URL patterns.
|
||||
*/
|
||||
public PathMatcher getPathMatcher() {
|
||||
return this.pathMatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set "global" CORS configuration based on URL patterns. By default the
|
||||
* first matching URL pattern is combined with handler-level CORS
|
||||
* configuration if any.
|
||||
*/
|
||||
public void setCorsConfigurations(Map<String, CorsConfiguration> corsConfigurations) {
|
||||
this.globalCorsConfigSource.setCorsConfigurations(corsConfigurations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the "global" CORS configuration.
|
||||
*/
|
||||
public Map<String, CorsConfiguration> getCorsConfigurations() {
|
||||
return this.globalCorsConfigSource.getCorsConfigurations();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a custom {@link CorsProcessor} to use to apply the matched
|
||||
* {@link CorsConfiguration} for a request.
|
||||
* <p>By default an instance of {@link DefaultCorsProcessor} is used.
|
||||
*/
|
||||
public void setCorsProcessor(CorsProcessor corsProcessor) {
|
||||
Assert.notNull(corsProcessor, "CorsProcessor must not be null");
|
||||
this.corsProcessor = corsProcessor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the configured {@link CorsProcessor}.
|
||||
*/
|
||||
public CorsProcessor getCorsProcessor() {
|
||||
return this.corsProcessor;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Mono<Object> getHandler(ServerWebExchange exchange) {
|
||||
return getHandlerInternal(exchange).map(handler -> {
|
||||
if (CorsUtils.isCorsRequest(exchange.getRequest())) {
|
||||
CorsConfiguration configA = this.globalCorsConfigSource.getCorsConfiguration(exchange);
|
||||
CorsConfiguration configB = getCorsConfiguration(handler, exchange);
|
||||
CorsConfiguration config = (configA != null ? configA.combine(configB) : configB);
|
||||
if (!getCorsProcessor().processRequest(config, exchange) ||
|
||||
CorsUtils.isPreFlightRequest(exchange.getRequest())) {
|
||||
return REQUEST_HANDLED_HANDLER;
|
||||
}
|
||||
}
|
||||
return handler;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}.
|
||||
* <p>On CORS pre-flight requests this method should return a match not for
|
||||
* the pre-flight request but for the expected actual request based on the URL
|
||||
* path, the HTTP methods from the "Access-Control-Request-Method" header, and
|
||||
* the headers from the "Access-Control-Request-Headers" header thus allowing
|
||||
* the CORS configuration to be obtained via {@link #getCorsConfigurations},
|
||||
* @param exchange current exchange
|
||||
* @return {@code Mono} for the matching handler, if any
|
||||
*/
|
||||
protected abstract Mono<?> getHandlerInternal(ServerWebExchange exchange);
|
||||
|
||||
/**
|
||||
* Retrieve the CORS configuration for the given handler.
|
||||
* @param handler the handler to check (never {@code null})
|
||||
* @param exchange the current exchange
|
||||
* @return the CORS configuration for the handler, or {@code null} if none
|
||||
*/
|
||||
protected CorsConfiguration getCorsConfiguration(Object handler, ServerWebExchange exchange) {
|
||||
if (handler instanceof CorsConfigurationSource) {
|
||||
return ((CorsConfigurationSource) handler).getCorsConfiguration(exchange);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.handler;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* Abstract base class for URL-mapped
|
||||
* {@link org.springframework.web.reactive.HandlerMapping} implementations.
|
||||
*
|
||||
* <p>Supports direct matches, e.g. a registered "/test" matches "/test", and
|
||||
* various Ant-style pattern matches, e.g. a registered "/t*" pattern matches
|
||||
* both "/test" and "/team", "/test/*" matches all paths under "/test",
|
||||
* "/test/**" matches all paths below "/test". For details, see the
|
||||
* {@link org.springframework.util.AntPathMatcher AntPathMatcher} javadoc.
|
||||
*
|
||||
* <p>Will search all path patterns to find the most exact match for the
|
||||
* current request path. The most exact match is defined as the longest
|
||||
* path pattern that matches the current request path.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Juergen Hoeller
|
||||
* @since 5.0
|
||||
*/
|
||||
public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping {
|
||||
|
||||
private boolean useTrailingSlashMatch = false;
|
||||
|
||||
private boolean lazyInitHandlers = false;
|
||||
|
||||
private final Map<String, Object> handlerMap = new LinkedHashMap<>();
|
||||
|
||||
|
||||
/**
|
||||
* Whether to match to URLs irrespective of the presence of a trailing slash.
|
||||
* If enabled a URL pattern such as "/users" also matches to "/users/".
|
||||
* <p>The default value is {@code false}.
|
||||
*/
|
||||
public void setUseTrailingSlashMatch(boolean useTrailingSlashMatch) {
|
||||
this.useTrailingSlashMatch = useTrailingSlashMatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to match to URLs irrespective of the presence of a trailing slash.
|
||||
*/
|
||||
public boolean useTrailingSlashMatch() {
|
||||
return this.useTrailingSlashMatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether to lazily initialize handlers. Only applicable to
|
||||
* singleton handlers, as prototypes are always lazily initialized.
|
||||
* Default is "false", as eager initialization allows for more efficiency
|
||||
* through referencing the controller objects directly.
|
||||
* <p>If you want to allow your controllers to be lazily initialized,
|
||||
* make them "lazy-init" and set this flag to true. Just making them
|
||||
* "lazy-init" will not work, as they are initialized through the
|
||||
* references from the handler mapping in this case.
|
||||
*/
|
||||
public void setLazyInitHandlers(boolean lazyInitHandlers) {
|
||||
this.lazyInitHandlers = lazyInitHandlers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the registered handlers as an unmodifiable Map, with the registered path
|
||||
* as key and the handler object (or handler bean name in case of a lazy-init handler)
|
||||
* as value.
|
||||
*/
|
||||
public final Map<String, Object> getHandlerMap() {
|
||||
return Collections.unmodifiableMap(this.handlerMap);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Mono<Object> getHandlerInternal(ServerWebExchange exchange) {
|
||||
String lookupPath = getPathHelper().getLookupPathForRequest(exchange);
|
||||
Object handler;
|
||||
try {
|
||||
handler = lookupHandler(lookupPath, exchange);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
return Mono.error(ex);
|
||||
}
|
||||
|
||||
if (handler != null && logger.isDebugEnabled()) {
|
||||
logger.debug("Mapping [" + lookupPath + "] to " + handler);
|
||||
}
|
||||
else if (handler == null && logger.isTraceEnabled()) {
|
||||
logger.trace("No handler mapping found for [" + lookupPath + "]");
|
||||
}
|
||||
|
||||
return Mono.justOrEmpty(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up a handler instance for the given URL path.
|
||||
*
|
||||
* <p>Supports direct matches, e.g. a registered "/test" matches "/test",
|
||||
* and various Ant-style pattern matches, e.g. a registered "/t*" matches
|
||||
* both "/test" and "/team". For details, see the AntPathMatcher class.
|
||||
*
|
||||
* <p>Looks for the most exact pattern, where most exact is defined as
|
||||
* the longest path pattern.
|
||||
*
|
||||
* @param urlPath URL the bean is mapped to
|
||||
* @param exchange the current exchange
|
||||
* @return the associated handler instance, or {@code null} if not found
|
||||
* @see org.springframework.util.AntPathMatcher
|
||||
*/
|
||||
protected Object lookupHandler(String urlPath, ServerWebExchange exchange) throws Exception {
|
||||
// Direct match?
|
||||
Object handler = this.handlerMap.get(urlPath);
|
||||
if (handler != null) {
|
||||
return handleMatch(handler, urlPath, urlPath, exchange);
|
||||
}
|
||||
|
||||
// Pattern match?
|
||||
List<String> matches = new ArrayList<>();
|
||||
for (String pattern : this.handlerMap.keySet()) {
|
||||
if (getPathMatcher().match(pattern, urlPath)) {
|
||||
matches.add(pattern);
|
||||
}
|
||||
else if (useTrailingSlashMatch()) {
|
||||
if (!pattern.endsWith("/") && getPathMatcher().match(pattern + "/", urlPath)) {
|
||||
matches.add(pattern +"/");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String bestMatch = null;
|
||||
Comparator<String> comparator = getPathMatcher().getPatternComparator(urlPath);
|
||||
if (!matches.isEmpty()) {
|
||||
Collections.sort(matches, comparator);
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Matching patterns for request [" + urlPath + "] are " + matches);
|
||||
}
|
||||
bestMatch = matches.get(0);
|
||||
}
|
||||
if (bestMatch != null) {
|
||||
handler = this.handlerMap.get(bestMatch);
|
||||
if (handler == null) {
|
||||
if (bestMatch.endsWith("/")) {
|
||||
handler = this.handlerMap.get(bestMatch.substring(0, bestMatch.length() - 1));
|
||||
}
|
||||
if (handler == null) {
|
||||
throw new IllegalStateException(
|
||||
"Could not find handler for best pattern match [" + bestMatch + "]");
|
||||
}
|
||||
}
|
||||
String pathWithinMapping = getPathMatcher().extractPathWithinPattern(bestMatch, urlPath);
|
||||
return handleMatch(handler, bestMatch, pathWithinMapping, exchange);
|
||||
}
|
||||
|
||||
// No handler found...
|
||||
return null;
|
||||
}
|
||||
|
||||
private Object handleMatch(Object handler, String bestMatch, String pathWithinMapping,
|
||||
ServerWebExchange exchange) throws Exception {
|
||||
|
||||
// Bean name or resolved handler?
|
||||
if (handler instanceof String) {
|
||||
String handlerName = (String) handler;
|
||||
handler = getApplicationContext().getBean(handlerName);
|
||||
}
|
||||
|
||||
validateHandler(handler, exchange);
|
||||
|
||||
exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, pathWithinMapping);
|
||||
exchange.getAttributes().put(BEST_MATCHING_PATTERN_ATTRIBUTE, bestMatch);
|
||||
|
||||
return handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the given handler against the current request.
|
||||
* <p>The default implementation is empty. Can be overridden in subclasses,
|
||||
* for example to enforce specific preconditions expressed in URL mappings.
|
||||
* @param handler the handler object to validate
|
||||
* @param exchange current exchange
|
||||
* @throws Exception if validation failed
|
||||
*/
|
||||
@SuppressWarnings("UnusedParameters")
|
||||
protected void validateHandler(Object handler, ServerWebExchange exchange) throws Exception {
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the specified handler for the given URL paths.
|
||||
* @param urlPaths the URLs that the bean should be mapped to
|
||||
* @param beanName the name of the handler bean
|
||||
* @throws BeansException if the handler couldn't be registered
|
||||
* @throws IllegalStateException if there is a conflicting handler registered
|
||||
*/
|
||||
protected void registerHandler(String[] urlPaths, String beanName) throws BeansException, IllegalStateException {
|
||||
Assert.notNull(urlPaths, "URL path array must not be null");
|
||||
for (String urlPath : urlPaths) {
|
||||
registerHandler(urlPath, beanName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the specified handler for the given URL path.
|
||||
* @param urlPath the URL the bean should be mapped to
|
||||
* @param handler the handler instance or handler bean name String
|
||||
* (a bean name will automatically be resolved into the corresponding handler bean)
|
||||
* @throws BeansException if the handler couldn't be registered
|
||||
* @throws IllegalStateException if there is a conflicting handler registered
|
||||
*/
|
||||
protected void registerHandler(String urlPath, Object handler) throws BeansException, IllegalStateException {
|
||||
Assert.notNull(urlPath, "URL path must not be null");
|
||||
Assert.notNull(handler, "Handler object must not be null");
|
||||
Object resolvedHandler = handler;
|
||||
|
||||
// Eagerly resolve handler if referencing singleton via name.
|
||||
if (!this.lazyInitHandlers && handler instanceof String) {
|
||||
String handlerName = (String) handler;
|
||||
if (getApplicationContext().isSingleton(handlerName)) {
|
||||
resolvedHandler = getApplicationContext().getBean(handlerName);
|
||||
}
|
||||
}
|
||||
|
||||
Object mappedHandler = this.handlerMap.get(urlPath);
|
||||
if (mappedHandler != null) {
|
||||
if (mappedHandler != resolvedHandler) {
|
||||
throw new IllegalStateException(
|
||||
"Cannot map " + getHandlerDescription(handler) + " to URL path [" + urlPath +
|
||||
"]: There is already " + getHandlerDescription(mappedHandler) + " mapped.");
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.handlerMap.put(urlPath, resolvedHandler);
|
||||
if (logger.isInfoEnabled()) {
|
||||
logger.info("Mapped URL path [" + urlPath + "] onto " + getHandlerDescription(handler));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getHandlerDescription(Object handler) {
|
||||
return "handler " + (handler instanceof String ? "'" + handler + "'" : "of type [" + handler.getClass() + "]");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.handler;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* Implementation of the {@link org.springframework.web.reactive.HandlerMapping}
|
||||
* interface to map from URLs to request handler beans. Supports both mapping
|
||||
* to bean instances and mapping to bean names; the latter is required for
|
||||
* non-singleton handlers.
|
||||
*
|
||||
* <p>The "urlMap" property is suitable for populating the handler map with
|
||||
* bean instances. Mappings to bean names can be set via the "mappings"
|
||||
* property, in a form accepted by the {@code java.util.Properties} class,
|
||||
* like as follows:
|
||||
*
|
||||
* <pre>
|
||||
* /welcome.html=ticketController
|
||||
* /show.html=ticketController
|
||||
* </pre>
|
||||
*
|
||||
* <p>The syntax is {@code PATH=HANDLER_BEAN_NAME}. If the path doesn't begin
|
||||
* with a slash, one is prepended.
|
||||
*
|
||||
* <p>Supports direct matches, e.g. a registered "/test" matches "/test", and
|
||||
* various Ant-style pattern matches, e.g. a registered "/t*" pattern matches
|
||||
* both "/test" and "/team", "/test/*" matches all paths under "/test",
|
||||
* "/test/**" matches all paths below "/test". For details, see the
|
||||
* {@link org.springframework.util.AntPathMatcher AntPathMatcher} javadoc.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public class SimpleUrlHandlerMapping extends AbstractUrlHandlerMapping {
|
||||
|
||||
private final Map<String, Object> urlMap = new LinkedHashMap<>();
|
||||
|
||||
|
||||
/**
|
||||
* Map URL paths to handler bean names.
|
||||
* This is the typical way of configuring this HandlerMapping.
|
||||
* <p>Supports direct URL matches and Ant-style pattern matches. For syntax
|
||||
* details, see the {@link org.springframework.util.AntPathMatcher} javadoc.
|
||||
* @param mappings properties with URLs as keys and bean names as values
|
||||
* @see #setUrlMap
|
||||
*/
|
||||
public void setMappings(Properties mappings) {
|
||||
CollectionUtils.mergePropertiesIntoMap(mappings, this.urlMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a Map with URL paths as keys and handler beans (or handler bean names)
|
||||
* as values. Convenient for population with bean references.
|
||||
* <p>Supports direct URL matches and Ant-style pattern matches. For syntax
|
||||
* details, see the {@link org.springframework.util.AntPathMatcher} javadoc.
|
||||
* @param urlMap map with URLs as keys and beans as values
|
||||
* @see #setMappings
|
||||
*/
|
||||
public void setUrlMap(Map<String, ?> urlMap) {
|
||||
this.urlMap.putAll(urlMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow Map access to the URL path mappings, with the option to add or
|
||||
* override specific entries.
|
||||
* <p>Useful for specifying entries directly, for example via "urlMap[myKey]".
|
||||
* This is particularly useful for adding or overriding entries in child
|
||||
* bean definitions.
|
||||
*/
|
||||
public Map<String, ?> getUrlMap() {
|
||||
return this.urlMap;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Calls the {@link #registerHandlers} method in addition to the
|
||||
* superclass's initialization.
|
||||
*/
|
||||
@Override
|
||||
public void initApplicationContext() throws BeansException {
|
||||
super.initApplicationContext();
|
||||
registerHandlers(this.urlMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all handlers specified in the URL map for the corresponding paths.
|
||||
* @param urlMap Map with URL paths as keys and handler beans or bean names as values
|
||||
* @throws BeansException if a handler couldn't be registered
|
||||
* @throws IllegalStateException if there is a conflicting handler registered
|
||||
*/
|
||||
protected void registerHandlers(Map<String, Object> urlMap) throws BeansException {
|
||||
if (urlMap.isEmpty()) {
|
||||
logger.warn("Neither 'urlMap' nor 'mappings' set on SimpleUrlHandlerMapping");
|
||||
}
|
||||
else {
|
||||
for (Map.Entry<String, Object> entry : urlMap.entrySet()) {
|
||||
String url = entry.getKey();
|
||||
Object handler = entry.getValue();
|
||||
// Prepend with slash if not already present.
|
||||
if (!url.startsWith("/")) {
|
||||
url = "/" + url;
|
||||
}
|
||||
// Remove whitespace from handler bean name.
|
||||
if (handler instanceof String) {
|
||||
handler = ((String) handler).trim();
|
||||
}
|
||||
registerHandler(url, handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Provides HandlerMapping implementations including abstract base classes.
|
||||
*/
|
||||
package org.springframework.web.reactive.handler;
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Core interfaces and classes for Spring Web Reactive.
|
||||
*/
|
||||
package org.springframework.web.reactive;
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* Base {@link ResourceResolver} providing consistent logging.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public abstract class AbstractResourceResolver implements ResourceResolver {
|
||||
|
||||
protected final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
|
||||
@Override
|
||||
public Mono<Resource> resolveResource(ServerWebExchange exchange, String requestPath,
|
||||
List<? extends Resource> locations, ResourceResolverChain chain) {
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Resolving resource for request path \"" + requestPath + "\"");
|
||||
}
|
||||
return resolveResourceInternal(exchange, requestPath, locations, chain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> resolveUrlPath(String resourceUrlPath, List<? extends Resource> locations,
|
||||
ResourceResolverChain chain) {
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Resolving public URL for resource path \"" + resourceUrlPath + "\"");
|
||||
}
|
||||
|
||||
return resolveUrlPathInternal(resourceUrlPath, locations, chain);
|
||||
}
|
||||
|
||||
|
||||
protected abstract Mono<Resource> resolveResourceInternal(ServerWebExchange exchange,
|
||||
String requestPath, List<? extends Resource> locations, ResourceResolverChain chain);
|
||||
|
||||
protected abstract Mono<String> resolveUrlPathInternal(String resourceUrlPath,
|
||||
List<? extends Resource> locations, ResourceResolverChain chain);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Abstract base class for {@link VersionStrategy} implementations.
|
||||
*
|
||||
* <p>Supports versions as:
|
||||
* <ul>
|
||||
* <li>prefix in the request path, like "version/static/myresource.js"
|
||||
* <li>file name suffix in the request path, like "static/myresource-version.js"
|
||||
* </ul>
|
||||
*
|
||||
* <p>Note: This base class does <i>not</i> provide support for generating the
|
||||
* version string.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
* @since 5.0
|
||||
*/
|
||||
public abstract class AbstractVersionStrategy implements VersionStrategy {
|
||||
|
||||
protected final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private final VersionPathStrategy pathStrategy;
|
||||
|
||||
|
||||
protected AbstractVersionStrategy(VersionPathStrategy pathStrategy) {
|
||||
Assert.notNull(pathStrategy, "VersionPathStrategy is required");
|
||||
this.pathStrategy = pathStrategy;
|
||||
}
|
||||
|
||||
|
||||
public VersionPathStrategy getVersionPathStrategy() {
|
||||
return this.pathStrategy;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String extractVersion(String requestPath) {
|
||||
return this.pathStrategy.extractVersion(requestPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String removeVersion(String requestPath, String version) {
|
||||
return this.pathStrategy.removeVersion(requestPath, version);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String addVersion(String requestPath, String version) {
|
||||
return this.pathStrategy.addVersion(requestPath, version);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A prefix-based {@code VersionPathStrategy},
|
||||
* e.g. {@code "{version}/path/foo.js"}.
|
||||
*/
|
||||
protected static class PrefixVersionPathStrategy implements VersionPathStrategy {
|
||||
|
||||
private final String prefix;
|
||||
|
||||
public PrefixVersionPathStrategy(String version) {
|
||||
Assert.hasText(version, "'version' must not be empty");
|
||||
this.prefix = version;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String extractVersion(String requestPath) {
|
||||
return (requestPath.startsWith(this.prefix) ? this.prefix : null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String removeVersion(String requestPath, String version) {
|
||||
return requestPath.substring(this.prefix.length());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String addVersion(String path, String version) {
|
||||
if (path.startsWith(".")) {
|
||||
return path;
|
||||
}
|
||||
else {
|
||||
return (this.prefix.endsWith("/") || path.startsWith("/") ?
|
||||
this.prefix + path : this.prefix + '/' + path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* File name-based {@code VersionPathStrategy},
|
||||
* e.g. {@code "path/foo-{version}.css"}.
|
||||
*/
|
||||
protected static class FileNameVersionPathStrategy implements VersionPathStrategy {
|
||||
|
||||
private static final Pattern pattern = Pattern.compile("-(\\S*)\\.");
|
||||
|
||||
@Override
|
||||
public String extractVersion(String requestPath) {
|
||||
Matcher matcher = pattern.matcher(requestPath);
|
||||
if (matcher.find()) {
|
||||
String match = matcher.group(1);
|
||||
return (match.contains("-") ? match.substring(match.lastIndexOf('-') + 1) : match);
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String removeVersion(String requestPath, String version) {
|
||||
return StringUtils.delete(requestPath, "-" + version);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String addVersion(String requestPath, String version) {
|
||||
String baseFilename = StringUtils.stripFilenameExtension(requestPath);
|
||||
String extension = StringUtils.getFilenameExtension(requestPath);
|
||||
return (baseFilename + '-' + version + '.' + extension);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Scanner;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import reactor.core.Exceptions;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.SynchronousSink;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.DigestUtils;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* A {@link ResourceTransformer} implementation that helps handling resources
|
||||
* within HTML5 AppCache manifests for HTML5 offline applications.
|
||||
*
|
||||
* <p>This transformer:
|
||||
* <ul>
|
||||
* <li>modifies links to match the public URL paths that should be exposed to
|
||||
* clients, using configured {@code ResourceResolver} strategies
|
||||
* <li>appends a comment in the manifest, containing a Hash
|
||||
* (e.g. "# Hash: 9de0f09ed7caf84e885f1f0f11c7e326"), thus changing the content
|
||||
* of the manifest in order to trigger an appcache reload in the browser.
|
||||
* </ul>
|
||||
*
|
||||
* <p>All files that have the ".appcache" file extension, or the extension given in the constructor,
|
||||
* will be transformed by this class. This hash is computed using the content of the appcache manifest
|
||||
* and the content of the linked resources; so changing a resource linked in the manifest
|
||||
* or the manifest itself should invalidate the browser cache.
|
||||
*
|
||||
* <p>In order to serve manifest files with the proper {@code "text/manifest"} content type,
|
||||
* it is required to configure it with
|
||||
* {@code requestedContentTypeResolverBuilder.mediaType("appcache", MediaType.valueOf("text/manifest")}
|
||||
* in {@code WebReactiveConfiguration.configureContentTypeResolver()}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
* @since 5.0
|
||||
* @see <a href="https://html.spec.whatwg.org/multipage/browsers.html#offline">HTML5 offline applications spec</a>
|
||||
*/
|
||||
public class AppCacheManifestTransformer extends ResourceTransformerSupport {
|
||||
|
||||
private static final Collection<String> MANIFEST_SECTION_HEADERS =
|
||||
Arrays.asList("CACHE MANIFEST", "NETWORK:", "FALLBACK:", "CACHE:");
|
||||
|
||||
private static final String MANIFEST_HEADER = "CACHE MANIFEST";
|
||||
|
||||
private static final String CACHE_HEADER = "CACHE:";
|
||||
|
||||
private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
|
||||
|
||||
private static final Log logger = LogFactory.getLog(AppCacheManifestTransformer.class);
|
||||
|
||||
|
||||
private final String fileExtension;
|
||||
|
||||
|
||||
/**
|
||||
* Create an AppCacheResourceTransformer that transforms files with extension ".appcache".
|
||||
*/
|
||||
public AppCacheManifestTransformer() {
|
||||
this("appcache");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an AppCacheResourceTransformer that transforms files with the extension
|
||||
* given as a parameter.
|
||||
*/
|
||||
public AppCacheManifestTransformer(String fileExtension) {
|
||||
this.fileExtension = fileExtension;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Mono<Resource> transform(ServerWebExchange exchange, Resource inputResource,
|
||||
ResourceTransformerChain chain) {
|
||||
|
||||
return chain.transform(exchange, inputResource)
|
||||
.then(resource -> {
|
||||
String name = resource.getFilename();
|
||||
if (!this.fileExtension.equals(StringUtils.getFilenameExtension(name))) {
|
||||
return Mono.just(resource);
|
||||
}
|
||||
String content = new String(getResourceBytes(resource), DEFAULT_CHARSET);
|
||||
if (!content.startsWith(MANIFEST_HEADER)) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Manifest should start with 'CACHE MANIFEST', skip: " + resource);
|
||||
}
|
||||
return Mono.just(resource);
|
||||
}
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Transforming resource: " + resource);
|
||||
}
|
||||
return Flux.generate(new LineGenerator(content))
|
||||
.concatMap(info -> processLine(info, exchange, resource, chain))
|
||||
.collect(() -> new LineAggregator(resource, content), LineAggregator::add)
|
||||
.then(aggregator -> Mono.just(aggregator.createResource()));
|
||||
});
|
||||
}
|
||||
|
||||
private static byte[] getResourceBytes(Resource resource) {
|
||||
try {
|
||||
return FileCopyUtils.copyToByteArray(resource.getInputStream());
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw Exceptions.propagate(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private Mono<LineOutput> processLine(LineInfo info, ServerWebExchange exchange,
|
||||
Resource resource, ResourceTransformerChain chain) {
|
||||
|
||||
if (!info.isLink()) {
|
||||
return Mono.just(new LineOutput(info.getLine(), null));
|
||||
}
|
||||
|
||||
String link = toAbsolutePath(info.getLine(), exchange.getRequest());
|
||||
Mono<String> pathMono = resolveUrlPath(link, exchange, resource, chain)
|
||||
.doOnNext(path -> {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Link modified: " + path + " (original: " + info.getLine() + ")");
|
||||
}
|
||||
});
|
||||
|
||||
Mono<Resource> resourceMono = chain.getResolverChain()
|
||||
.resolveResource(null, info.getLine(), Collections.singletonList(resource));
|
||||
|
||||
return Flux.zip(pathMono, resourceMono, LineOutput::new).next();
|
||||
}
|
||||
|
||||
|
||||
private static class LineGenerator implements Consumer<SynchronousSink<LineInfo>> {
|
||||
|
||||
private final Scanner scanner;
|
||||
|
||||
private LineInfo previous;
|
||||
|
||||
|
||||
public LineGenerator(String content) {
|
||||
this.scanner = new Scanner(content);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void accept(SynchronousSink<LineInfo> sink) {
|
||||
if (this.scanner.hasNext()) {
|
||||
String line = this.scanner.nextLine();
|
||||
LineInfo current = new LineInfo(line, this.previous);
|
||||
sink.next(current);
|
||||
this.previous = current;
|
||||
}
|
||||
else {
|
||||
sink.complete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class LineInfo {
|
||||
|
||||
private final String line;
|
||||
|
||||
private final boolean cacheSection;
|
||||
|
||||
private final boolean link;
|
||||
|
||||
|
||||
public LineInfo(String line, LineInfo previousLine) {
|
||||
this.line = line;
|
||||
this.cacheSection = initCacheSectionFlag(line, previousLine);
|
||||
this.link = iniLinkFlag(line, this.cacheSection);
|
||||
}
|
||||
|
||||
private static boolean initCacheSectionFlag(String line, LineInfo previousLine) {
|
||||
if (MANIFEST_SECTION_HEADERS.contains(line.trim())) {
|
||||
return line.trim().equals(CACHE_HEADER);
|
||||
}
|
||||
else if (previousLine != null) {
|
||||
return previousLine.isCacheSection();
|
||||
}
|
||||
throw new IllegalStateException(
|
||||
"Manifest does not start with " + MANIFEST_HEADER + ": " + line);
|
||||
}
|
||||
|
||||
private static boolean iniLinkFlag(String line, boolean isCacheSection) {
|
||||
return (isCacheSection && StringUtils.hasText(line) && !line.startsWith("#")
|
||||
&& !line.startsWith("//") && !hasScheme(line));
|
||||
}
|
||||
|
||||
private static boolean hasScheme(String line) {
|
||||
int index = line.indexOf(":");
|
||||
return (line.startsWith("//") || (index > 0 && !line.substring(0, index).contains("/")));
|
||||
}
|
||||
|
||||
|
||||
public String getLine() {
|
||||
return this.line;
|
||||
}
|
||||
|
||||
public boolean isCacheSection() {
|
||||
return this.cacheSection;
|
||||
}
|
||||
|
||||
public boolean isLink() {
|
||||
return this.link;
|
||||
}
|
||||
}
|
||||
|
||||
private static class LineOutput {
|
||||
|
||||
private final String line;
|
||||
|
||||
private final Resource resource;
|
||||
|
||||
|
||||
public LineOutput(String line, Resource resource) {
|
||||
this.line = line;
|
||||
this.resource = resource;
|
||||
}
|
||||
|
||||
public String getLine() {
|
||||
return this.line;
|
||||
}
|
||||
|
||||
public Resource getResource() {
|
||||
return this.resource;
|
||||
}
|
||||
}
|
||||
|
||||
private static class LineAggregator {
|
||||
|
||||
private final StringWriter writer = new StringWriter();
|
||||
|
||||
private final ByteArrayOutputStream baos;
|
||||
|
||||
private final Resource resource;
|
||||
|
||||
|
||||
public LineAggregator(Resource resource, String content) {
|
||||
this.resource = resource;
|
||||
this.baos = new ByteArrayOutputStream(content.length());
|
||||
}
|
||||
|
||||
public void add(LineOutput lineOutput) {
|
||||
this.writer.write(lineOutput.getLine() + "\n");
|
||||
try {
|
||||
byte[] bytes = (lineOutput.getResource() != null ?
|
||||
DigestUtils.md5Digest(getResourceBytes(lineOutput.getResource())) :
|
||||
lineOutput.getLine().getBytes(DEFAULT_CHARSET));
|
||||
this.baos.write(bytes);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw Exceptions.propagate(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public TransformedResource createResource() {
|
||||
String hash = DigestUtils.md5DigestAsHex(this.baos.toByteArray());
|
||||
this.writer.write("\n" + "# Hash: " + hash);
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("AppCache file: [" + resource.getFilename()+ "] hash: [" + hash + "]");
|
||||
}
|
||||
byte[] bytes = this.writer.toString().getBytes(DEFAULT_CHARSET);
|
||||
return new TransformedResource(this.resource, bytes);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* A {@link ResourceResolver} that resolves resources from a {@link Cache} or
|
||||
* otherwise delegates to the resolver chain and caches the result.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
* @since 5.0
|
||||
*/
|
||||
public class CachingResourceResolver extends AbstractResourceResolver {
|
||||
|
||||
public static final String RESOLVED_RESOURCE_CACHE_KEY_PREFIX = "resolvedResource:";
|
||||
|
||||
public static final String RESOLVED_URL_PATH_CACHE_KEY_PREFIX = "resolvedUrlPath:";
|
||||
|
||||
|
||||
private final Cache cache;
|
||||
|
||||
|
||||
public CachingResourceResolver(CacheManager cacheManager, String cacheName) {
|
||||
this(cacheManager.getCache(cacheName));
|
||||
}
|
||||
|
||||
public CachingResourceResolver(Cache cache) {
|
||||
Assert.notNull(cache, "Cache is required");
|
||||
this.cache = cache;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the configured {@code Cache}.
|
||||
*/
|
||||
public Cache getCache() {
|
||||
return this.cache;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected Mono<Resource> resolveResourceInternal(ServerWebExchange exchange, String requestPath,
|
||||
List<? extends Resource> locations, ResourceResolverChain chain) {
|
||||
|
||||
String key = computeKey(exchange, requestPath);
|
||||
Resource cachedResource = this.cache.get(key, Resource.class);
|
||||
|
||||
if (cachedResource != null) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Found match: " + cachedResource);
|
||||
}
|
||||
return Mono.just(cachedResource);
|
||||
}
|
||||
|
||||
return chain.resolveResource(exchange, requestPath, locations)
|
||||
.doOnNext(resource -> {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Putting resolved resource in cache: " + resource);
|
||||
}
|
||||
this.cache.put(key, resource);
|
||||
});
|
||||
}
|
||||
|
||||
protected String computeKey(ServerWebExchange exchange, String requestPath) {
|
||||
StringBuilder key = new StringBuilder(RESOLVED_RESOURCE_CACHE_KEY_PREFIX);
|
||||
key.append(requestPath);
|
||||
if (exchange != null) {
|
||||
String encoding = exchange.getRequest().getHeaders().getFirst("Accept-Encoding");
|
||||
if (encoding != null && encoding.contains("gzip")) {
|
||||
key.append("+encoding=gzip");
|
||||
}
|
||||
}
|
||||
return key.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Mono<String> resolveUrlPathInternal(String resourceUrlPath,
|
||||
List<? extends Resource> locations, ResourceResolverChain chain) {
|
||||
|
||||
String key = RESOLVED_URL_PATH_CACHE_KEY_PREFIX + resourceUrlPath;
|
||||
String cachedUrlPath = this.cache.get(key, String.class);
|
||||
|
||||
if (cachedUrlPath != null) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Found match: \"" + cachedUrlPath + "\"");
|
||||
}
|
||||
return Mono.just(cachedUrlPath);
|
||||
}
|
||||
|
||||
return chain.resolveUrlPath(resourceUrlPath, locations)
|
||||
.doOnNext(resolvedPath -> {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Putting resolved resource URL path in cache: \"" + resolvedPath + "\"");
|
||||
}
|
||||
this.cache.put(key, resolvedPath);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* A {@link ResourceTransformer} that checks a {@link Cache} to see if a
|
||||
* previously transformed resource exists in the cache and returns it if found,
|
||||
* or otherwise delegates to the resolver chain and caches the result.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public class CachingResourceTransformer implements ResourceTransformer {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(CachingResourceTransformer.class);
|
||||
|
||||
private final Cache cache;
|
||||
|
||||
|
||||
public CachingResourceTransformer(CacheManager cacheManager, String cacheName) {
|
||||
this(cacheManager.getCache(cacheName));
|
||||
}
|
||||
|
||||
public CachingResourceTransformer(Cache cache) {
|
||||
Assert.notNull(cache, "Cache is required");
|
||||
this.cache = cache;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the configured {@code Cache}.
|
||||
*/
|
||||
public Cache getCache() {
|
||||
return this.cache;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Mono<Resource> transform(ServerWebExchange exchange, Resource resource,
|
||||
ResourceTransformerChain transformerChain) {
|
||||
|
||||
Resource cachedResource = this.cache.get(resource, Resource.class);
|
||||
if (cachedResource != null) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Found match: " + cachedResource);
|
||||
}
|
||||
return Mono.just(cachedResource);
|
||||
}
|
||||
|
||||
return transformerChain.transform(exchange, resource)
|
||||
.doOnNext(transformed -> {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Putting transformed resource in cache: " + transformed);
|
||||
}
|
||||
this.cache.put(resource, transformed);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.DigestUtils;
|
||||
|
||||
/**
|
||||
* A {@code VersionStrategy} that calculates an Hex MD5 hashes from the content
|
||||
* of the resource and appends it to the file name, e.g.
|
||||
* {@code "styles/main-e36d2e05253c6c7085a91522ce43a0b4.css"}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
* @since 5.0
|
||||
* @see VersionResourceResolver
|
||||
*/
|
||||
public class ContentVersionStrategy extends AbstractVersionStrategy {
|
||||
|
||||
|
||||
public ContentVersionStrategy() {
|
||||
super(new FileNameVersionPathStrategy());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getResourceVersion(Resource resource) {
|
||||
try {
|
||||
return DigestUtils.md5DigestAsHex(resource.getInputStream());
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new IllegalStateException("Failed to calculate hash for " + resource, ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import reactor.core.Exceptions;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* A {@link ResourceTransformer} implementation that modifies links in a CSS
|
||||
* file to match the public URL paths that should be exposed to clients (e.g.
|
||||
* with an MD5 content-based hash inserted in the URL).
|
||||
*
|
||||
* <p>The implementation looks for links in CSS {@code @import} statements and
|
||||
* also inside CSS {@code url()} functions. All links are then passed through the
|
||||
* {@link ResourceResolverChain} and resolved relative to the location of the
|
||||
* containing CSS file. If successfully resolved, the link is modified, otherwise
|
||||
* the original link is preserved.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public class CssLinkResourceTransformer extends ResourceTransformerSupport {
|
||||
|
||||
private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
|
||||
|
||||
private static final Log logger = LogFactory.getLog(CssLinkResourceTransformer.class);
|
||||
|
||||
private final List<LinkParser> linkParsers = new ArrayList<>(2);
|
||||
|
||||
|
||||
public CssLinkResourceTransformer() {
|
||||
this.linkParsers.add(new ImportLinkParser());
|
||||
this.linkParsers.add(new UrlFunctionLinkParser());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Mono<Resource> transform(ServerWebExchange exchange, Resource resource,
|
||||
ResourceTransformerChain transformerChain) {
|
||||
|
||||
return transformerChain.transform(exchange, resource)
|
||||
.then(newResource -> {
|
||||
String filename = newResource.getFilename();
|
||||
if (!"css".equals(StringUtils.getFilenameExtension(filename)) ||
|
||||
resource instanceof GzipResourceResolver.GzippedResource) {
|
||||
return Mono.just(newResource);
|
||||
}
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Transforming resource: " + newResource);
|
||||
}
|
||||
|
||||
byte[] bytes = new byte[0];
|
||||
try {
|
||||
bytes = FileCopyUtils.copyToByteArray(newResource.getInputStream());
|
||||
}
|
||||
catch (IOException ex) {
|
||||
return Mono.error(Exceptions.propagate(ex));
|
||||
}
|
||||
String fullContent = new String(bytes, DEFAULT_CHARSET);
|
||||
List<Segment> segments = parseContent(fullContent);
|
||||
|
||||
if (segments.isEmpty()) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("No links found.");
|
||||
}
|
||||
return Mono.just(newResource);
|
||||
}
|
||||
|
||||
return Flux.fromIterable(segments)
|
||||
.concatMap(segment -> {
|
||||
String segmentContent = segment.getContent(fullContent);
|
||||
if (segment.isLink() && !hasScheme(segmentContent)) {
|
||||
String link = toAbsolutePath(segmentContent, exchange.getRequest());
|
||||
return resolveUrlPath(link, exchange, newResource, transformerChain)
|
||||
.defaultIfEmpty(segmentContent);
|
||||
}
|
||||
else {
|
||||
return Mono.just(segmentContent);
|
||||
}
|
||||
})
|
||||
.reduce(new StringWriter(), (writer, chunk) -> {
|
||||
writer.write(chunk);
|
||||
return writer;
|
||||
})
|
||||
.then(writer -> {
|
||||
byte[] newContent = writer.toString().getBytes(DEFAULT_CHARSET);
|
||||
return Mono.just(new TransformedResource(resource, newContent));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private List<Segment> parseContent(String fullContent) {
|
||||
|
||||
List<Segment> links = new ArrayList<>();
|
||||
for (LinkParser parser : this.linkParsers) {
|
||||
links.addAll(parser.parseLinks(fullContent));
|
||||
}
|
||||
|
||||
if (links.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Collections.sort(links);
|
||||
|
||||
int index = 0;
|
||||
List<Segment> allSegments = new ArrayList<>(links);
|
||||
for (Segment link : links) {
|
||||
allSegments.add(new Segment(index, link.getStart(), false));
|
||||
index = link.getEnd();
|
||||
}
|
||||
if (index < fullContent.length()) {
|
||||
allSegments.add(new Segment(index, fullContent.length(), false));
|
||||
}
|
||||
|
||||
Collections.sort(allSegments);
|
||||
|
||||
return allSegments;
|
||||
}
|
||||
|
||||
private boolean hasScheme(String link) {
|
||||
int schemeIndex = link.indexOf(":");
|
||||
return (schemeIndex > 0 && !link.substring(0, schemeIndex).contains("/")) || link.indexOf("//") == 0;
|
||||
}
|
||||
|
||||
|
||||
@FunctionalInterface
|
||||
protected interface LinkParser {
|
||||
|
||||
Set<Segment> parseLinks(String fullContent);
|
||||
|
||||
}
|
||||
|
||||
|
||||
protected static abstract class AbstractLinkParser implements LinkParser {
|
||||
|
||||
/** Return the keyword to use to search for links. */
|
||||
protected abstract String getKeyword();
|
||||
|
||||
@Override
|
||||
public Set<Segment> parseLinks(String fullContent) {
|
||||
Set<Segment> linksToAdd = new HashSet<>(8);
|
||||
int index = 0;
|
||||
do {
|
||||
index = fullContent.indexOf(getKeyword(), index);
|
||||
if (index == -1) {
|
||||
break;
|
||||
}
|
||||
index = skipWhitespace(fullContent, index + getKeyword().length());
|
||||
if (fullContent.charAt(index) == '\'') {
|
||||
index = addLink(index, "'", fullContent, linksToAdd);
|
||||
}
|
||||
else if (fullContent.charAt(index) == '"') {
|
||||
index = addLink(index, "\"", fullContent, linksToAdd);
|
||||
}
|
||||
else {
|
||||
index = extractLink(index, fullContent, linksToAdd);
|
||||
|
||||
}
|
||||
}
|
||||
while (true);
|
||||
return linksToAdd;
|
||||
}
|
||||
|
||||
private int skipWhitespace(String content, int index) {
|
||||
while (true) {
|
||||
if (Character.isWhitespace(content.charAt(index))) {
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
protected int addLink(int index, String endKey, String content, Set<Segment> linksToAdd) {
|
||||
int start = index + 1;
|
||||
int end = content.indexOf(endKey, start);
|
||||
linksToAdd.add(new Segment(start, end, true));
|
||||
return end + endKey.length();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked after a keyword match, after whitespaces removed, and when
|
||||
* the next char is neither a single nor double quote.
|
||||
*/
|
||||
protected abstract int extractLink(int index, String content, Set<Segment> linksToAdd);
|
||||
|
||||
}
|
||||
|
||||
|
||||
private static class ImportLinkParser extends AbstractLinkParser {
|
||||
|
||||
@Override
|
||||
protected String getKeyword() {
|
||||
return "@import";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int extractLink(int index, String content, Set<Segment> linksToAdd) {
|
||||
if (content.substring(index, index + 4).equals("url(")) {
|
||||
// Ignore, UrlLinkParser will take care
|
||||
}
|
||||
else if (logger.isErrorEnabled()) {
|
||||
logger.error("Unexpected syntax for @import link at index " + index);
|
||||
}
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class UrlFunctionLinkParser extends AbstractLinkParser {
|
||||
|
||||
@Override
|
||||
protected String getKeyword() {
|
||||
return "url(";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int extractLink(int index, String content, Set<Segment> linksToAdd) {
|
||||
// A url() function without unquoted
|
||||
return addLink(index - 1, ")", content, linksToAdd);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class Segment implements Comparable<Segment> {
|
||||
|
||||
private final int start;
|
||||
|
||||
private final int end;
|
||||
|
||||
private final boolean link;
|
||||
|
||||
|
||||
public Segment(int start, int end, boolean isLink) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
this.link = isLink;
|
||||
}
|
||||
|
||||
|
||||
public int getStart() {
|
||||
return this.start;
|
||||
}
|
||||
|
||||
public int getEnd() {
|
||||
return this.end;
|
||||
}
|
||||
|
||||
public boolean isLink() {
|
||||
return this.link;
|
||||
}
|
||||
|
||||
public String getContent(String fullContent) {
|
||||
return fullContent.substring(this.start, this.end);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Segment other) {
|
||||
return (this.start < other.start ? -1 : (this.start == other.start ? 0 : 1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj != null && obj instanceof Segment) {
|
||||
Segment other = (Segment) obj;
|
||||
return (this.start == other.start && this.end == other.end);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return this.start * 31 + this.end;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* A default implementation of {@link ResourceResolverChain} for invoking a list
|
||||
* of {@link ResourceResolver}s.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
class DefaultResourceResolverChain implements ResourceResolverChain {
|
||||
|
||||
private final List<ResourceResolver> resolvers = new ArrayList<>();
|
||||
|
||||
private int index = -1;
|
||||
|
||||
|
||||
public DefaultResourceResolverChain(List<? extends ResourceResolver> resolvers) {
|
||||
if (resolvers != null) {
|
||||
this.resolvers.addAll(resolvers);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Mono<Resource> resolveResource(ServerWebExchange exchange, String requestPath,
|
||||
List<? extends Resource> locations) {
|
||||
|
||||
ResourceResolver resolver = getNext();
|
||||
if (resolver == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return resolver.resolveResource(exchange, requestPath, locations, this);
|
||||
}
|
||||
finally {
|
||||
this.index--;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> resolveUrlPath(String resourcePath, List<? extends Resource> locations) {
|
||||
ResourceResolver resolver = getNext();
|
||||
if (resolver == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return resolver.resolveUrlPath(resourcePath, locations, this);
|
||||
}
|
||||
finally {
|
||||
this.index--;
|
||||
}
|
||||
}
|
||||
|
||||
private ResourceResolver getNext() {
|
||||
Assert.state(this.index <= this.resolvers.size(),
|
||||
"Current index exceeds the number of configured ResourceResolvers");
|
||||
|
||||
if (this.index == (this.resolvers.size() - 1)) {
|
||||
return null;
|
||||
}
|
||||
this.index++;
|
||||
return this.resolvers.get(this.index);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* A default implementation of {@link ResourceTransformerChain} for invoking
|
||||
* a list of {@link ResourceTransformer}s.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
class DefaultResourceTransformerChain implements ResourceTransformerChain {
|
||||
|
||||
private final ResourceResolverChain resolverChain;
|
||||
|
||||
private final List<ResourceTransformer> transformers = new ArrayList<>();
|
||||
|
||||
private int index = -1;
|
||||
|
||||
|
||||
public DefaultResourceTransformerChain(ResourceResolverChain resolverChain,
|
||||
List<ResourceTransformer> transformers) {
|
||||
|
||||
Assert.notNull(resolverChain, "ResourceResolverChain is required");
|
||||
this.resolverChain = resolverChain;
|
||||
if (transformers != null) {
|
||||
this.transformers.addAll(transformers);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public ResourceResolverChain getResolverChain() {
|
||||
return this.resolverChain;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Mono<Resource> transform(ServerWebExchange exchange, Resource resource) {
|
||||
ResourceTransformer transformer = getNext();
|
||||
if (transformer == null) {
|
||||
return Mono.just(resource);
|
||||
}
|
||||
try {
|
||||
return transformer.transform(exchange, resource, this);
|
||||
}
|
||||
finally {
|
||||
this.index--;
|
||||
}
|
||||
}
|
||||
|
||||
private ResourceTransformer getNext() {
|
||||
Assert.state(this.index <= this.transformers.size(),
|
||||
"Current index exceeds the number of configured ResourceTransformer's");
|
||||
|
||||
if (this.index == (this.transformers.size() - 1)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.index++;
|
||||
return this.transformers.get(this.index);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
|
||||
/**
|
||||
* A {@code VersionStrategy} that relies on a fixed version applied as a request
|
||||
* path prefix, e.g. reduced SHA, version name, release date, etc.
|
||||
*
|
||||
* <p>This is useful for example when {@link ContentVersionStrategy} cannot be
|
||||
* used such as when using JavaScript module loaders which are in charge of
|
||||
* loading the JavaScript resources and need to know their relative paths.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
* @since 5.0
|
||||
* @see VersionResourceResolver
|
||||
*/
|
||||
public class FixedVersionStrategy extends AbstractVersionStrategy {
|
||||
|
||||
private final String version;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new FixedVersionStrategy with the given version string.
|
||||
* @param version the fixed version string to use
|
||||
*/
|
||||
public FixedVersionStrategy(String version) {
|
||||
super(new PrefixVersionPathStrategy(version));
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getResourceVersion(Resource resource) {
|
||||
return this.version;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.util.List;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.io.AbstractResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* A {@code ResourceResolver} that delegates to the chain to locate a resource
|
||||
* and then attempts to find a variation with the ".gz" extension.
|
||||
*
|
||||
* <p>The resolver gets involved only if the "Accept-Encoding" request header
|
||||
* contains the value "gzip" indicating the client accepts gzipped responses.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public class GzipResourceResolver extends AbstractResourceResolver {
|
||||
|
||||
@Override
|
||||
protected Mono<Resource> resolveResourceInternal(ServerWebExchange exchange, String requestPath,
|
||||
List<? extends Resource> locations, ResourceResolverChain chain) {
|
||||
|
||||
return chain.resolveResource(exchange, requestPath, locations)
|
||||
.map(resource -> {
|
||||
if (exchange == null || isGzipAccepted(exchange)) {
|
||||
try {
|
||||
Resource gzipped = new GzippedResource(resource);
|
||||
if (gzipped.exists()) {
|
||||
resource = gzipped;
|
||||
}
|
||||
}
|
||||
catch (IOException ex) {
|
||||
logger.trace("No gzipped resource for [" + resource.getFilename() + "]", ex);
|
||||
}
|
||||
}
|
||||
return resource;
|
||||
});
|
||||
}
|
||||
|
||||
private boolean isGzipAccepted(ServerWebExchange exchange) {
|
||||
String value = exchange.getRequest().getHeaders().getFirst("Accept-Encoding");
|
||||
return (value != null && value.toLowerCase().contains("gzip"));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Mono<String> resolveUrlPathInternal(String resourceUrlPath,
|
||||
List<? extends Resource> locations, ResourceResolverChain chain) {
|
||||
|
||||
return chain.resolveUrlPath(resourceUrlPath, locations);
|
||||
}
|
||||
|
||||
|
||||
static final class GzippedResource extends AbstractResource implements HttpResource {
|
||||
|
||||
private final Resource original;
|
||||
|
||||
private final Resource gzipped;
|
||||
|
||||
public GzippedResource(Resource original) throws IOException {
|
||||
this.original = original;
|
||||
this.gzipped = original.createRelative(original.getFilename() + ".gz");
|
||||
}
|
||||
|
||||
public InputStream getInputStream() throws IOException {
|
||||
return this.gzipped.getInputStream();
|
||||
}
|
||||
|
||||
public boolean exists() {
|
||||
return this.gzipped.exists();
|
||||
}
|
||||
|
||||
public boolean isReadable() {
|
||||
return this.gzipped.isReadable();
|
||||
}
|
||||
|
||||
public boolean isOpen() {
|
||||
return this.gzipped.isOpen();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFile() {
|
||||
return this.gzipped.isFile();
|
||||
}
|
||||
|
||||
public URL getURL() throws IOException {
|
||||
return this.gzipped.getURL();
|
||||
}
|
||||
|
||||
public URI getURI() throws IOException {
|
||||
return this.gzipped.getURI();
|
||||
}
|
||||
|
||||
public File getFile() throws IOException {
|
||||
return this.gzipped.getFile();
|
||||
}
|
||||
|
||||
public long contentLength() throws IOException {
|
||||
return this.gzipped.contentLength();
|
||||
}
|
||||
|
||||
public long lastModified() throws IOException {
|
||||
return this.gzipped.lastModified();
|
||||
}
|
||||
|
||||
public Resource createRelative(String relativePath) throws IOException {
|
||||
return this.gzipped.createRelative(relativePath);
|
||||
}
|
||||
|
||||
public String getFilename() {
|
||||
return this.original.getFilename();
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return this.gzipped.getDescription();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders getResponseHeaders() {
|
||||
HttpHeaders headers;
|
||||
if(this.original instanceof HttpResource) {
|
||||
headers = ((HttpResource) this.original).getResponseHeaders();
|
||||
}
|
||||
else {
|
||||
headers = new HttpHeaders();
|
||||
}
|
||||
headers.add(HttpHeaders.CONTENT_ENCODING, "gzip");
|
||||
return headers;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
|
||||
/**
|
||||
* Extended interface for a {@link Resource} to be written to an
|
||||
* HTTP response.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @since 5.0
|
||||
*/
|
||||
public interface HttpResource extends Resource {
|
||||
|
||||
/**
|
||||
* The HTTP headers to be contributed to the HTTP response
|
||||
* that serves the current resource.
|
||||
* @return the HTTP response headers
|
||||
*/
|
||||
HttpHeaders getResponseHeaders();
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URLDecoder;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.UrlResource;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* A simple {@code ResourceResolver} that tries to find a resource under the given
|
||||
* locations matching to the request path.
|
||||
*
|
||||
* <p>This resolver does not delegate to the {@code ResourceResolverChain} and is
|
||||
* expected to be configured at the end in a chain of resolvers.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public class PathResourceResolver extends AbstractResourceResolver {
|
||||
|
||||
private Resource[] allowedLocations;
|
||||
|
||||
|
||||
/**
|
||||
* By default when a Resource is found, the path of the resolved resource is
|
||||
* compared to ensure it's under the input location where it was found.
|
||||
* However sometimes that may not be the case, e.g. when
|
||||
* {@link CssLinkResourceTransformer}
|
||||
* resolves public URLs of links it contains, the CSS file is the location
|
||||
* and the resources being resolved are css files, images, fonts and others
|
||||
* located in adjacent or parent directories.
|
||||
* <p>This property allows configuring a complete list of locations under
|
||||
* which resources must be so that if a resource is not under the location
|
||||
* relative to which it was found, this list may be checked as well.
|
||||
* <p>By default {@link ResourceWebHandler} initializes this property
|
||||
* to match its list of locations.
|
||||
* @param locations the list of allowed locations
|
||||
*/
|
||||
public void setAllowedLocations(Resource... locations) {
|
||||
this.allowedLocations = locations;
|
||||
}
|
||||
|
||||
public Resource[] getAllowedLocations() {
|
||||
return this.allowedLocations;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected Mono<Resource> resolveResourceInternal(ServerWebExchange exchange, String requestPath,
|
||||
List<? extends Resource> locations, ResourceResolverChain chain) {
|
||||
|
||||
return getResource(requestPath, locations);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Mono<String> resolveUrlPathInternal(String path, List<? extends Resource> locations,
|
||||
ResourceResolverChain chain) {
|
||||
|
||||
if (StringUtils.hasText(path)) {
|
||||
return getResource(path, locations).map(resource -> path);
|
||||
}
|
||||
else {
|
||||
return Mono.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private Mono<Resource> getResource(String resourcePath, List<? extends Resource> locations) {
|
||||
return Flux.fromIterable(locations)
|
||||
.concatMap(location -> getResource(resourcePath, location))
|
||||
.next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the resource under the given location.
|
||||
* <p>The default implementation checks if there is a readable
|
||||
* {@code Resource} for the given path relative to the location.
|
||||
* @param resourcePath the path to the resource
|
||||
* @param location the location to check
|
||||
* @return the resource, or {@code null} if none found
|
||||
*/
|
||||
protected Mono<Resource> getResource(String resourcePath, Resource location) {
|
||||
try {
|
||||
Resource resource = location.createRelative(resourcePath);
|
||||
if (resource.exists() && resource.isReadable()) {
|
||||
if (checkResource(resource, location)) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Found match: " + resource);
|
||||
}
|
||||
return Mono.just(resource);
|
||||
}
|
||||
else if (logger.isTraceEnabled()) {
|
||||
logger.trace("Resource path=\"" + resourcePath + "\" was successfully resolved " +
|
||||
"but resource=\"" + resource.getURL() + "\" is neither under the " +
|
||||
"current location=\"" + location.getURL() + "\" nor under any of the " +
|
||||
"allowed locations=" + Arrays.asList(getAllowedLocations()));
|
||||
}
|
||||
}
|
||||
else if (logger.isTraceEnabled()) {
|
||||
logger.trace("No match for location: " + location);
|
||||
}
|
||||
return Mono.empty();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Failure checking for relative resource under location + " + location, ex);
|
||||
}
|
||||
return Mono.error(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform additional checks on a resolved resource beyond checking whether the
|
||||
* resources exists and is readable. The default implementation also verifies
|
||||
* the resource is either under the location relative to which it was found or
|
||||
* is under one of the {@link #setAllowedLocations allowed locations}.
|
||||
* @param resource the resource to check
|
||||
* @param location the location relative to which the resource was found
|
||||
* @return "true" if resource is in a valid location, "false" otherwise.
|
||||
*/
|
||||
protected boolean checkResource(Resource resource, Resource location) throws IOException {
|
||||
if (isResourceUnderLocation(resource, location)) {
|
||||
return true;
|
||||
}
|
||||
if (getAllowedLocations() != null) {
|
||||
for (Resource current : getAllowedLocations()) {
|
||||
if (isResourceUnderLocation(resource, current)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isResourceUnderLocation(Resource resource, Resource location) throws IOException {
|
||||
if (resource.getClass() != location.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String resourcePath;
|
||||
String locationPath;
|
||||
|
||||
if (resource instanceof UrlResource) {
|
||||
resourcePath = resource.getURL().toExternalForm();
|
||||
locationPath = StringUtils.cleanPath(location.getURL().toString());
|
||||
}
|
||||
else if (resource instanceof ClassPathResource) {
|
||||
resourcePath = ((ClassPathResource) resource).getPath();
|
||||
locationPath = StringUtils.cleanPath(((ClassPathResource) location).getPath());
|
||||
}
|
||||
else {
|
||||
resourcePath = resource.getURL().getPath();
|
||||
locationPath = StringUtils.cleanPath(location.getURL().getPath());
|
||||
}
|
||||
|
||||
if (locationPath.equals(resourcePath)) {
|
||||
return true;
|
||||
}
|
||||
locationPath = (locationPath.endsWith("/") || locationPath.isEmpty() ? locationPath : locationPath + "/");
|
||||
if (!resourcePath.startsWith(locationPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (resourcePath.contains("%")) {
|
||||
// Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars...
|
||||
if (URLDecoder.decode(resourcePath, "UTF-8").contains("../")) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Resolved resource path contains \"../\" after decoding: " + resourcePath);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* A strategy for resolving a request to a server-side resource.
|
||||
*
|
||||
* <p>Provides mechanisms for resolving an incoming request to an actual
|
||||
* {@link Resource} and for obtaining the
|
||||
* public URL path that clients should use when requesting the resource.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public interface ResourceResolver {
|
||||
|
||||
/**
|
||||
* Resolve the supplied request and request path to a {@link Resource} that
|
||||
* exists under one of the given resource locations.
|
||||
* @param exchange the current exchange
|
||||
* @param requestPath the portion of the request path to use
|
||||
* @param locations the locations to search in when looking up resources
|
||||
* @param chain the chain of remaining resolvers to delegate to
|
||||
* @return the resolved resource or an empty {@code Mono} if unresolved
|
||||
*/
|
||||
Mono<Resource> resolveResource(ServerWebExchange exchange, String requestPath,
|
||||
List<? extends Resource> locations, ResourceResolverChain chain);
|
||||
|
||||
/**
|
||||
* Resolve the externally facing <em>public</em> URL path for clients to use
|
||||
* to access the resource that is located at the given <em>internal</em>
|
||||
* resource path.
|
||||
* <p>This is useful when rendering URL links to clients.
|
||||
* @param resourcePath the internal resource path
|
||||
* @param locations the locations to search in when looking up resources
|
||||
* @param chain the chain of resolvers to delegate to
|
||||
* @return the resolved public URL path or an empty {@code Mono} if unresolved
|
||||
*/
|
||||
Mono<String> resolveUrlPath(String resourcePath, List<? extends Resource> locations,
|
||||
ResourceResolverChain chain);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* A contract for invoking a chain of {@link ResourceResolver}s where each resolver
|
||||
* is given a reference to the chain allowing it to delegate when necessary.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public interface ResourceResolverChain {
|
||||
|
||||
/**
|
||||
* Resolve the supplied request and request path to a {@link Resource} that
|
||||
* exists under one of the given resource locations.
|
||||
* @param exchange the current exchange
|
||||
* @param requestPath the portion of the request path to use
|
||||
* @param locations the locations to search in when looking up resources
|
||||
* @return the resolved resource or an empty {@code Mono} if unresolved
|
||||
*/
|
||||
Mono<Resource> resolveResource(ServerWebExchange exchange, String requestPath,
|
||||
List<? extends Resource> locations);
|
||||
|
||||
/**
|
||||
* Resolve the externally facing <em>public</em> URL path for clients to use
|
||||
* to access the resource that is located at the given <em>internal</em>
|
||||
* resource path.
|
||||
* <p>This is useful when rendering URL links to clients.
|
||||
* @param resourcePath the internal resource path
|
||||
* @param locations the locations to search in when looking up resources
|
||||
* @return the resolved public URL path or an empty {@code Mono} if unresolved
|
||||
*/
|
||||
Mono<String> resolveUrlPath(String resourcePath, List<? extends Resource> locations);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* An abstraction for transforming the content of a resource.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ResourceTransformer {
|
||||
|
||||
/**
|
||||
* Transform the given resource.
|
||||
* @param exchange the current exchange
|
||||
* @param resource the resource to transform
|
||||
* @param transformerChain the chain of remaining transformers to delegate to
|
||||
* @return the transformed resource (never {@code null})
|
||||
*/
|
||||
Mono<Resource> transform(ServerWebExchange exchange, Resource resource,
|
||||
ResourceTransformerChain transformerChain);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* A contract for invoking a chain of {@link ResourceTransformer}s where each resolver
|
||||
* is given a reference to the chain allowing it to delegate when necessary.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public interface ResourceTransformerChain {
|
||||
|
||||
/**
|
||||
* Return the {@code ResourceResolverChain} that was used to resolve the
|
||||
* {@code Resource} being transformed. This may be needed for resolving
|
||||
* related resources, e.g. links to other resources.
|
||||
*/
|
||||
ResourceResolverChain getResolverChain();
|
||||
|
||||
/**
|
||||
* Transform the given resource.
|
||||
* @param exchange the current exchange
|
||||
* @param resource the candidate resource to transform
|
||||
* @return the transformed or the same resource, never {@code null}
|
||||
*/
|
||||
Mono<Resource> transform(ServerWebExchange exchange, Resource resource);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* A base class for a {@code ResourceTransformer} with an optional helper method
|
||||
* for resolving public links within a transformed resource.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
* @since 5.0
|
||||
*/
|
||||
public abstract class ResourceTransformerSupport implements ResourceTransformer {
|
||||
|
||||
private ResourceUrlProvider resourceUrlProvider;
|
||||
|
||||
|
||||
/**
|
||||
* Configure a {@link ResourceUrlProvider} to use when resolving the public
|
||||
* URL of links in a transformed resource (e.g. import links in a CSS file).
|
||||
* This is required only for links expressed as full paths and not for
|
||||
* relative links.
|
||||
* @param resourceUrlProvider the URL provider to use
|
||||
*/
|
||||
public void setResourceUrlProvider(ResourceUrlProvider resourceUrlProvider) {
|
||||
this.resourceUrlProvider = resourceUrlProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the configured {@code ResourceUrlProvider}.
|
||||
*/
|
||||
public ResourceUrlProvider getResourceUrlProvider() {
|
||||
return this.resourceUrlProvider;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A transformer can use this method when a resource being transformed
|
||||
* contains links to other resources. Such links need to be replaced with the
|
||||
* public facing link as determined by the resource resolver chain (e.g. the
|
||||
* public URL may have a version inserted).
|
||||
* @param resourcePath the path to a resource that needs to be re-written
|
||||
* @param exchange the current exchange
|
||||
* @param resource the resource being transformed
|
||||
* @param transformerChain the transformer chain
|
||||
* @return the resolved URL or null
|
||||
*/
|
||||
protected Mono<String> resolveUrlPath(String resourcePath, ServerWebExchange exchange,
|
||||
Resource resource, ResourceTransformerChain transformerChain) {
|
||||
|
||||
if (resourcePath.startsWith("/")) {
|
||||
// full resource path
|
||||
ResourceUrlProvider urlProvider = getResourceUrlProvider();
|
||||
return (urlProvider != null ? urlProvider.getForRequestUrl(exchange, resourcePath) : Mono.empty());
|
||||
}
|
||||
else {
|
||||
// try resolving as relative path
|
||||
return transformerChain.getResolverChain()
|
||||
.resolveUrlPath(resourcePath, Collections.singletonList(resource));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the given relative request path to an absolute path,
|
||||
* taking the path of the given request as a point of reference.
|
||||
* The resulting path is also cleaned from sequences like "path/..".
|
||||
*
|
||||
* @param path the relative path to transform
|
||||
* @param request the referer request
|
||||
* @return the absolute request path for the given resource path
|
||||
*/
|
||||
protected String toAbsolutePath(String path, ServerHttpRequest request) {
|
||||
String requestPath = request.getURI().getPath();
|
||||
String absolutePath = StringUtils.applyRelativePath(requestPath, path);
|
||||
return StringUtils.cleanPath(absolutePath);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
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.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.event.ContextRefreshedEvent;
|
||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.util.PathMatcher;
|
||||
import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.support.HttpRequestPathHelper;
|
||||
|
||||
/**
|
||||
* A central component to use to obtain the public URL path that clients should
|
||||
* use to access a static resource.
|
||||
*
|
||||
* <p>This class is aware of Spring MVC handler mappings used to serve static
|
||||
* resources and uses the {@code ResourceResolver} chains of the configured
|
||||
* {@code ResourceHttpRequestHandler}s to make its decisions.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public class ResourceUrlProvider implements ApplicationListener<ContextRefreshedEvent> {
|
||||
|
||||
protected final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private HttpRequestPathHelper urlPathHelper = new HttpRequestPathHelper();
|
||||
|
||||
private PathMatcher pathMatcher = new AntPathMatcher();
|
||||
|
||||
private final Map<String, ResourceWebHandler> handlerMap = new LinkedHashMap<>();
|
||||
|
||||
private boolean autodetect = true;
|
||||
|
||||
|
||||
/**
|
||||
* Configure a {@code UrlPathHelper} to use in
|
||||
* {@link #getForRequestUrl(ServerWebExchange, String)}
|
||||
* in order to derive the lookup path for a target request URL path.
|
||||
*/
|
||||
public void setUrlPathHelper(HttpRequestPathHelper urlPathHelper) {
|
||||
this.urlPathHelper = urlPathHelper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the configured {@code UrlPathHelper}.
|
||||
*/
|
||||
public HttpRequestPathHelper getUrlPathHelper() {
|
||||
return this.urlPathHelper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a {@code PathMatcher} to use when comparing target lookup path
|
||||
* against resource mappings.
|
||||
*/
|
||||
public void setPathMatcher(PathMatcher pathMatcher) {
|
||||
this.pathMatcher = pathMatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the configured {@code PathMatcher}.
|
||||
*/
|
||||
public PathMatcher getPathMatcher() {
|
||||
return this.pathMatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually configure the resource mappings.
|
||||
* <p><strong>Note:</strong> by default resource mappings are auto-detected
|
||||
* from the Spring {@code ApplicationContext}. However if this property is
|
||||
* used, the auto-detection is turned off.
|
||||
*/
|
||||
public void setHandlerMap(Map<String, ResourceWebHandler> handlerMap) {
|
||||
if (handlerMap != null) {
|
||||
this.handlerMap.clear();
|
||||
this.handlerMap.putAll(handlerMap);
|
||||
this.autodetect = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the resource mappings, either manually configured or auto-detected
|
||||
* when the Spring {@code ApplicationContext} is refreshed.
|
||||
*/
|
||||
public Map<String, ResourceWebHandler> getHandlerMap() {
|
||||
return this.handlerMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return {@code false} if resource mappings were manually configured,
|
||||
* {@code true} otherwise.
|
||||
*/
|
||||
public boolean isAutodetect() {
|
||||
return this.autodetect;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(ContextRefreshedEvent event) {
|
||||
if (isAutodetect()) {
|
||||
this.handlerMap.clear();
|
||||
detectResourceHandlers(event.getApplicationContext());
|
||||
if (this.handlerMap.isEmpty() && logger.isDebugEnabled()) {
|
||||
logger.debug("No resource handling mappings found");
|
||||
}
|
||||
if (!this.handlerMap.isEmpty()) {
|
||||
this.autodetect = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected void detectResourceHandlers(ApplicationContext appContext) {
|
||||
logger.debug("Looking for resource handler mappings");
|
||||
|
||||
Map<String, SimpleUrlHandlerMapping> map = appContext.getBeansOfType(SimpleUrlHandlerMapping.class);
|
||||
List<SimpleUrlHandlerMapping> handlerMappings = new ArrayList<>(map.values());
|
||||
AnnotationAwareOrderComparator.sort(handlerMappings);
|
||||
|
||||
for (SimpleUrlHandlerMapping hm : handlerMappings) {
|
||||
for (String pattern : hm.getHandlerMap().keySet()) {
|
||||
Object handler = hm.getHandlerMap().get(pattern);
|
||||
if (handler instanceof ResourceWebHandler) {
|
||||
ResourceWebHandler resourceHandler = (ResourceWebHandler) handler;
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Found resource handler mapping: URL pattern=\"" + pattern + "\", " +
|
||||
"locations=" + resourceHandler.getLocations() + ", " +
|
||||
"resolvers=" + resourceHandler.getResourceResolvers());
|
||||
}
|
||||
this.handlerMap.put(pattern, resourceHandler);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A variation on {@link #getForLookupPath(String)} that accepts a full request
|
||||
* URL path and returns the full request URL path to expose for public use.
|
||||
* @param exchange the current exchange
|
||||
* @param requestUrl the request URL path to resolve
|
||||
* @return the resolved public URL path, or {@code null} if unresolved
|
||||
*/
|
||||
public final Mono<String> getForRequestUrl(ServerWebExchange exchange, String requestUrl) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Getting resource URL for request URL \"" + requestUrl + "\"");
|
||||
}
|
||||
int prefixIndex = getLookupPathIndex(exchange);
|
||||
int suffixIndex = getEndPathIndex(requestUrl);
|
||||
String prefix = requestUrl.substring(0, prefixIndex);
|
||||
String suffix = requestUrl.substring(suffixIndex);
|
||||
String lookupPath = requestUrl.substring(prefixIndex, suffixIndex);
|
||||
return getForLookupPath(lookupPath).map(resolvedPath -> prefix + resolvedPath + suffix);
|
||||
}
|
||||
|
||||
private int getLookupPathIndex(ServerWebExchange exchange) {
|
||||
ServerHttpRequest request = exchange.getRequest();
|
||||
String requestPath = request.getURI().getPath();
|
||||
String lookupPath = getUrlPathHelper().getLookupPathForRequest(exchange);
|
||||
return requestPath.indexOf(lookupPath);
|
||||
}
|
||||
|
||||
private int getEndPathIndex(String lookupPath) {
|
||||
int suffixIndex = lookupPath.length();
|
||||
int queryIndex = lookupPath.indexOf("?");
|
||||
if(queryIndex > 0) {
|
||||
suffixIndex = queryIndex;
|
||||
}
|
||||
int hashIndex = lookupPath.indexOf("#");
|
||||
if(hashIndex > 0) {
|
||||
suffixIndex = Math.min(suffixIndex, hashIndex);
|
||||
}
|
||||
return suffixIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare the given path against configured resource handler mappings and
|
||||
* if a match is found use the {@code ResourceResolver} chain of the matched
|
||||
* {@code ResourceHttpRequestHandler} to resolve the URL path to expose for
|
||||
* public use.
|
||||
* <p>It is expected that the given path is what Spring uses for
|
||||
* request mapping purposes.
|
||||
* <p>If several handler mappings match, the handler used will be the one
|
||||
* configured with the most specific pattern.
|
||||
* @param lookupPath the lookup path to check
|
||||
* @return the resolved public URL path, or {@code null} if unresolved
|
||||
*/
|
||||
public final Mono<String> getForLookupPath(String lookupPath) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Getting resource URL for lookup path \"" + lookupPath + "\"");
|
||||
}
|
||||
|
||||
List<String> matchingPatterns = new ArrayList<>();
|
||||
for (String pattern : this.handlerMap.keySet()) {
|
||||
if (getPathMatcher().match(pattern, lookupPath)) {
|
||||
matchingPatterns.add(pattern);
|
||||
}
|
||||
}
|
||||
|
||||
if (matchingPatterns.isEmpty()) {
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
Comparator<String> patternComparator = getPathMatcher().getPatternComparator(lookupPath);
|
||||
Collections.sort(matchingPatterns, patternComparator);
|
||||
|
||||
return Flux.fromIterable(matchingPatterns)
|
||||
.concatMap(pattern -> {
|
||||
String pathWithinMapping = getPathMatcher().extractPathWithinPattern(pattern, lookupPath);
|
||||
String pathMapping = lookupPath.substring(0, lookupPath.indexOf(pathWithinMapping));
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Invoking ResourceResolverChain for URL pattern \"" + pattern + "\"");
|
||||
}
|
||||
ResourceWebHandler handler = this.handlerMap.get(pattern);
|
||||
ResourceResolverChain chain = new DefaultResourceResolverChain(handler.getResourceResolvers());
|
||||
return chain.resolveUrlPath(pathWithinMapping, handler.getLocations())
|
||||
.map(resolvedPath -> {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Resolved public resource URL path \"" + resolvedPath + "\"");
|
||||
}
|
||||
return pathMapping + resolvedPath;
|
||||
});
|
||||
})
|
||||
.next();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,514 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLDecoder;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import reactor.core.Exceptions;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.SmartInitializingSingleton;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.CacheControl;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.ResourceHttpMessageWriter;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.ResourceUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.reactive.HandlerMapping;
|
||||
import org.springframework.web.reactive.accept.CompositeContentTypeResolver;
|
||||
import org.springframework.web.reactive.accept.PathExtensionContentTypeResolver;
|
||||
import org.springframework.web.server.MethodNotAllowedException;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebHandler;
|
||||
|
||||
/**
|
||||
* {@code HttpRequestHandler} that serves static resources in an optimized way
|
||||
* according to the guidelines of Page Speed, YSlow, etc.
|
||||
*
|
||||
* <p>The {@linkplain #setLocations "locations"} property takes a list of Spring
|
||||
* {@link Resource} locations from which static resources are allowed to
|
||||
* be served by this handler. Resources could be served from a classpath location,
|
||||
* e.g. "classpath:/META-INF/public-web-resources/", allowing convenient packaging
|
||||
* and serving of resources such as .js, .css, and others in jar files.
|
||||
*
|
||||
* <p>This request handler may also be configured with a
|
||||
* {@link #setResourceResolvers(List) resourcesResolver} and
|
||||
* {@link #setResourceTransformers(List) resourceTransformer} chains to support
|
||||
* arbitrary resolution and transformation of resources being served. By default a
|
||||
* {@link PathResourceResolver} simply finds resources based on the configured
|
||||
* "locations". An application can configure additional resolvers and
|
||||
* transformers such as the {@link VersionResourceResolver} which can resolve
|
||||
* and prepare URLs for resources with a version in the URL.
|
||||
*
|
||||
* <p>This handler also properly evaluates the {@code Last-Modified} header (if
|
||||
* present) so that a {@code 304} status code will be returned as appropriate,
|
||||
* avoiding unnecessary overhead for resources that are already cached by the
|
||||
* client.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
* @since 5.0
|
||||
*/
|
||||
public class ResourceWebHandler
|
||||
implements WebHandler, InitializingBean, SmartInitializingSingleton {
|
||||
|
||||
/** Set of supported HTTP methods */
|
||||
private static final Set<String> SUPPORTED_METHODS = new LinkedHashSet<>(2);
|
||||
|
||||
private static final Log logger = LogFactory.getLog(ResourceWebHandler.class);
|
||||
|
||||
static {
|
||||
SUPPORTED_METHODS.addAll(Arrays.asList("GET", "HEAD"));
|
||||
}
|
||||
|
||||
|
||||
private final List<Resource> locations = new ArrayList<>(4);
|
||||
|
||||
private final List<ResourceResolver> resourceResolvers = new ArrayList<>(4);
|
||||
|
||||
private final List<ResourceTransformer> resourceTransformers = new ArrayList<>(4);
|
||||
|
||||
private CacheControl cacheControl;
|
||||
|
||||
private ResourceHttpMessageWriter resourceHttpMessageWriter;
|
||||
|
||||
private CompositeContentTypeResolver contentTypeResolver;
|
||||
|
||||
private PathExtensionContentTypeResolver pathExtensionResolver;
|
||||
|
||||
|
||||
/**
|
||||
* Set the {@code List} of {@code Resource} paths to use as sources
|
||||
* for serving static resources.
|
||||
*/
|
||||
public void setLocations(List<Resource> locations) {
|
||||
Assert.notNull(locations, "Locations list must not be null");
|
||||
this.locations.clear();
|
||||
this.locations.addAll(locations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the {@code List} of {@code Resource} paths to use as sources
|
||||
* for serving static resources.
|
||||
*/
|
||||
public List<Resource> getLocations() {
|
||||
return this.locations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the list of {@link ResourceResolver}s to use.
|
||||
* <p>By default {@link PathResourceResolver} is configured. If using this property,
|
||||
* it is recommended to add {@link PathResourceResolver} as the last resolver.
|
||||
*/
|
||||
public void setResourceResolvers(List<ResourceResolver> resourceResolvers) {
|
||||
this.resourceResolvers.clear();
|
||||
if (resourceResolvers != null) {
|
||||
this.resourceResolvers.addAll(resourceResolvers);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of configured resource resolvers.
|
||||
*/
|
||||
public List<ResourceResolver> getResourceResolvers() {
|
||||
return this.resourceResolvers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the list of {@link ResourceTransformer}s to use.
|
||||
* <p>By default no transformers are configured for use.
|
||||
*/
|
||||
public void setResourceTransformers(List<ResourceTransformer> resourceTransformers) {
|
||||
this.resourceTransformers.clear();
|
||||
if (resourceTransformers != null) {
|
||||
this.resourceTransformers.addAll(resourceTransformers);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of configured resource transformers.
|
||||
*/
|
||||
public List<ResourceTransformer> getResourceTransformers() {
|
||||
return this.resourceTransformers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link org.springframework.http.CacheControl} instance to build
|
||||
* the Cache-Control HTTP response header.
|
||||
*/
|
||||
public void setCacheControl(CacheControl cacheControl) {
|
||||
this.cacheControl = cacheControl;
|
||||
}
|
||||
|
||||
public CacheControl getCacheControl() {
|
||||
return this.cacheControl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the {@link ResourceHttpMessageWriter} to use.
|
||||
* <p>By default a {@link ResourceHttpMessageWriter} will be configured.
|
||||
*/
|
||||
public void setResourceHttpMessageWriter(ResourceHttpMessageWriter httpMessageWriter) {
|
||||
this.resourceHttpMessageWriter = httpMessageWriter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the configured resource message writer.
|
||||
*/
|
||||
public ResourceHttpMessageWriter getResourceHttpMessageWriter() {
|
||||
return this.resourceHttpMessageWriter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a {@link CompositeContentTypeResolver} to help determine the
|
||||
* media types for resources being served. If the manager contains a path
|
||||
* extension resolver it will be checked for registered file extension.
|
||||
* @param contentTypeResolver the resolver in use
|
||||
*/
|
||||
public void setContentTypeResolver(CompositeContentTypeResolver contentTypeResolver) {
|
||||
this.contentTypeResolver = contentTypeResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the configured {@link CompositeContentTypeResolver}.
|
||||
*/
|
||||
public CompositeContentTypeResolver getContentTypeResolver() {
|
||||
return this.contentTypeResolver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
if (logger.isWarnEnabled() && CollectionUtils.isEmpty(this.locations)) {
|
||||
logger.warn("Locations list is empty. No resources will be served unless a " +
|
||||
"custom ResourceResolver is configured as an alternative to PathResourceResolver.");
|
||||
}
|
||||
if (this.resourceResolvers.isEmpty()) {
|
||||
this.resourceResolvers.add(new PathResourceResolver());
|
||||
}
|
||||
initAllowedLocations();
|
||||
if (this.resourceHttpMessageWriter == null) {
|
||||
this.resourceHttpMessageWriter = new ResourceHttpMessageWriter();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for a {@code PathResourceResolver} among the configured resource
|
||||
* resolvers and set its {@code allowedLocations} property (if empty) to
|
||||
* match the {@link #setLocations locations} configured on this class.
|
||||
*/
|
||||
protected void initAllowedLocations() {
|
||||
if (CollectionUtils.isEmpty(this.locations)) {
|
||||
return;
|
||||
}
|
||||
for (int i = getResourceResolvers().size() - 1; i >= 0; i--) {
|
||||
if (getResourceResolvers().get(i) instanceof PathResourceResolver) {
|
||||
PathResourceResolver resolver = (PathResourceResolver) getResourceResolvers().get(i);
|
||||
if (ObjectUtils.isEmpty(resolver.getAllowedLocations())) {
|
||||
resolver.setAllowedLocations(getLocations().toArray(new Resource[getLocations().size()]));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterSingletonsInstantiated() {
|
||||
this.pathExtensionResolver = initContentNegotiationStrategy();
|
||||
}
|
||||
|
||||
protected PathExtensionContentTypeResolver initContentNegotiationStrategy() {
|
||||
Map<String, MediaType> mediaTypes = null;
|
||||
if (getContentTypeResolver() != null) {
|
||||
PathExtensionContentTypeResolver strategy =
|
||||
getContentTypeResolver().findResolver(PathExtensionContentTypeResolver.class);
|
||||
if (strategy != null) {
|
||||
mediaTypes = new HashMap<>(strategy.getMediaTypes());
|
||||
}
|
||||
}
|
||||
return new PathExtensionContentTypeResolver(mediaTypes);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Processes a resource request.
|
||||
* <p>Checks for the existence of the requested resource in the configured list of locations.
|
||||
* If the resource does not exist, a {@code 404} response will be returned to the client.
|
||||
* If the resource exists, the request will be checked for the presence of the
|
||||
* {@code Last-Modified} header, and its value will be compared against the last-modified
|
||||
* timestamp of the given resource, returning a {@code 304} status code if the
|
||||
* {@code Last-Modified} value is greater. If the resource is newer than the
|
||||
* {@code Last-Modified} value, or the header is not present, the content resource
|
||||
* of the resource will be written to the response with caching headers
|
||||
* set to expire one year in the future.
|
||||
*/
|
||||
@Override
|
||||
public Mono<Void> handle(ServerWebExchange exchange) {
|
||||
|
||||
return getResource(exchange)
|
||||
.otherwiseIfEmpty(Mono.defer(() -> {
|
||||
logger.trace("No matching resource found - returning 404");
|
||||
exchange.getResponse().setStatusCode(HttpStatus.NOT_FOUND);
|
||||
return Mono.empty();
|
||||
}))
|
||||
.then(resource -> {
|
||||
try {
|
||||
if (HttpMethod.OPTIONS.equals(exchange.getRequest().getMethod())) {
|
||||
exchange.getResponse().getHeaders().add("Allow", "GET,HEAD,OPTIONS");
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
// Supported methods and required session
|
||||
String httpMehtod = exchange.getRequest().getMethod().name();
|
||||
if (!SUPPORTED_METHODS.contains(httpMehtod)) {
|
||||
return Mono.error(new MethodNotAllowedException(httpMehtod, SUPPORTED_METHODS));
|
||||
}
|
||||
|
||||
// Header phase
|
||||
if (exchange.checkNotModified(Instant.ofEpochMilli(resource.lastModified()))) {
|
||||
logger.trace("Resource not modified - returning 304");
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
// Apply cache settings, if any
|
||||
if (getCacheControl() != null) {
|
||||
String value = getCacheControl().getHeaderValue();
|
||||
if (value != null) {
|
||||
exchange.getResponse().getHeaders().setCacheControl(value);
|
||||
}
|
||||
}
|
||||
|
||||
// Check the media type for the resource
|
||||
MediaType mediaType = getMediaType(exchange, resource);
|
||||
if (mediaType != null) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Determined media type '" + mediaType + "' for " + resource);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("No media type found " +
|
||||
"for " + resource + " - not sending a content-type header");
|
||||
}
|
||||
}
|
||||
|
||||
// Content phase
|
||||
if (HttpMethod.HEAD.equals(exchange.getRequest().getMethod())) {
|
||||
setHeaders(exchange, resource, mediaType);
|
||||
exchange.getResponse().getHeaders().set(HttpHeaders.ACCEPT_RANGES, "bytes");
|
||||
logger.trace("HEAD request - skipping content");
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
setHeaders(exchange, resource, mediaType);
|
||||
return this.resourceHttpMessageWriter.write(Mono.just(resource),
|
||||
null, ResolvableType.forClass(Resource.class), mediaType,
|
||||
exchange.getRequest(), exchange.getResponse(), Collections.emptyMap());
|
||||
}
|
||||
catch (IOException ex) {
|
||||
return Mono.error(ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected Mono<Resource> getResource(ServerWebExchange exchange) {
|
||||
|
||||
String attributeName = HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE;
|
||||
Optional<String> optional = exchange.getAttribute(attributeName);
|
||||
if (!optional.isPresent()) {
|
||||
return Mono.error(new IllegalStateException(
|
||||
"Required request attribute '" + attributeName + "' is not set"));
|
||||
}
|
||||
|
||||
String path = processPath(optional.get());
|
||||
if (!StringUtils.hasText(path) || isInvalidPath(path)) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Ignoring invalid resource path [" + path + "]");
|
||||
}
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
if (path.contains("%")) {
|
||||
try {
|
||||
// Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars
|
||||
if (isInvalidPath(URLDecoder.decode(path, "UTF-8"))) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Ignoring invalid resource path with escape sequences [" + path + "].");
|
||||
}
|
||||
return Mono.empty();
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
// ignore
|
||||
}
|
||||
catch (UnsupportedEncodingException ex) {
|
||||
return Mono.error(Exceptions.propagate(ex));
|
||||
}
|
||||
}
|
||||
|
||||
ResourceResolverChain resolveChain = createResolverChain();
|
||||
return resolveChain.resolveResource(exchange, path, getLocations())
|
||||
.then(resource -> {
|
||||
ResourceTransformerChain transformerChain = createTransformerChain(resolveChain);
|
||||
return transformerChain.transform(exchange, resource);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the given resource path to be used.
|
||||
* <p>The default implementation replaces any combination of leading '/' and
|
||||
* control characters (00-1F and 7F) with a single "/" or "". For example
|
||||
* {@code " // /// //// foo/bar"} becomes {@code "/foo/bar"}.
|
||||
*/
|
||||
protected String processPath(String path) {
|
||||
boolean slash = false;
|
||||
for (int i = 0; i < path.length(); i++) {
|
||||
if (path.charAt(i) == '/') {
|
||||
slash = true;
|
||||
}
|
||||
else if (path.charAt(i) > ' ' && path.charAt(i) != 127) {
|
||||
if (i == 0 || (i == 1 && slash)) {
|
||||
return path;
|
||||
}
|
||||
path = slash ? "/" + path.substring(i) : path.substring(i);
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Path after trimming leading '/' and control characters: " + path);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
}
|
||||
return (slash ? "/" : "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies invalid resource paths. By default rejects:
|
||||
* <ul>
|
||||
* <li>Paths that contain "WEB-INF" or "META-INF"
|
||||
* <li>Paths that contain "../" after a call to
|
||||
* {@link StringUtils#cleanPath}.
|
||||
* <li>Paths that represent a {@link ResourceUtils#isUrl
|
||||
* valid URL} or would represent one after the leading slash is removed.
|
||||
* </ul>
|
||||
* <p><strong>Note:</strong> this method assumes that leading, duplicate '/'
|
||||
* or control characters (e.g. white space) have been trimmed so that the
|
||||
* path starts predictably with a single '/' or does not have one.
|
||||
* @param path the path to validate
|
||||
* @return {@code true} if the path is invalid, {@code false} otherwise
|
||||
*/
|
||||
protected boolean isInvalidPath(String path) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Applying \"invalid path\" checks to path: " + path);
|
||||
}
|
||||
if (path.contains("WEB-INF") || path.contains("META-INF")) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Path contains \"WEB-INF\" or \"META-INF\".");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (path.contains(":/")) {
|
||||
String relativePath = (path.charAt(0) == '/' ? path.substring(1) : path);
|
||||
if (ResourceUtils.isUrl(relativePath) || relativePath.startsWith("url:")) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Path represents URL or has \"url:\" prefix.");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (path.contains("..")) {
|
||||
path = StringUtils.cleanPath(path);
|
||||
if (path.contains("../")) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Path contains \"../\" after call to StringUtils#cleanPath.");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private ResourceResolverChain createResolverChain() {
|
||||
return new DefaultResourceResolverChain(getResourceResolvers());
|
||||
}
|
||||
|
||||
private ResourceTransformerChain createTransformerChain(ResourceResolverChain resolverChain) {
|
||||
return new DefaultResourceTransformerChain(resolverChain, getResourceTransformers());
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the media type for the given request and the resource matched
|
||||
* to it. This implementation tries to determine the MediaType based on the
|
||||
* file extension of the Resource via
|
||||
* {@link PathExtensionContentTypeResolver#resolveMediaTypeForResource(Resource)}.
|
||||
* @param exchange the current exchange
|
||||
* @param resource the resource to check
|
||||
* @return the corresponding media type, or {@code null} if none found
|
||||
*/
|
||||
protected MediaType getMediaType(ServerWebExchange exchange, Resource resource) {
|
||||
return this.pathExtensionResolver.resolveMediaTypeForResource(resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set headers on the response. Called for both GET and HEAD requests.
|
||||
* @param exchange current exchange
|
||||
* @param resource the identified resource (never {@code null})
|
||||
* @param mediaType the resource's media type (never {@code null})
|
||||
*/
|
||||
protected void setHeaders(ServerWebExchange exchange, Resource resource, MediaType mediaType)
|
||||
throws IOException {
|
||||
|
||||
HttpHeaders headers = exchange.getResponse().getHeaders();
|
||||
|
||||
long length = resource.contentLength();
|
||||
headers.setContentLength(length);
|
||||
|
||||
if (mediaType != null) {
|
||||
headers.setContentType(mediaType);
|
||||
}
|
||||
if (resource instanceof HttpResource) {
|
||||
HttpHeaders resourceHeaders = ((HttpResource) resource).getResponseHeaders();
|
||||
exchange.getResponse().getHeaders().putAll(resourceHeaders);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ResourceWebHandler [locations=" + getLocations() + ", resolvers=" + getResourceResolvers() + "]";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
|
||||
/**
|
||||
* An extension of {@link ByteArrayResource}
|
||||
* that a {@link ResourceTransformer} can use to represent an original
|
||||
* resource preserving all other information except the content.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public class TransformedResource extends ByteArrayResource {
|
||||
|
||||
private final String filename;
|
||||
|
||||
private final long lastModified;
|
||||
|
||||
|
||||
public TransformedResource(Resource original, byte[] transformedContent) {
|
||||
super(transformedContent);
|
||||
this.filename = original.getFilename();
|
||||
try {
|
||||
this.lastModified = original.lastModified();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
// should never happen
|
||||
throw new IllegalArgumentException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return this.filename;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long lastModified() {
|
||||
return this.lastModified;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
/**
|
||||
* A strategy for extracting and embedding a resource version in its URL path.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
* @since 5.0
|
||||
*/
|
||||
public interface VersionPathStrategy {
|
||||
|
||||
/**
|
||||
* Extract the resource version from the request path.
|
||||
* @param requestPath the request path to check
|
||||
* @return the version string or {@code null} if none was found
|
||||
*/
|
||||
String extractVersion(String requestPath);
|
||||
|
||||
/**
|
||||
* Remove the version from the request path. It is assumed that the given
|
||||
* version was extracted via {@link #extractVersion(String)}.
|
||||
* @param requestPath the request path of the resource being resolved
|
||||
* @param version the version obtained from {@link #extractVersion(String)}
|
||||
* @return the request path with the version removed
|
||||
*/
|
||||
String removeVersion(String requestPath, String version);
|
||||
|
||||
/**
|
||||
* Add a version to the given request path.
|
||||
* @param requestPath the requestPath
|
||||
* @param version the version
|
||||
* @return the requestPath updated with a version string
|
||||
*/
|
||||
String addVersion(String requestPath, String version);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.io.AbstractResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* Resolves request paths containing a version string that can be used as part
|
||||
* of an HTTP caching strategy in which a resource is cached with a date in the
|
||||
* distant future (e.g. 1 year) and cached until the version, and therefore the
|
||||
* URL, is changed.
|
||||
*
|
||||
* <p>Different versioning strategies exist, and this resolver must be configured
|
||||
* with one or more such strategies along with path mappings to indicate which
|
||||
* strategy applies to which resources.
|
||||
*
|
||||
* <p>{@code ContentVersionStrategy} is a good default choice except in cases
|
||||
* where it cannot be used. Most notably the {@code ContentVersionStrategy}
|
||||
* cannot be combined with JavaScript module loaders. For such cases the
|
||||
* {@code FixedVersionStrategy} is a better choice.
|
||||
*
|
||||
* <p>Note that using this resolver to serve CSS files means that the
|
||||
* {@link CssLinkResourceTransformer} should also be used in order to modify
|
||||
* links within CSS files to also contain the appropriate versions generated
|
||||
* by this resolver.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
* @since 5.0
|
||||
* @see VersionStrategy
|
||||
*/
|
||||
public class VersionResourceResolver extends AbstractResourceResolver {
|
||||
|
||||
private AntPathMatcher pathMatcher = new AntPathMatcher();
|
||||
|
||||
/** Map from path pattern -> VersionStrategy */
|
||||
private final Map<String, VersionStrategy> versionStrategyMap = new LinkedHashMap<>();
|
||||
|
||||
|
||||
/**
|
||||
* Set a Map with URL paths as keys and {@code VersionStrategy} as values.
|
||||
* <p>Supports direct URL matches and Ant-style pattern matches. For syntax
|
||||
* details, see the {@link AntPathMatcher} javadoc.
|
||||
* @param map map with URLs as keys and version strategies as values
|
||||
*/
|
||||
public void setStrategyMap(Map<String, VersionStrategy> map) {
|
||||
this.versionStrategyMap.clear();
|
||||
this.versionStrategyMap.putAll(map);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the map with version strategies keyed by path pattern.
|
||||
*/
|
||||
public Map<String, VersionStrategy> getStrategyMap() {
|
||||
return this.versionStrategyMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a content-based version in resource URLs that match the given path
|
||||
* patterns. The version is computed from the content of the file, e.g.
|
||||
* {@code "css/main-e36d2e05253c6c7085a91522ce43a0b4.css"}. This is a good
|
||||
* default strategy to use except when it cannot be, for example when using
|
||||
* JavaScript module loaders, use {@link #addFixedVersionStrategy} instead
|
||||
* for serving JavaScript files.
|
||||
* @param pathPatterns one or more resource URL path patterns,
|
||||
* relative to the pattern configured with the resource handler
|
||||
* @return the current instance for chained method invocation
|
||||
* @see ContentVersionStrategy
|
||||
*/
|
||||
public VersionResourceResolver addContentVersionStrategy(String... pathPatterns) {
|
||||
addVersionStrategy(new ContentVersionStrategy(), pathPatterns);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a fixed, prefix-based version in resource URLs that match the given
|
||||
* path patterns, for example: <code>"{version}/js/main.js"</code>. This is useful (vs.
|
||||
* content-based versions) when using JavaScript module loaders.
|
||||
* <p>The version may be a random number, the current date, or a value
|
||||
* fetched from a git commit sha, a property file, or environment variable
|
||||
* and set with SpEL expressions in the configuration (e.g. see {@code @Value}
|
||||
* in Java config).
|
||||
* <p>If not done already, variants of the given {@code pathPatterns}, prefixed with
|
||||
* the {@code version} will be also configured. For example, adding a {@code "/js/**"} path pattern
|
||||
* will also cofigure automatically a {@code "/v1.0.0/js/**"} with {@code "v1.0.0"} the
|
||||
* {@code version} String given as an argument.
|
||||
* @param version a version string
|
||||
* @param pathPatterns one or more resource URL path patterns,
|
||||
* relative to the pattern configured with the resource handler
|
||||
* @return the current instance for chained method invocation
|
||||
* @see FixedVersionStrategy
|
||||
*/
|
||||
public VersionResourceResolver addFixedVersionStrategy(String version, String... pathPatterns) {
|
||||
List<String> patternsList = Arrays.asList(pathPatterns);
|
||||
List<String> prefixedPatterns = new ArrayList<>(pathPatterns.length);
|
||||
String versionPrefix = "/" + version;
|
||||
for (String pattern : patternsList) {
|
||||
prefixedPatterns.add(pattern);
|
||||
if (!pattern.startsWith(versionPrefix) && !patternsList.contains(versionPrefix + pattern)) {
|
||||
prefixedPatterns.add(versionPrefix + pattern);
|
||||
}
|
||||
}
|
||||
return addVersionStrategy(new FixedVersionStrategy(version), prefixedPatterns.toArray(new String[0]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a custom VersionStrategy to apply to resource URLs that match the
|
||||
* given path patterns.
|
||||
* @param strategy the custom strategy
|
||||
* @param pathPatterns one or more resource URL path patterns,
|
||||
* relative to the pattern configured with the resource handler
|
||||
* @return the current instance for chained method invocation
|
||||
* @see VersionStrategy
|
||||
*/
|
||||
public VersionResourceResolver addVersionStrategy(VersionStrategy strategy, String... pathPatterns) {
|
||||
for (String pattern : pathPatterns) {
|
||||
getStrategyMap().put(pattern, strategy);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected Mono<Resource> resolveResourceInternal(ServerWebExchange exchange, String requestPath,
|
||||
List<? extends Resource> locations, ResourceResolverChain chain) {
|
||||
|
||||
return chain.resolveResource(exchange, requestPath, locations)
|
||||
.otherwiseIfEmpty(Mono.defer(() ->
|
||||
resolveVersionedResource(exchange, requestPath, locations, chain)));
|
||||
}
|
||||
|
||||
private Mono<Resource> resolveVersionedResource(ServerWebExchange exchange, String requestPath,
|
||||
List<? extends Resource> locations, ResourceResolverChain chain) {
|
||||
|
||||
VersionStrategy versionStrategy = getStrategyForPath(requestPath);
|
||||
if (versionStrategy == null) {
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
String candidateVersion = versionStrategy.extractVersion(requestPath);
|
||||
if (StringUtils.isEmpty(candidateVersion)) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("No version found in path \"" + requestPath + "\"");
|
||||
}
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
String simplePath = versionStrategy.removeVersion(requestPath, candidateVersion);
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Extracted version from path, re-resolving without version: \"" + simplePath + "\"");
|
||||
}
|
||||
|
||||
return chain.resolveResource(exchange, simplePath, locations)
|
||||
.then(baseResource -> {
|
||||
String actualVersion = versionStrategy.getResourceVersion(baseResource);
|
||||
if (candidateVersion.equals(actualVersion)) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Resource matches extracted version [" + candidateVersion + "]");
|
||||
}
|
||||
return Mono.just(new FileNameVersionedResource(baseResource, candidateVersion));
|
||||
}
|
||||
else {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Potential resource found for \"" + requestPath + "\", but version [" +
|
||||
candidateVersion + "] does not match");
|
||||
}
|
||||
return Mono.empty();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Mono<String> resolveUrlPathInternal(String resourceUrlPath,
|
||||
List<? extends Resource> locations, ResourceResolverChain chain) {
|
||||
|
||||
return chain.resolveUrlPath(resourceUrlPath, locations)
|
||||
.then(baseUrl -> {
|
||||
if (StringUtils.hasText(baseUrl)) {
|
||||
VersionStrategy versionStrategy = getStrategyForPath(resourceUrlPath);
|
||||
if (versionStrategy == null) {
|
||||
return Mono.empty();
|
||||
}
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Getting the original resource to determine version " +
|
||||
"for path \"" + resourceUrlPath + "\"");
|
||||
}
|
||||
return chain.resolveResource(null, baseUrl, locations)
|
||||
.map(resource -> {
|
||||
String version = versionStrategy.getResourceVersion(resource);
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Determined version [" + version + "] for " + resource);
|
||||
}
|
||||
return versionStrategy.addVersion(baseUrl, version);
|
||||
});
|
||||
}
|
||||
return Mono.empty();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a {@code VersionStrategy} for the request path of the requested resource.
|
||||
* @return an instance of a {@code VersionStrategy} or null if none matches that request path
|
||||
*/
|
||||
protected VersionStrategy getStrategyForPath(String requestPath) {
|
||||
String path = "/".concat(requestPath);
|
||||
List<String> matchingPatterns = new ArrayList<>();
|
||||
for (String pattern : this.versionStrategyMap.keySet()) {
|
||||
if (this.pathMatcher.match(pattern, path)) {
|
||||
matchingPatterns.add(pattern);
|
||||
}
|
||||
}
|
||||
if (!matchingPatterns.isEmpty()) {
|
||||
Comparator<String> comparator = this.pathMatcher.getPatternComparator(path);
|
||||
Collections.sort(matchingPatterns, comparator);
|
||||
return this.versionStrategyMap.get(matchingPatterns.get(0));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
private class FileNameVersionedResource extends AbstractResource implements HttpResource {
|
||||
|
||||
private final Resource original;
|
||||
|
||||
private final String version;
|
||||
|
||||
public FileNameVersionedResource(Resource original, String version) {
|
||||
this.original = original;
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists() {
|
||||
return this.original.exists();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReadable() {
|
||||
return this.original.isReadable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpen() {
|
||||
return this.original.isOpen();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFile() {
|
||||
return this.original.isFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public URL getURL() throws IOException {
|
||||
return this.original.getURL();
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI getURI() throws IOException {
|
||||
return this.original.getURI();
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getFile() throws IOException {
|
||||
return this.original.getFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return this.original.getFilename();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long contentLength() throws IOException {
|
||||
return this.original.contentLength();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long lastModified() throws IOException {
|
||||
return this.original.lastModified();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Resource createRelative(String relativePath) throws IOException {
|
||||
return this.original.createRelative(relativePath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return original.getDescription();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException {
|
||||
return original.getInputStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders getResponseHeaders() {
|
||||
HttpHeaders headers;
|
||||
if(this.original instanceof HttpResource) {
|
||||
headers = ((HttpResource) this.original).getResponseHeaders();
|
||||
}
|
||||
else {
|
||||
headers = new HttpHeaders();
|
||||
}
|
||||
headers.setETag("\"" + this.version + "\"");
|
||||
return headers;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
|
||||
/**
|
||||
* An extension of {@link VersionPathStrategy} that adds a method
|
||||
* to determine the actual version of a {@link Resource}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
* @since 5.0
|
||||
* @see VersionResourceResolver
|
||||
*/
|
||||
public interface VersionStrategy extends VersionPathStrategy {
|
||||
|
||||
/**
|
||||
* Determine the version for the given resource.
|
||||
* @param resource the resource to check
|
||||
* @return the version (never {@code null})
|
||||
*/
|
||||
String getResourceVersion(Resource resource);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.webjars.MultipleMatchesException;
|
||||
import org.webjars.WebJarAssetLocator;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* A {@code ResourceResolver} that delegates to the chain to locate a resource and then
|
||||
* attempts to find a matching versioned resource contained in a WebJar JAR file.
|
||||
*
|
||||
* <p>This allows WebJars.org users to write version agnostic paths in their templates,
|
||||
* like {@code <script src="/jquery/jquery.min.js"/>}.
|
||||
* This path will be resolved to the unique version {@code <script src="/jquery/1.2.0/jquery.min.js"/>},
|
||||
* which is a better fit for HTTP caching and version management in applications.
|
||||
*
|
||||
* <p>This also resolves resources for version agnostic HTTP requests {@code "GET /jquery/jquery.min.js"}.
|
||||
*
|
||||
* <p>This resolver requires the "org.webjars:webjars-locator" library on classpath,
|
||||
* and is automatically registered if that library is present.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
* @since 5.0
|
||||
* @see <a href="http://www.webjars.org">webjars.org</a>
|
||||
*/
|
||||
public class WebJarsResourceResolver extends AbstractResourceResolver {
|
||||
|
||||
private final static String WEBJARS_LOCATION = "META-INF/resources/webjars/";
|
||||
|
||||
private final static int WEBJARS_LOCATION_LENGTH = WEBJARS_LOCATION.length();
|
||||
|
||||
|
||||
private final WebJarAssetLocator webJarAssetLocator;
|
||||
|
||||
|
||||
/**
|
||||
* Create a {@code WebJarsResourceResolver} with a default {@code WebJarAssetLocator} instance.
|
||||
*/
|
||||
public WebJarsResourceResolver() {
|
||||
this(new WebJarAssetLocator());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@code WebJarsResourceResolver} with a custom {@code WebJarAssetLocator} instance,
|
||||
* e.g. with a custom index.
|
||||
*/
|
||||
public WebJarsResourceResolver(WebJarAssetLocator webJarAssetLocator) {
|
||||
this.webJarAssetLocator = webJarAssetLocator;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected Mono<Resource> resolveResourceInternal(ServerWebExchange exchange, String requestPath,
|
||||
List<? extends Resource> locations, ResourceResolverChain chain) {
|
||||
|
||||
return chain.resolveResource(exchange, requestPath, locations)
|
||||
.otherwiseIfEmpty(Mono.defer(() -> {
|
||||
String webJarsResourcePath = findWebJarResourcePath(requestPath);
|
||||
if (webJarsResourcePath != null) {
|
||||
return chain.resolveResource(exchange, webJarsResourcePath, locations);
|
||||
}
|
||||
else {
|
||||
return Mono.empty();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Mono<String> resolveUrlPathInternal(String resourceUrlPath,
|
||||
List<? extends Resource> locations, ResourceResolverChain chain) {
|
||||
|
||||
return chain.resolveUrlPath(resourceUrlPath, locations)
|
||||
.otherwiseIfEmpty(Mono.defer(() -> {
|
||||
String webJarResourcePath = findWebJarResourcePath(resourceUrlPath);
|
||||
if (webJarResourcePath != null) {
|
||||
return chain.resolveUrlPath(webJarResourcePath, locations);
|
||||
}
|
||||
else {
|
||||
return Mono.empty();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
protected String findWebJarResourcePath(String path) {
|
||||
try {
|
||||
int startOffset = (path.startsWith("/") ? 1 : 0);
|
||||
int endOffset = path.indexOf("/", 1);
|
||||
if (endOffset != -1) {
|
||||
String webjar = path.substring(startOffset, endOffset);
|
||||
String partialPath = path.substring(endOffset);
|
||||
String webJarPath = webJarAssetLocator.getFullPath(webjar, partialPath);
|
||||
return webJarPath.substring(WEBJARS_LOCATION_LENGTH);
|
||||
}
|
||||
}
|
||||
catch (MultipleMatchesException ex) {
|
||||
if (logger.isWarnEnabled()) {
|
||||
logger.warn("WebJar version conflict for \"" + path + "\"", ex);
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("No WebJar resource found for \"" + path + "\"");
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user