Rename spring-web-reactive to spring-webflux

Issue: SPR-15190
This commit is contained in:
Rossen Stoyanchev
2017-02-01 17:02:52 -05:00
parent 81d1217976
commit fafd2d20e1
386 changed files with 22 additions and 25 deletions

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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));
}
}

View File

@@ -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);
}

View File

@@ -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());
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}
}

View File

@@ -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();
}

View File

@@ -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());
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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">
* &#064;Configuration
* &#064;EnableWebReactive
* &#064;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">
* &#064;Configuration
* &#064;EnableWebReactive
* &#064;ComponentScan(basePackageClasses = MyConfiguration.class)
* public class MyConfiguration implements WebReactiveConfigurer {
*
* &#064;Override
* public void addFormatters(FormatterRegistry formatterRegistry) {
* formatterRegistry.addConverter(new MyConverter());
* }
*
* &#064;Override
* public void configureMessageWriters(List&lt;HttpMessageWriter&lt;?&gt&gt 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 {
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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");
}
}
}

View File

@@ -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) {
}
}
}

View File

@@ -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) {
}
}

View File

@@ -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.");
}
}
}

View File

@@ -0,0 +1,4 @@
/**
* Defines Spring Web Reactive configuration.
*/
package org.springframework.web.reactive.config;

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
});
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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&lt;Void&gt; request = ClientRequest.method(HttpMethod.GET,
* "http://example.com/resource").build();
*
* Mono&lt;String&gt; 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);
}
}

View File

@@ -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));
}
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -0,0 +1,4 @@
/**
* Provides a foundation for both the reactive client and server subpackages.
*/
package org.springframework.web.reactive.function;

View File

@@ -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;
}
}
}

View File

@@ -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());
}
}
}

View File

@@ -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));
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}

View File

@@ -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&lt;Resource&gt; 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;
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,4 @@
/**
* Provides the types that make up Spring's functional web framework.
*/
package org.springframework.web.reactive.function.server;

View File

@@ -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));
}
}

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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() + "]");
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -0,0 +1,4 @@
/**
* Provides HandlerMapping implementations including abstract base classes.
*/
package org.springframework.web.reactive.handler;

View File

@@ -0,0 +1,4 @@
/**
* Core interfaces and classes for Spring Web Reactive.
*/
package org.springframework.web.reactive;

View File

@@ -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);
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
});
}
}

View File

@@ -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);
});
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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();
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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() + "]";
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}

View File

@@ -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