Split HttpMessageConverter into ~Reader and ~Writer

This commit is contained in:
Rossen Stoyanchev
2016-07-18 21:54:51 -04:00
parent dca80788d4
commit 4b92bf2af1
39 changed files with 712 additions and 561 deletions

View File

@@ -25,6 +25,7 @@ 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;
@@ -97,9 +98,9 @@ public class ViewResolverRegistry {
/**
* Set default views associated with any view name and selected based on the
* best match for the requested content type.
* <p>Use {@link org.springframework.web.reactive.result.view.HttpMessageConverterView
* HttpMessageConverterView} to adapt and use any existing
* {@code HttpMessageConverter} (e.g. JSON, XML) as a {@code View}.
* <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));

View File

@@ -28,8 +28,8 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.codec.ByteBufferDecoder;
import org.springframework.core.codec.ByteBufferEncoder;
import org.springframework.core.codec.Decoder;
import org.springframework.core.codec.Encoder;
import org.springframework.core.codec.ResourceDecoder;
import org.springframework.core.codec.StringDecoder;
import org.springframework.core.codec.StringEncoder;
import org.springframework.core.convert.converter.Converter;
@@ -45,9 +45,11 @@ import org.springframework.http.codec.json.JacksonJsonDecoder;
import org.springframework.http.codec.json.JacksonJsonEncoder;
import org.springframework.http.codec.xml.Jaxb2Decoder;
import org.springframework.http.codec.xml.Jaxb2Encoder;
import org.springframework.http.converter.reactive.CodecHttpMessageConverter;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.http.converter.reactive.ResourceHttpMessageConverter;
import org.springframework.http.converter.reactive.DecoderHttpMessageReader;
import org.springframework.http.converter.reactive.EncoderHttpMessageWriter;
import org.springframework.http.converter.reactive.HttpMessageReader;
import org.springframework.http.converter.reactive.HttpMessageWriter;
import org.springframework.http.converter.reactive.ResourceHttpMessageWriter;
import org.springframework.util.ClassUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
@@ -87,7 +89,9 @@ public class WebReactiveConfiguration implements ApplicationContextAware {
private PathMatchConfigurer pathMatchConfigurer;
private List<HttpMessageConverter<?>> messageConverters;
private List<HttpMessageReader<?>> messageReaders;
private List<HttpMessageWriter<?>> messageWriters;
private ApplicationContext applicationContext;
@@ -189,7 +193,7 @@ public class WebReactiveConfiguration implements ApplicationContextAware {
adapter.setCustomArgumentResolvers(resolvers);
}
adapter.setMessageConverters(getMessageConverters());
adapter.setMessageReaders(getMessageReaders());
adapter.setConversionService(mvcConversionService());
adapter.setValidator(mvcValidator());
@@ -210,65 +214,54 @@ public class WebReactiveConfiguration implements ApplicationContextAware {
}
/**
* Main method to access message converters to use for decoding
* controller method arguments and encoding return values.
* <p>Use {@link #configureMessageConverters} to configure the list or
* {@link #extendMessageConverters} to add in addition to the default ones.
* 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<HttpMessageConverter<?>> getMessageConverters() {
if (this.messageConverters == null) {
this.messageConverters = new ArrayList<>();
configureMessageConverters(this.messageConverters);
if (this.messageConverters.isEmpty()) {
addDefaultHttpMessageConverters(this.messageConverters);
protected final List<HttpMessageReader<?>> getMessageReaders() {
if (this.messageReaders == null) {
this.messageReaders = new ArrayList<>();
configureMessageReaders(this.messageReaders);
if (this.messageReaders.isEmpty()) {
addDefaultHttpMessageReaders(this.messageReaders);
}
extendMessageConverters(this.messageConverters);
extendMessageReaders(this.messageReaders);
}
return this.messageConverters;
return this.messageReaders;
}
/**
* Override to configure the message converters to use for decoding
* controller method arguments and encoding return values.
* <p>If no converters are specified, default will be added via
* {@link #addDefaultHttpMessageConverters}.
* @param converters a list to add converters to, initially an empty
* 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 configureMessageConverters(List<HttpMessageConverter<?>> converters) {
protected void configureMessageReaders(List<HttpMessageReader<?>> messageReaders) {
}
/**
* Adds default converters that sub-classes can call from
* {@link #configureMessageConverters(List)}.
* {@link #configureMessageReaders(List)}.
*/
protected final void addDefaultHttpMessageConverters(List<HttpMessageConverter<?>> converters) {
List<Encoder<?>> sseDataEncoders = new ArrayList<>();
converters.add(converter(new ByteBufferEncoder(), new ByteBufferDecoder()));
converters.add(converter(new StringEncoder(), new StringDecoder()));
converters.add(new ResourceHttpMessageConverter());
protected final void addDefaultHttpMessageReaders(List<HttpMessageReader<?>> readers) {
readers.add(new DecoderHttpMessageReader<>(new ByteBufferDecoder()));
readers.add(new DecoderHttpMessageReader<>(new StringDecoder()));
readers.add(new DecoderHttpMessageReader<>(new ResourceDecoder()));
if (jaxb2Present) {
converters.add(converter(new Jaxb2Encoder(), new Jaxb2Decoder()));
readers.add(new DecoderHttpMessageReader<>(new Jaxb2Decoder()));
}
if (jackson2Present) {
JacksonJsonEncoder jacksonEncoder = new JacksonJsonEncoder();
JacksonJsonDecoder jacksonDecoder = new JacksonJsonDecoder();
converters.add(converter(jacksonEncoder, jacksonDecoder));
sseDataEncoders.add(jacksonEncoder);
} else {
readers.add(new DecoderHttpMessageReader<>(new JacksonJsonDecoder()));
}
converters.add(converter(new SseEventEncoder(sseDataEncoders), null));
}
private static <T> HttpMessageConverter<T> converter(Encoder<T> encoder, Decoder<T> decoder) {
return new CodecHttpMessageConverter<>(encoder, decoder);
}
/**
* Override this to modify the list of converters after it has been
* 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 extendMessageConverters(List<HttpMessageConverter<?>> converters) {
protected void extendMessageReaders(List<HttpMessageReader<?>> messageReaders) {
}
@Bean
@@ -345,16 +338,67 @@ public class WebReactiveConfiguration implements ApplicationContextAware {
@Bean
public ResponseEntityResultHandler responseEntityResultHandler() {
return new ResponseEntityResultHandler(getMessageConverters(), mvcConversionService(),
return new ResponseEntityResultHandler(getMessageWriters(), mvcConversionService(),
mvcContentTypeResolver());
}
@Bean
public ResponseBodyResultHandler responseBodyResultHandler() {
return new ResponseBodyResultHandler(getMessageConverters(), mvcConversionService(),
return new ResponseBodyResultHandler(getMessageWriters(), mvcConversionService(),
mvcContentTypeResolver());
}
/**
* 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 ByteBufferEncoder()));
writers.add(new EncoderHttpMessageWriter<>(new StringEncoder()));
writers.add(new ResourceHttpMessageWriter());
if (jaxb2Present) {
writers.add(new EncoderHttpMessageWriter<>(new Jaxb2Encoder()));
}
if (jackson2Present) {
JacksonJsonEncoder jacksonEncoder = new JacksonJsonEncoder();
writers.add(new EncoderHttpMessageWriter<>(jacksonEncoder));
sseDataEncoders.add(jacksonEncoder);
}
writers.add(new EncoderHttpMessageWriter<>(new SseEventEncoder(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(this.applicationContext);

View File

@@ -30,7 +30,7 @@ import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.http.MediaType;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.http.converter.reactive.HttpMessageReader;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
@@ -45,7 +45,7 @@ import org.springframework.web.server.UnsupportedMediaTypeStatusException;
/**
* Abstract base class for argument resolvers that resolve method arguments
* by reading the request body with an {@link HttpMessageConverter}.
* by reading the request body with an {@link HttpMessageReader}.
*
* <p>Applies validation if the method argument is annotated with
* {@code @javax.validation.Valid} or
@@ -55,14 +55,14 @@ import org.springframework.web.server.UnsupportedMediaTypeStatusException;
* @author Rossen Stoyanchev
* @since 5.0
*/
public abstract class AbstractMessageConverterArgumentResolver {
public abstract class AbstractMessageReaderArgumentResolver {
private static final TypeDescriptor MONO_TYPE = TypeDescriptor.valueOf(Mono.class);
private static final TypeDescriptor FLUX_TYPE = TypeDescriptor.valueOf(Flux.class);
private final List<HttpMessageConverter<?>> messageConverters;
private final List<HttpMessageReader<?>> messageReaders;
private final ConversionService conversionService;
@@ -73,19 +73,19 @@ public abstract class AbstractMessageConverterArgumentResolver {
/**
* Constructor with message converters and a ConversionService.
* @param converters converters for reading the request body with
* @param messageReaders readers to convert from the request body
* @param service for converting to other reactive types from Flux and Mono
* @param validator validator to validate decoded objects with
*/
protected AbstractMessageConverterArgumentResolver(List<HttpMessageConverter<?>> converters,
protected AbstractMessageReaderArgumentResolver(List<HttpMessageReader<?>> messageReaders,
ConversionService service, Validator validator) {
Assert.notEmpty(converters, "At least one message converter is required.");
Assert.notEmpty(messageReaders, "At least one message reader is required.");
Assert.notNull(service, "'conversionService' is required.");
this.messageConverters = converters;
this.messageReaders = messageReaders;
this.conversionService = service;
this.validator = validator;
this.supportedMediaTypes = converters.stream()
this.supportedMediaTypes = messageReaders.stream()
.flatMap(converter -> converter.getReadableMediaTypes().stream())
.collect(Collectors.toList());
}
@@ -94,8 +94,8 @@ public abstract class AbstractMessageConverterArgumentResolver {
/**
* Return the configured message converters.
*/
public List<HttpMessageConverter<?>> getMessageConverters() {
return this.messageConverters;
public List<HttpMessageReader<?>> getMessageReaders() {
return this.messageReaders;
}
/**
@@ -124,10 +124,10 @@ public abstract class AbstractMessageConverterArgumentResolver {
mediaType = MediaType.APPLICATION_OCTET_STREAM;
}
for (HttpMessageConverter<?> converter : getMessageConverters()) {
if (converter.canRead(elementType, mediaType)) {
for (HttpMessageReader<?> reader : getMessageReaders()) {
if (reader.canRead(elementType, mediaType)) {
if (convertFromFlux) {
Flux<?> flux = converter.read(elementType, request)
Flux<?> flux = reader.read(elementType, request)
.onErrorResumeWith(ex -> Flux.error(getReadError(ex, bodyParameter)));
if (checkRequired(bodyParameter, isBodyRequired)) {
flux = flux.switchIfEmpty(Flux.error(getRequiredBodyError(bodyParameter)));
@@ -138,7 +138,7 @@ public abstract class AbstractMessageConverterArgumentResolver {
return Mono.just(getConversionService().convert(flux, FLUX_TYPE, typeDescriptor));
}
else {
Mono<?> mono = converter.readMono(elementType, request)
Mono<?> mono = reader.readMono(elementType, request)
.otherwise(ex -> Mono.error(getReadError(ex, bodyParameter)));
if (checkRequired(bodyParameter, isBodyRequired)) {
mono = mono.otherwiseIfEmpty(Mono.error(getRequiredBodyError(bodyParameter)));

View File

@@ -27,7 +27,7 @@ import org.springframework.core.ResolvableType;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.http.MediaType;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.http.converter.reactive.HttpMessageWriter;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.Assert;
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
@@ -37,43 +37,43 @@ import org.springframework.web.server.ServerWebExchange;
/**
* Abstract base class for result handlers that handle return values by writing
* to the response with {@link HttpMessageConverter}.
* to the response with {@link HttpMessageWriter}.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public abstract class AbstractMessageConverterResultHandler extends ContentNegotiatingResultHandlerSupport {
public abstract class AbstractMessageWriterResultHandler extends ContentNegotiatingResultHandlerSupport {
protected static final TypeDescriptor MONO_TYPE = TypeDescriptor.valueOf(Mono.class);
protected static final TypeDescriptor FLUX_TYPE = TypeDescriptor.valueOf(Flux.class);
private final List<HttpMessageConverter<?>> messageConverters;
private final List<HttpMessageWriter<?>> messageWriters;
/**
* Constructor with message converters, a {@code ConversionService}, and a
* {@code RequestedContentTypeResolver}.
*
* @param converters converters for writing the response body with
* @param messageWriters for serializing Objects to the response body stream
* @param conversionService for converting other reactive types (e.g.
* rx.Observable, rx.Single, etc.) to Flux or Mono
* @param contentTypeResolver for resolving the requested content type
*/
protected AbstractMessageConverterResultHandler(List<HttpMessageConverter<?>> converters,
protected AbstractMessageWriterResultHandler(List<HttpMessageWriter<?>> messageWriters,
ConversionService conversionService, RequestedContentTypeResolver contentTypeResolver) {
super(conversionService, contentTypeResolver);
Assert.notEmpty(converters, "At least one message converter is required.");
this.messageConverters = converters;
Assert.notEmpty(messageWriters, "At least one message writer is required.");
this.messageWriters = messageWriters;
}
/**
* Return the configured message converters.
*/
public List<HttpMessageConverter<?>> getMessageConverters() {
return this.messageConverters;
public List<HttpMessageWriter<?>> getMessageWriters() {
return this.messageWriters;
}
@@ -118,10 +118,10 @@ public abstract class AbstractMessageConverterResultHandler extends ContentNegot
MediaType bestMediaType = selectMediaType(exchange, producibleTypes);
if (bestMediaType != null) {
for (HttpMessageConverter<?> converter : getMessageConverters()) {
if (converter.canWrite(elementType, bestMediaType)) {
for (HttpMessageWriter<?> messageWriter : getMessageWriters()) {
if (messageWriter.canWrite(elementType, bestMediaType)) {
ServerHttpResponse response = exchange.getResponse();
return converter.write((Publisher) publisher, elementType, bestMediaType, response);
return messageWriter.write((Publisher) publisher, elementType, bestMediaType, response);
}
}
}
@@ -130,7 +130,7 @@ public abstract class AbstractMessageConverterResultHandler extends ContentNegot
}
private List<MediaType> getProducibleMediaTypes(ResolvableType elementType) {
return getMessageConverters().stream()
return getMessageWriters().stream()
.filter(converter -> converter.canWrite(elementType, null))
.flatMap(converter -> converter.getWritableMediaTypes().stream())
.collect(Collectors.toList());

View File

@@ -25,7 +25,7 @@ import org.springframework.core.convert.ConversionService;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.RequestEntity;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.http.converter.reactive.HttpMessageReader;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.ui.ModelMap;
import org.springframework.validation.Validator;
@@ -35,36 +35,36 @@ import org.springframework.web.server.ServerWebExchange;
/**
* Resolves method arguments of type {@link HttpEntity} or {@link RequestEntity}
* by reading the body of the request through a compatible
* {@code HttpMessageConverter}.
* {@code HttpMessageReader}.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public class HttpEntityArgumentResolver extends AbstractMessageConverterArgumentResolver
public class HttpEntityArgumentResolver extends AbstractMessageReaderArgumentResolver
implements HandlerMethodArgumentResolver {
/**
* Constructor with message converters and a ConversionService.
* @param converters converters for reading the request body with
* @param messageReaders readers for de-serializing the request body with
* @param service for converting to other reactive types from Flux and Mono
*/
public HttpEntityArgumentResolver(List<HttpMessageConverter<?>> converters,
public HttpEntityArgumentResolver(List<HttpMessageReader<?>> messageReaders,
ConversionService service) {
this(converters, service, null);
this(messageReaders, service, null);
}
/**
* Constructor with message converters and a ConversionService.
* @param converters converters for reading the request body with
* @param messageReaders readers for de-serializing the request body with
* @param service for converting to other reactive types from Flux and Mono
* @param validator validator to validate decoded objects with
*/
public HttpEntityArgumentResolver(List<HttpMessageConverter<?>> converters,
public HttpEntityArgumentResolver(List<HttpMessageReader<?>> messageReaders,
ConversionService service, Validator validator) {
super(converters, service, validator);
super(messageReaders, service, validator);
}

View File

@@ -22,7 +22,7 @@ import reactor.core.publisher.Mono;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.ConversionService;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.http.converter.reactive.HttpMessageReader;
import org.springframework.ui.ModelMap;
import org.springframework.validation.Validator;
import org.springframework.web.bind.annotation.RequestBody;
@@ -32,7 +32,7 @@ import org.springframework.web.server.ServerWebInputException;
/**
* Resolves method arguments annotated with {@code @RequestBody} by reading the
* body of the request through a compatible {@code HttpMessageConverter}.
* body of the request through a compatible {@code HttpMessageReader}.
*
* <p>An {@code @RequestBody} method argument is also validated if it is
* annotated with {@code @javax.validation.Valid} or
@@ -44,31 +44,31 @@ import org.springframework.web.server.ServerWebInputException;
* @author Rossen Stoyanchev
* @since 5.0
*/
public class RequestBodyArgumentResolver extends AbstractMessageConverterArgumentResolver
public class RequestBodyArgumentResolver extends AbstractMessageReaderArgumentResolver
implements HandlerMethodArgumentResolver {
/**
* Constructor with message converters and a ConversionService.
* @param converters converters for reading the request body with
* @param messageReaders readers for de-serializing the request body with
* @param service for converting to other reactive types from Flux and Mono
*/
public RequestBodyArgumentResolver(List<HttpMessageConverter<?>> converters,
public RequestBodyArgumentResolver(List<HttpMessageReader<?>> messageReaders,
ConversionService service) {
this(converters, service, null);
this(messageReaders, service, null);
}
/**
* Constructor with message converters and a ConversionService.
* @param converters converters for reading the request body with
* @param messageReaders readers for de-serializing the request body with
* @param service for converting to other reactive types from Flux and Mono
* @param validator validator to validate decoded objects with
*/
public RequestBodyArgumentResolver(List<HttpMessageConverter<?>> converters,
public RequestBodyArgumentResolver(List<HttpMessageReader<?>> messageReaders,
ConversionService service, Validator validator) {
super(converters, service, validator);
super(messageReaders, service, validator);
}

View File

@@ -35,8 +35,8 @@ import org.springframework.core.codec.ByteBufferDecoder;
import org.springframework.core.codec.StringDecoder;
import org.springframework.core.convert.ConversionService;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.http.converter.reactive.CodecHttpMessageConverter;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.http.converter.reactive.DecoderHttpMessageReader;
import org.springframework.http.converter.reactive.HttpMessageReader;
import org.springframework.ui.ExtendedModelMap;
import org.springframework.ui.ModelMap;
import org.springframework.validation.Validator;
@@ -64,7 +64,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory
private List<HandlerMethodArgumentResolver> argumentResolvers;
private final List<HttpMessageConverter<?>> messageConverters = new ArrayList<>(10);
private final List<HttpMessageReader<?>> messageReaders = new ArrayList<>(10);
private ConversionService conversionService = new DefaultFormattingConversionService();
@@ -77,8 +77,8 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory
public RequestMappingHandlerAdapter() {
this.messageConverters.add(new CodecHttpMessageConverter<>(new ByteBufferDecoder()));
this.messageConverters.add(new CodecHttpMessageConverter<>(new StringDecoder()));
this.messageReaders.add(new DecoderHttpMessageReader<>(new ByteBufferDecoder()));
this.messageReaders.add(new DecoderHttpMessageReader<>(new StringDecoder()));
}
@@ -112,18 +112,18 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory
}
/**
* Configure message converters to read the request body with.
* Configure message readers to de-serialize the request body with.
*/
public void setMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
this.messageConverters.clear();
this.messageConverters.addAll(messageConverters);
public void setMessageReaders(List<HttpMessageReader<?>> messageReaders) {
this.messageReaders.clear();
this.messageReaders.addAll(messageReaders);
}
/**
* Return the configured message converters.
* Return the configured message readers.
*/
public List<HttpMessageConverter<?>> getMessageConverters() {
return this.messageConverters;
public List<HttpMessageReader<?>> getMessageReaders() {
return this.messageReaders;
}
/**
@@ -193,7 +193,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory
resolvers.add(new RequestParamMapMethodArgumentResolver());
resolvers.add(new PathVariableMethodArgumentResolver(cs, getBeanFactory()));
resolvers.add(new PathVariableMapMethodArgumentResolver());
resolvers.add(new RequestBodyArgumentResolver(getMessageConverters(), cs, getValidator()));
resolvers.add(new RequestBodyArgumentResolver(getMessageReaders(), cs, getValidator()));
resolvers.add(new RequestHeaderMethodArgumentResolver(cs, getBeanFactory()));
resolvers.add(new RequestHeaderMapMethodArgumentResolver());
resolvers.add(new CookieValueMethodArgumentResolver(cs, getBeanFactory()));

View File

@@ -25,7 +25,7 @@ import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.convert.ConversionService;
import org.springframework.http.HttpEntity;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.http.converter.reactive.HttpMessageWriter;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.reactive.HandlerResult;
import org.springframework.web.reactive.HandlerResultHandler;
@@ -37,7 +37,7 @@ import org.springframework.web.server.ServerWebExchange;
/**
* {@code HandlerResultHandler} that handles return values from methods annotated
* with {@code @ResponseBody} writing to the body of the request or response with
* an {@link HttpMessageConverter}.
* an {@link HttpMessageWriter}.
*
* <p>By default the order for this result handler is set to 100. As it detects
* the presence of {@code @ResponseBody} it should be ordered after result
@@ -50,7 +50,7 @@ import org.springframework.web.server.ServerWebExchange;
* @author Arjen Poutsma
* @since 5.0
*/
public class ResponseBodyResultHandler extends AbstractMessageConverterResultHandler
public class ResponseBodyResultHandler extends AbstractMessageWriterResultHandler
implements HandlerResultHandler {
/**
@@ -58,28 +58,28 @@ public class ResponseBodyResultHandler extends AbstractMessageConverterResultHan
* and creating a {@link HeaderContentTypeResolver}, i.e. using Accept header
* to determine the requested content type.
*
* @param converters converters for writing the response body with
* @param messageWriters writers for serializing to the response body stream
* @param conversionService for converting to Flux and Mono from other reactive types
*/
public ResponseBodyResultHandler(List<HttpMessageConverter<?>> converters,
public ResponseBodyResultHandler(List<HttpMessageWriter<?>> messageWriters,
ConversionService conversionService) {
this(converters, conversionService, new HeaderContentTypeResolver());
this(messageWriters, conversionService, new HeaderContentTypeResolver());
}
/**
* Constructor with message converters, a {@code ConversionService}, and a
* {@code RequestedContentTypeResolver}.
*
* @param converters converters for writing the response body with
* @param messageWriters writers for serializing to the response body stream
* @param conversionService for converting other reactive types (e.g.
* rx.Observable, rx.Single, etc.) to Flux or Mono
* @param contentTypeResolver for resolving the requested content type
*/
public ResponseBodyResultHandler(List<HttpMessageConverter<?>> converters,
public ResponseBodyResultHandler(List<HttpMessageWriter<?>> messageWriters,
ConversionService conversionService, RequestedContentTypeResolver contentTypeResolver) {
super(converters, conversionService, contentTypeResolver);
super(messageWriters, conversionService, contentTypeResolver);
setOrder(100);
}

View File

@@ -27,7 +27,7 @@ import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.http.converter.reactive.HttpMessageWriter;
import org.springframework.util.Assert;
import org.springframework.web.reactive.HandlerResult;
import org.springframework.web.reactive.HandlerResultHandler;
@@ -44,7 +44,7 @@ import org.springframework.web.server.ServerWebExchange;
* @author Rossen Stoyanchev
* @since 5.0
*/
public class ResponseEntityResultHandler extends AbstractMessageConverterResultHandler
public class ResponseEntityResultHandler extends AbstractMessageWriterResultHandler
implements HandlerResultHandler {
/**
@@ -52,28 +52,28 @@ public class ResponseEntityResultHandler extends AbstractMessageConverterResultH
* and creating a {@link HeaderContentTypeResolver}, i.e. using Accept header
* to determine the requested content type.
*
* @param converters converters for writing the response body with
* @param messageWriters writers for serializing to the response body stream
* @param conversionService for converting to Flux and Mono from other reactive types
*/
public ResponseEntityResultHandler(List<HttpMessageConverter<?>> converters,
public ResponseEntityResultHandler(List<HttpMessageWriter<?>> messageWriters,
ConversionService conversionService) {
this(converters, conversionService, new HeaderContentTypeResolver());
this(messageWriters, conversionService, new HeaderContentTypeResolver());
}
/**
* Constructor with message converters, a {@code ConversionService}, and a
* {@code RequestedContentTypeResolver}.
*
* @param converters converters for writing the response body with
* @param messageWriters writers for serializing to the response body stream
* @param conversionService for converting other reactive types (e.g.
* rx.Observable, rx.Single, etc.) to Flux or Mono
* @param contentTypeResolver for resolving the requested content type
*/
public ResponseEntityResultHandler(List<HttpMessageConverter<?>> converters,
public ResponseEntityResultHandler(List<HttpMessageWriter<?>> messageWriters,
ConversionService conversionService, RequestedContentTypeResolver contentTypeResolver) {
super(converters, conversionService, contentTypeResolver);
super(messageWriters, conversionService, contentTypeResolver);
setOrder(0);
}

View File

@@ -27,8 +27,8 @@ import reactor.core.publisher.Mono;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.Encoder;
import org.springframework.http.MediaType;
import org.springframework.http.converter.reactive.CodecHttpMessageConverter;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.http.converter.reactive.EncoderHttpMessageWriter;
import org.springframework.http.converter.reactive.HttpMessageWriter;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.ui.ModelMap;
import org.springframework.util.Assert;
@@ -37,14 +37,14 @@ import org.springframework.web.server.ServerWebExchange;
/**
* A {@link View} that delegates to an {@link HttpMessageConverter}.
* A {@link View} that delegates to an {@link HttpMessageWriter}.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public class HttpMessageConverterView implements View {
public class HttpMessageWriterView implements View {
private final HttpMessageConverter<?> converter;
private final HttpMessageWriter<?> messageWriter;
private final Set<String> modelKeys = new HashSet<>(4);
@@ -52,35 +52,33 @@ public class HttpMessageConverterView implements View {
/**
* Create a {@code View} with the given {@code Encoder}.
* Internally this creates
* {@link CodecHttpMessageConverter#CodecHttpMessageConverter(Encoder)
* CodecHttpMessageConverter(Encoder)}.
* Create a {@code View} with the given {@code Encoder} wrapping it as an
* {@link EncoderHttpMessageWriter}.
*/
public HttpMessageConverterView(Encoder<?> encoder) {
this(new CodecHttpMessageConverter<>(encoder));
public HttpMessageWriterView(Encoder<?> encoder) {
this(new EncoderHttpMessageWriter<>(encoder));
}
/**
* Create a View that delegates to the given message converter.
* Create a View that delegates to the given message messageWriter.
*/
public HttpMessageConverterView(HttpMessageConverter<?> converter) {
Assert.notNull(converter, "'converter' is required.");
this.converter = converter;
this.mediaTypes = converter.getWritableMediaTypes();
public HttpMessageWriterView(HttpMessageWriter<?> messageWriter) {
Assert.notNull(messageWriter, "'messageWriter' is required.");
this.messageWriter = messageWriter;
this.mediaTypes = messageWriter.getWritableMediaTypes();
}
/**
* Return the configured message converter.
* Return the configured message messageWriter.
*/
public HttpMessageConverter<?> getConverter() {
return this.converter;
public HttpMessageWriter<?> getMessageWriter() {
return this.messageWriter;
}
/**
* By default model attributes are filtered with
* {@link HttpMessageConverter#canWrite} to find the ones that can be
* {@link HttpMessageWriter#canWrite} to find the ones that can be
* rendered. Use this property to further narrow the list and consider only
* attribute(s) under specific model key(s).
* <p>If more than one matching attribute is found, than a Map is rendered,
@@ -109,7 +107,7 @@ public class HttpMessageConverterView implements View {
@Override
public Mono<Void> render(HandlerResult result, MediaType contentType, ServerWebExchange exchange) {
Object value = extractObjectToRender(result);
return applyConverter(value, contentType, exchange);
return applyMessageWriter(value, contentType, exchange);
}
protected Object extractObjectToRender(HandlerResult result) {
@@ -126,13 +124,13 @@ public class HttpMessageConverterView implements View {
else if (map.size() == 1) {
return map.values().iterator().next();
}
else if (getConverter().canWrite(ResolvableType.forClass(Map.class), null)) {
else if (getMessageWriter().canWrite(ResolvableType.forClass(Map.class), null)) {
return map;
}
else {
throw new IllegalStateException(
"Multiple matching attributes found: " + map + ". " +
"However Map rendering is not supported by " + getConverter());
"However Map rendering is not supported by " + getMessageWriter());
}
}
@@ -145,28 +143,28 @@ public class HttpMessageConverterView implements View {
protected boolean isEligibleAttribute(String attributeName, Object attributeValue) {
ResolvableType type = ResolvableType.forClass(attributeValue.getClass());
if (getModelKeys().isEmpty()) {
return getConverter().canWrite(type, null);
return getMessageWriter().canWrite(type, null);
}
if (getModelKeys().contains(attributeName)) {
if (getConverter().canWrite(type, null)) {
if (getMessageWriter().canWrite(type, null)) {
return true;
}
throw new IllegalStateException(
"Model object [" + attributeValue + "] retrieved via key " +
"[" + attributeName + "] is not supported by " + getConverter());
"[" + attributeName + "] is not supported by " + getMessageWriter());
}
return false;
}
@SuppressWarnings("unchecked")
private <T> Mono<Void> applyConverter(Object value, MediaType contentType, ServerWebExchange exchange) {
private <T> Mono<Void> applyMessageWriter(Object value, MediaType contentType, ServerWebExchange exchange) {
if (value == null) {
return Mono.empty();
}
Publisher<? extends T> stream = Mono.just((T) value);
ResolvableType type = ResolvableType.forClass(value.getClass());
ServerHttpResponse response = exchange.getResponse();
return ((HttpMessageConverter<T>) getConverter()).write(stream, type, contentType, response);
return ((HttpMessageWriter<T>) getMessageWriter()).write(stream, type, contentType, response);
}
}

View File

@@ -36,7 +36,7 @@ import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.converter.reactive.CodecHttpMessageConverter;
import org.springframework.http.converter.reactive.EncoderHttpMessageWriter;
import org.springframework.http.server.reactive.MockServerHttpRequest;
import org.springframework.http.server.reactive.MockServerHttpResponse;
import org.springframework.stereotype.Controller;
@@ -197,7 +197,7 @@ public class DispatcherHandlerErrorTests {
@Bean
public ResponseBodyResultHandler resultHandler() {
return new ResponseBodyResultHandler(
Collections.singletonList(new CodecHttpMessageConverter<>(new StringEncoder())),
Collections.singletonList(new EncoderHttpMessageWriter<>(new StringEncoder())),
new DefaultConversionService());
}

View File

@@ -21,7 +21,7 @@ import org.junit.Test;
import org.springframework.core.Ordered;
import org.springframework.http.codec.json.JacksonJsonEncoder;
import org.springframework.web.context.support.StaticWebApplicationContext;
import org.springframework.web.reactive.result.view.HttpMessageConverterView;
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.freemarker.FreeMarkerConfigurer;
@@ -80,7 +80,7 @@ public class ViewResolverRegistryTests {
@Test
public void defaultViews() throws Exception {
View view = new HttpMessageConverterView(new JacksonJsonEncoder());
View view = new HttpMessageWriterView(new JacksonJsonEncoder());
this.registry.defaultViews(view);
assertEquals(1, this.registry.getDefaultViews().size());

View File

@@ -43,8 +43,10 @@ import org.springframework.http.MediaType;
import org.springframework.http.codec.json.JacksonJsonEncoder;
import org.springframework.http.codec.xml.Jaxb2Decoder;
import org.springframework.http.codec.xml.Jaxb2Encoder;
import org.springframework.http.converter.reactive.CodecHttpMessageConverter;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.http.converter.reactive.DecoderHttpMessageReader;
import org.springframework.http.converter.reactive.EncoderHttpMessageWriter;
import org.springframework.http.converter.reactive.HttpMessageReader;
import org.springframework.http.converter.reactive.HttpMessageWriter;
import org.springframework.http.server.reactive.MockServerHttpRequest;
import org.springframework.http.server.reactive.MockServerHttpResponse;
import org.springframework.util.MimeType;
@@ -56,7 +58,7 @@ import org.springframework.web.reactive.result.method.annotation.RequestMappingH
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.HttpMessageConverterView;
import org.springframework.web.reactive.result.view.HttpMessageWriterView;
import org.springframework.web.reactive.result.view.View;
import org.springframework.web.reactive.result.view.ViewResolutionResultHandler;
import org.springframework.web.reactive.result.view.ViewResolver;
@@ -142,15 +144,15 @@ public class WebReactiveConfigurationTests {
RequestMappingHandlerAdapter adapter = context.getBean(name, RequestMappingHandlerAdapter.class);
assertNotNull(adapter);
List<HttpMessageConverter<?>> converters = adapter.getMessageConverters();
assertEquals(6, converters.size());
List<HttpMessageReader<?>> readers = adapter.getMessageReaders();
assertEquals(5, readers.size());
assertHasConverter(converters, ByteBuffer.class, APPLICATION_OCTET_STREAM, APPLICATION_OCTET_STREAM);
assertHasConverter(converters, String.class, TEXT_PLAIN, TEXT_PLAIN);
assertHasConverter(converters, Resource.class, IMAGE_PNG, IMAGE_PNG);
assertHasConverter(converters, TestBean.class, APPLICATION_XML, APPLICATION_XML);
assertHasConverter(converters, TestBean.class, APPLICATION_JSON, APPLICATION_JSON);
assertHasConverter(converters, TestBean.class, null, MediaType.parseMediaType("text/event-stream"));
assertHasMessageReader(readers, ByteBuffer.class, APPLICATION_OCTET_STREAM);
assertHasMessageReader(readers, String.class, TEXT_PLAIN);
assertHasMessageReader(readers, Resource.class, IMAGE_PNG);
assertHasMessageReader(readers, TestBean.class, APPLICATION_XML);
assertHasMessageReader(readers, TestBean.class, APPLICATION_JSON);
assertHasMessageReader(readers, TestBean.class, null);
name = "mvcConversionService";
ConversionService service = context.getBean(name, ConversionService.class);
@@ -170,11 +172,11 @@ public class WebReactiveConfigurationTests {
RequestMappingHandlerAdapter adapter = context.getBean(name, RequestMappingHandlerAdapter.class);
assertNotNull(adapter);
List<HttpMessageConverter<?>> converters = adapter.getMessageConverters();
assertEquals(2, converters.size());
List<HttpMessageReader<?>> messageReaders = adapter.getMessageReaders();
assertEquals(2, messageReaders.size());
assertHasConverter(converters, String.class, TEXT_PLAIN, TEXT_PLAIN);
assertHasConverter(converters, TestBean.class, APPLICATION_XML, APPLICATION_XML);
assertHasMessageReader(messageReaders, String.class, TEXT_PLAIN);
assertHasMessageReader(messageReaders, TestBean.class, APPLICATION_XML);
}
@Test
@@ -199,15 +201,15 @@ public class WebReactiveConfigurationTests {
assertEquals(0, handler.getOrder());
List<HttpMessageConverter<?>> converters = handler.getMessageConverters();
assertEquals(6, converters.size());
List<HttpMessageWriter<?>> writers = handler.getMessageWriters();
assertEquals(6, writers.size());
assertHasConverter(converters, ByteBuffer.class, APPLICATION_OCTET_STREAM, APPLICATION_OCTET_STREAM);
assertHasConverter(converters, String.class, TEXT_PLAIN, TEXT_PLAIN);
assertHasConverter(converters, Resource.class, IMAGE_PNG, IMAGE_PNG);
assertHasConverter(converters, TestBean.class, APPLICATION_XML, APPLICATION_XML);
assertHasConverter(converters, TestBean.class, APPLICATION_JSON, APPLICATION_JSON);
assertHasConverter(converters, TestBean.class, null, MediaType.parseMediaType("text/event-stream"));
assertHasMessageWriter(writers, ByteBuffer.class, APPLICATION_OCTET_STREAM);
assertHasMessageWriter(writers, String.class, TEXT_PLAIN);
assertHasMessageWriter(writers, Resource.class, IMAGE_PNG);
assertHasMessageWriter(writers, TestBean.class, APPLICATION_XML);
assertHasMessageWriter(writers, TestBean.class, APPLICATION_JSON);
assertHasMessageWriter(writers, TestBean.class, MediaType.parseMediaType("text/event-stream"));
name = "mvcContentTypeResolver";
RequestedContentTypeResolver resolver = context.getBean(name, RequestedContentTypeResolver.class);
@@ -224,15 +226,15 @@ public class WebReactiveConfigurationTests {
assertEquals(100, handler.getOrder());
List<HttpMessageConverter<?>> converters = handler.getMessageConverters();
assertEquals(6, converters.size());
List<HttpMessageWriter<?>> writers = handler.getMessageWriters();
assertEquals(6, writers.size());
assertHasConverter(converters, ByteBuffer.class, APPLICATION_OCTET_STREAM, APPLICATION_OCTET_STREAM);
assertHasConverter(converters, String.class, TEXT_PLAIN, TEXT_PLAIN);
assertHasConverter(converters, Resource.class, IMAGE_PNG, IMAGE_PNG);
assertHasConverter(converters, TestBean.class, APPLICATION_XML, APPLICATION_XML);
assertHasConverter(converters, TestBean.class, APPLICATION_JSON, APPLICATION_JSON);
assertHasConverter(converters, TestBean.class, null, MediaType.parseMediaType("text/event-stream"));
assertHasMessageWriter(writers, ByteBuffer.class, APPLICATION_OCTET_STREAM);
assertHasMessageWriter(writers, String.class, TEXT_PLAIN);
assertHasMessageWriter(writers, Resource.class, IMAGE_PNG);
assertHasMessageWriter(writers, TestBean.class, APPLICATION_XML);
assertHasMessageWriter(writers, TestBean.class, APPLICATION_JSON);
assertHasMessageWriter(writers, TestBean.class, null);
name = "mvcContentTypeResolver";
RequestedContentTypeResolver resolver = context.getBean(name, RequestedContentTypeResolver.class);
@@ -261,12 +263,18 @@ public class WebReactiveConfigurationTests {
}
private void assertHasConverter(List<HttpMessageConverter<?>> converters, Class<?> clazz,
MediaType readMediaType, MediaType writeMediaType) {
private void assertHasMessageReader(List<HttpMessageReader<?>> readers, Class<?> clazz, MediaType mediaType) {
ResolvableType type = ResolvableType.forClass(clazz);
assertTrue(converters.stream()
.filter(c -> (readMediaType == null || c.canRead(type, readMediaType))
&& (writeMediaType == null || c.canWrite(type, writeMediaType)))
assertTrue(readers.stream()
.filter(c -> mediaType == null || c.canRead(type, mediaType))
.findAny()
.isPresent());
}
private void assertHasMessageWriter(List<HttpMessageWriter<?>> writers, Class<?> clazz, MediaType mediaType) {
ResolvableType type = ResolvableType.forClass(clazz);
assertTrue(writers.stream()
.filter(c -> mediaType == null || c.canWrite(type, mediaType))
.findAny()
.isPresent());
}
@@ -293,13 +301,23 @@ public class WebReactiveConfigurationTests {
static class CustomMessageConverterConfig extends WebReactiveConfiguration {
@Override
protected void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new CodecHttpMessageConverter<>(new StringEncoder(), new StringDecoder()));
protected void configureMessageReaders(List<HttpMessageReader<?>> messageReaders) {
messageReaders.add(new DecoderHttpMessageReader<>(new StringDecoder()));
}
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new CodecHttpMessageConverter<>(new Jaxb2Encoder(), new Jaxb2Decoder()));
protected void configureMessageWriters(List<HttpMessageWriter<?>> messageWriters) {
messageWriters.add(new EncoderHttpMessageWriter<>(new StringEncoder()));
}
@Override
protected void extendMessageReaders(List<HttpMessageReader<?>> messageReaders) {
messageReaders.add(new DecoderHttpMessageReader<>(new Jaxb2Decoder()));
}
@Override
protected void extendMessageWriters(List<HttpMessageWriter<?>> messageWriters) {
messageWriters.add(new EncoderHttpMessageWriter<>(new Jaxb2Encoder()));
}
}
@@ -309,7 +327,7 @@ public class WebReactiveConfigurationTests {
@Override
protected void configureViewResolvers(ViewResolverRegistry registry) {
registry.freeMarker();
registry.defaultViews(new HttpMessageConverterView(new JacksonJsonEncoder()));
registry.defaultViews(new HttpMessageWriterView(new JacksonJsonEncoder()));
}
@Bean

View File

@@ -45,8 +45,8 @@ import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.converter.reactive.CodecHttpMessageConverter;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.http.converter.reactive.DecoderHttpMessageReader;
import org.springframework.http.converter.reactive.HttpMessageReader;
import org.springframework.http.server.reactive.MockServerHttpRequest;
import org.springframework.http.server.reactive.MockServerHttpResponse;
import org.springframework.ui.ExtendedModelMap;
@@ -66,7 +66,7 @@ import static org.springframework.core.ResolvableType.forClassWithGenerics;
/**
* Unit tests for {@link HttpEntityArgumentResolver}.When adding a test also
* consider whether the logic under test is in a parent class, then see:
* {@link MessageConverterArgumentResolverTests}.
* {@link MessageReaderArgumentResolverTests}.
*
* @author Rossen Stoyanchev
*/
@@ -89,14 +89,14 @@ public class HttpEntityArgumentResolverTests {
}
private HttpEntityArgumentResolver createResolver() {
List<HttpMessageConverter<?>> converters = new ArrayList<>();
converters.add(new CodecHttpMessageConverter<>(new StringDecoder()));
List<HttpMessageReader<?>> readers = new ArrayList<>();
readers.add(new DecoderHttpMessageReader<>(new StringDecoder()));
FormattingConversionService service = new DefaultFormattingConversionService();
service.addConverter(new MonoToCompletableFutureConverter());
service.addConverter(new ReactorToRxJava1Converter());
return new HttpEntityArgumentResolver(converters, service);
return new HttpEntityArgumentResolver(readers, service);
}

View File

@@ -50,8 +50,8 @@ import org.springframework.format.support.FormattingConversionService;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.codec.json.JacksonJsonDecoder;
import org.springframework.http.converter.reactive.CodecHttpMessageConverter;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.http.converter.reactive.DecoderHttpMessageReader;
import org.springframework.http.converter.reactive.HttpMessageReader;
import org.springframework.http.server.reactive.MockServerHttpRequest;
import org.springframework.http.server.reactive.MockServerHttpResponse;
import org.springframework.validation.Errors;
@@ -72,12 +72,12 @@ import static org.springframework.core.ResolvableType.forClass;
import static org.springframework.core.ResolvableType.forClassWithGenerics;
/**
* Unit tests for {@link AbstractMessageConverterArgumentResolver}.
* Unit tests for {@link AbstractMessageReaderArgumentResolver}.
* @author Rossen Stoyanchev
*/
public class MessageConverterArgumentResolverTests {
public class MessageReaderArgumentResolverTests {
private AbstractMessageConverterArgumentResolver resolver = resolver(new JacksonJsonDecoder());
private AbstractMessageReaderArgumentResolver resolver = resolver(new JacksonJsonDecoder());
private ServerWebExchange exchange;
@@ -276,16 +276,16 @@ public class MessageConverterArgumentResolverTests {
}
@SuppressWarnings("Convert2MethodRef")
private AbstractMessageConverterArgumentResolver resolver(Decoder<?>... decoders) {
private AbstractMessageReaderArgumentResolver resolver(Decoder<?>... decoders) {
List<HttpMessageConverter<?>> converters = new ArrayList<>();
Arrays.asList(decoders).forEach(decoder -> converters.add(new CodecHttpMessageConverter<>(decoder)));
List<HttpMessageReader<?>> readers = new ArrayList<>();
Arrays.asList(decoders).forEach(decoder -> readers.add(new DecoderHttpMessageReader<>(decoder)));
FormattingConversionService service = new DefaultFormattingConversionService();
service.addConverter(new MonoToCompletableFutureConverter());
service.addConverter(new ReactorToRxJava1Converter());
return new AbstractMessageConverterArgumentResolver(converters, service, new TestBeanValidator()) {};
return new AbstractMessageReaderArgumentResolver(readers, service, new TestBeanValidator()) {};
}
private DataBuffer dataBuffer(String body) {

View File

@@ -49,9 +49,9 @@ import org.springframework.core.io.buffer.support.DataBufferTestUtils;
import org.springframework.http.HttpMethod;
import org.springframework.http.codec.json.JacksonJsonEncoder;
import org.springframework.http.codec.xml.Jaxb2Encoder;
import org.springframework.http.converter.reactive.CodecHttpMessageConverter;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.http.converter.reactive.ResourceHttpMessageConverter;
import org.springframework.http.converter.reactive.EncoderHttpMessageWriter;
import org.springframework.http.converter.reactive.HttpMessageWriter;
import org.springframework.http.converter.reactive.ResourceHttpMessageWriter;
import org.springframework.http.server.reactive.MockServerHttpRequest;
import org.springframework.http.server.reactive.MockServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpRequest;
@@ -70,12 +70,12 @@ import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8;
import static org.springframework.web.reactive.HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE;
/**
* Unit tests for {@link AbstractMessageConverterResultHandler}.
* Unit tests for {@link AbstractMessageWriterResultHandler}.
* @author Rossen Stoyanchev
*/
public class MessageConverterResultHandlerTests {
public class MessageWriterResultHandlerTests {
private AbstractMessageConverterResultHandler resultHandler;
private AbstractMessageWriterResultHandler resultHandler;
private MockServerHttpResponse response = new MockServerHttpResponse();
@@ -131,8 +131,8 @@ public class MessageConverterResultHandlerTests {
ByteArrayOutputStream body = new ByteArrayOutputStream();
ResolvableType type = ResolvableType.forType(OutputStream.class);
HttpMessageConverter<?> converter = new CodecHttpMessageConverter<>(new ByteBufferEncoder());
Mono<Void> mono = createResultHandler(converter).writeBody(this.exchange, body, type, returnType(type));
HttpMessageWriter<?> writer = new EncoderHttpMessageWriter<>(new ByteBufferEncoder());
Mono<Void> mono = createResultHandler(writer).writeBody(this.exchange, body, type, returnType(type));
TestSubscriber.subscribe(mono).assertError(IllegalStateException.class);
}
@@ -175,18 +175,18 @@ public class MessageConverterResultHandlerTests {
return ResolvableMethod.onClass(TestController.class).returning(bodyType).resolveReturnType();
}
private AbstractMessageConverterResultHandler createResultHandler(HttpMessageConverter<?>... converters) {
List<HttpMessageConverter<?>> converterList;
if (ObjectUtils.isEmpty(converters)) {
converterList = new ArrayList<>();
converterList.add(new CodecHttpMessageConverter<>(new ByteBufferEncoder()));
converterList.add(new CodecHttpMessageConverter<>(new StringEncoder()));
converterList.add(new ResourceHttpMessageConverter());
converterList.add(new CodecHttpMessageConverter<>(new Jaxb2Encoder()));
converterList.add(new CodecHttpMessageConverter<>(new JacksonJsonEncoder()));
private AbstractMessageWriterResultHandler createResultHandler(HttpMessageWriter<?>... writers) {
List<HttpMessageWriter<?>> writerList;
if (ObjectUtils.isEmpty(writers)) {
writerList = new ArrayList<>();
writerList.add(new EncoderHttpMessageWriter<>(new ByteBufferEncoder()));
writerList.add(new EncoderHttpMessageWriter<>(new StringEncoder()));
writerList.add(new ResourceHttpMessageWriter());
writerList.add(new EncoderHttpMessageWriter<>(new Jaxb2Encoder()));
writerList.add(new EncoderHttpMessageWriter<>(new JacksonJsonEncoder()));
}
else {
converterList = Arrays.asList(converters);
writerList = Arrays.asList(writers);
}
GenericConversionService service = new GenericConversionService();
@@ -195,7 +195,7 @@ public class MessageConverterResultHandlerTests {
RequestedContentTypeResolver resolver = new RequestedContentTypeResolverBuilder().build();
return new AbstractMessageConverterResultHandler(converterList, service, resolver) {};
return new AbstractMessageWriterResultHandler(writerList, service, resolver) {};
}
private void assertResponseBody(String responseBody) {

View File

@@ -44,8 +44,8 @@ import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.http.HttpMethod;
import org.springframework.http.converter.reactive.CodecHttpMessageConverter;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.http.converter.reactive.DecoderHttpMessageReader;
import org.springframework.http.converter.reactive.HttpMessageReader;
import org.springframework.http.server.reactive.MockServerHttpRequest;
import org.springframework.http.server.reactive.MockServerHttpResponse;
import org.springframework.ui.ExtendedModelMap;
@@ -67,7 +67,7 @@ import static org.springframework.core.ResolvableType.forClassWithGenerics;
/**
* Unit tests for {@link RequestBodyArgumentResolver}. When adding a test also
* consider whether the logic under test is in a parent class, then see:
* {@link MessageConverterArgumentResolverTests}.
* {@link MessageReaderArgumentResolverTests}.
*
* @author Rossen Stoyanchev
*/
@@ -90,14 +90,14 @@ public class RequestBodyArgumentResolverTests {
}
private RequestBodyArgumentResolver resolver() {
List<HttpMessageConverter<?>> converters = new ArrayList<>();
converters.add(new CodecHttpMessageConverter<>(new StringDecoder()));
List<HttpMessageReader<?>> readers = new ArrayList<>();
readers.add(new DecoderHttpMessageReader<>(new StringDecoder()));
FormattingConversionService service = new DefaultFormattingConversionService();
service.addConverter(new MonoToCompletableFutureConverter());
service.addConverter(new ReactorToRxJava1Converter());
return new RequestBodyArgumentResolver(converters, service);
return new RequestBodyArgumentResolver(readers, service);
}

View File

@@ -36,9 +36,9 @@ import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.http.codec.json.JacksonJsonEncoder;
import org.springframework.http.codec.xml.Jaxb2Encoder;
import org.springframework.http.converter.reactive.CodecHttpMessageConverter;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.http.converter.reactive.ResourceHttpMessageConverter;
import org.springframework.http.converter.reactive.EncoderHttpMessageWriter;
import org.springframework.http.converter.reactive.HttpMessageWriter;
import org.springframework.http.converter.reactive.ResourceHttpMessageWriter;
import org.springframework.http.server.reactive.MockServerHttpRequest;
import org.springframework.http.server.reactive.MockServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpRequest;
@@ -62,7 +62,7 @@ import static org.junit.Assert.assertEquals;
* Unit tests for {@link ResponseBodyResultHandler}.When adding a test also
* consider whether the logic under test is in a parent class, then see:
* <ul>
* <li>{@code MessageConverterResultHandlerTests},
* <li>{@code MessageWriterResultHandlerTests},
* <li>{@code ContentNegotiatingResultHandlerSupportTests}
* </ul>
*
@@ -86,25 +86,25 @@ public class ResponseBodyResultHandlerTests {
}
private ResponseBodyResultHandler createHandler(HttpMessageConverter<?>... converters) {
List<HttpMessageConverter<?>> converterList;
if (ObjectUtils.isEmpty(converters)) {
converterList = new ArrayList<>();
converterList.add(new CodecHttpMessageConverter<>(new ByteBufferEncoder()));
converterList.add(new CodecHttpMessageConverter<>(new StringEncoder()));
converterList.add(new ResourceHttpMessageConverter());
converterList.add(new CodecHttpMessageConverter<>(new Jaxb2Encoder()));
converterList.add(new CodecHttpMessageConverter<>(new JacksonJsonEncoder()));
private ResponseBodyResultHandler createHandler(HttpMessageWriter<?>... writers) {
List<HttpMessageWriter<?>> writerList;
if (ObjectUtils.isEmpty(writers)) {
writerList = new ArrayList<>();
writerList.add(new EncoderHttpMessageWriter<>(new ByteBufferEncoder()));
writerList.add(new EncoderHttpMessageWriter<>(new StringEncoder()));
writerList.add(new ResourceHttpMessageWriter());
writerList.add(new EncoderHttpMessageWriter<>(new Jaxb2Encoder()));
writerList.add(new EncoderHttpMessageWriter<>(new JacksonJsonEncoder()));
}
else {
converterList = Arrays.asList(converters);
writerList = Arrays.asList(writers);
}
FormattingConversionService service = new DefaultFormattingConversionService();
service.addConverter(new MonoToCompletableFutureConverter());
service.addConverter(new ReactorToRxJava1Converter());
RequestedContentTypeResolver resolver = new RequestedContentTypeResolverBuilder().build();
return new ResponseBodyResultHandler(converterList, new DefaultConversionService(), resolver);
return new ResponseBodyResultHandler(writerList, new DefaultConversionService(), resolver);
}
@Test

View File

@@ -43,9 +43,9 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.codec.json.JacksonJsonEncoder;
import org.springframework.http.codec.xml.Jaxb2Encoder;
import org.springframework.http.converter.reactive.CodecHttpMessageConverter;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.http.converter.reactive.ResourceHttpMessageConverter;
import org.springframework.http.converter.reactive.EncoderHttpMessageWriter;
import org.springframework.http.converter.reactive.HttpMessageWriter;
import org.springframework.http.converter.reactive.ResourceHttpMessageWriter;
import org.springframework.http.server.reactive.MockServerHttpRequest;
import org.springframework.http.server.reactive.MockServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpRequest;
@@ -68,7 +68,7 @@ import static org.springframework.core.ResolvableType.forClassWithGenerics;
* Unit tests for {@link ResponseEntityResultHandler}. When adding a test also
* consider whether the logic under test is in a parent class, then see:
* <ul>
* <li>{@code MessageConverterResultHandlerTests},
* <li>{@code MessageWriterResultHandlerTests},
* <li>{@code ContentNegotiatingResultHandlerSupportTests}
* </ul>
* @author Rossen Stoyanchev
@@ -89,18 +89,18 @@ public class ResponseEntityResultHandlerTests {
this.exchange = new DefaultServerWebExchange(request, this.response, new MockWebSessionManager());
}
private ResponseEntityResultHandler createHandler(HttpMessageConverter<?>... converters) {
List<HttpMessageConverter<?>> converterList;
if (ObjectUtils.isEmpty(converters)) {
converterList = new ArrayList<>();
converterList.add(new CodecHttpMessageConverter<>(new ByteBufferEncoder()));
converterList.add(new CodecHttpMessageConverter<>(new StringEncoder()));
converterList.add(new ResourceHttpMessageConverter());
converterList.add(new CodecHttpMessageConverter<>(new Jaxb2Encoder()));
converterList.add(new CodecHttpMessageConverter<>(new JacksonJsonEncoder()));
private ResponseEntityResultHandler createHandler(HttpMessageWriter<?>... writers) {
List<HttpMessageWriter<?>> writerList;
if (ObjectUtils.isEmpty(writers)) {
writerList = new ArrayList<>();
writerList.add(new EncoderHttpMessageWriter<>(new ByteBufferEncoder()));
writerList.add(new EncoderHttpMessageWriter<>(new StringEncoder()));
writerList.add(new ResourceHttpMessageWriter());
writerList.add(new EncoderHttpMessageWriter<>(new Jaxb2Encoder()));
writerList.add(new EncoderHttpMessageWriter<>(new JacksonJsonEncoder()));
}
else {
converterList = Arrays.asList(converters);
writerList = Arrays.asList(writers);
}
FormattingConversionService service = new DefaultFormattingConversionService();
service.addConverter(new MonoToCompletableFutureConverter());
@@ -109,7 +109,7 @@ public class ResponseEntityResultHandlerTests {
RequestedContentTypeResolver resolver = new RequestedContentTypeResolverBuilder().build();
return new ResponseEntityResultHandler(converterList, service, resolver);
return new ResponseEntityResultHandler(writerList, service, resolver);
}

View File

@@ -16,13 +16,8 @@
package org.springframework.web.reactive.result.method.annotation;
import static org.junit.Assume.assumeFalse;
import static org.springframework.web.client.reactive.ClientWebRequestBuilders.*;
import static org.springframework.web.client.reactive.ResponseExtractors.*;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.junit.Before;
@@ -34,18 +29,13 @@ import reactor.test.TestSubscriber;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.codec.ByteBufferDecoder;
import org.springframework.core.codec.ByteBufferEncoder;
import org.springframework.core.codec.Encoder;
import org.springframework.core.codec.StringDecoder;
import org.springframework.core.codec.StringEncoder;
import org.springframework.http.MediaType;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.http.codec.SseEventEncoder;
import org.springframework.http.codec.SseEvent;
import org.springframework.http.codec.json.JacksonJsonDecoder;
import org.springframework.http.codec.json.JacksonJsonEncoder;
import org.springframework.http.converter.reactive.CodecHttpMessageConverter;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.http.converter.reactive.DecoderHttpMessageReader;
import org.springframework.http.converter.reactive.HttpMessageReader;
import org.springframework.http.server.reactive.AbstractHttpHandlerIntegrationTests;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.http.server.reactive.bootstrap.JettyHttpServer;
@@ -54,9 +44,12 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.reactive.WebClient;
import org.springframework.web.reactive.DispatcherHandler;
import org.springframework.web.reactive.config.WebReactiveConfiguration;
import org.springframework.http.codec.SseEvent;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
import static org.junit.Assume.assumeFalse;
import static org.springframework.web.client.reactive.ClientWebRequestBuilders.get;
import static org.springframework.web.client.reactive.ResponseExtractors.bodyStream;
/**
* @author Sebastien Deleuze
*/
@@ -74,11 +67,6 @@ public class SseIntegrationTests extends AbstractHttpHandlerIntegrationTests {
assumeFalse(server instanceof JettyHttpServer);
this.webClient = new WebClient(new ReactorClientHttpConnector());
List<HttpMessageConverter<?>> converters = new ArrayList<>();
converters.add(new CodecHttpMessageConverter<>(new ByteBufferEncoder(), new ByteBufferDecoder()));
converters.add(new CodecHttpMessageConverter<>(new StringEncoder(), new StringDecoder(false)));
converters.add(new CodecHttpMessageConverter<>(new JacksonJsonEncoder(), new JacksonJsonDecoder()));
this.webClient.setMessageConverters(converters);
}
@Override
@@ -110,16 +98,14 @@ public class SseIntegrationTests extends AbstractHttpHandlerIntegrationTests {
}
@Test
public void sseAsPojo() throws Exception {
public void sseAsPerson() throws Exception {
Mono<String> result = this.webClient
.perform(get("http://localhost:" + port + "/sse/person")
.accept(new MediaType("text", "event-stream")))
.extract(bodyStream(String.class))
.filter(s -> !s.equals("\n"))
.map(s -> (s.replace("\n", "")))
.takeUntil(s -> {
return s.endsWith("foo 1\"}");
})
.map(s -> s.replace("\n", ""))
.takeUntil(s -> s.endsWith("foo 1\"}"))
.reduce((s1, s2) -> s1 + s2);
TestSubscriber
@@ -135,7 +121,7 @@ public class SseIntegrationTests extends AbstractHttpHandlerIntegrationTests {
.accept(new MediaType("text", "event-stream")))
.extract(bodyStream(String.class))
.filter(s -> !s.equals("\n"))
.map(s -> (s.replace("\n", "")))
.map(s -> s.replace("\n", ""))
.take(2);
TestSubscriber
@@ -182,12 +168,6 @@ public class SseIntegrationTests extends AbstractHttpHandlerIntegrationTests {
public SseController sseController() {
return new SseController();
}
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
Encoder<Object> sseEncoder = new SseEventEncoder(Arrays.asList(new JacksonJsonEncoder()));
converters.add(new CodecHttpMessageConverter<>(sseEncoder));
}
}
private static class Person {

View File

@@ -58,12 +58,12 @@ import static org.junit.Assert.fail;
/**
* Unit tests for {@link HttpMessageConverterView}.
* Unit tests for {@link HttpMessageWriterView}.
* @author Rossen Stoyanchev
*/
public class HttpMessageConverterViewTests {
public class HttpMessageWriterViewTests {
private HttpMessageConverterView view = new HttpMessageConverterView(new JacksonJsonEncoder());
private HttpMessageWriterView view = new HttpMessageWriterView(new JacksonJsonEncoder());
private HandlerResult result;
@@ -123,7 +123,7 @@ public class HttpMessageConverterViewTests {
@Test
public void extractObjectMultipleMatchesNotSupported() throws Exception {
HttpMessageConverterView view = new HttpMessageConverterView(new StringEncoder());
HttpMessageWriterView view = new HttpMessageWriterView(new StringEncoder());
view.setModelKeys(new HashSet<>(Arrays.asList("foo1", "foo2")));
this.model.addAttribute("foo1", "bar1");
this.model.addAttribute("foo2", "bar2");
@@ -140,7 +140,7 @@ public class HttpMessageConverterViewTests {
@Test
public void extractObjectNotSupported() throws Exception {
HttpMessageConverterView view = new HttpMessageConverterView(new Jaxb2Encoder());
HttpMessageWriterView view = new HttpMessageWriterView(new Jaxb2Encoder());
view.setModelKeys(new HashSet<>(Collections.singletonList("foo1")));
this.model.addAttribute("foo1", "bar1");

View File

@@ -90,8 +90,7 @@ public class SseEventEncoder extends AbstractEncoder<Object> {
Object data = event.getData();
Flux<DataBuffer> dataBuffer = Flux.empty();
MediaType mediaType = (event.getMediaType() == null ?
MediaType.ALL : event.getMediaType());
MediaType mediaType = (event.getMediaType() == null ? MediaType.ALL : event.getMediaType());
if (data != null) {
sb.append("data:");
if (data instanceof String) {

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.http.converter.reactive;
import java.util.Collections;
import java.util.List;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.Decoder;
import org.springframework.http.MediaType;
import org.springframework.http.ReactiveHttpInputMessage;
/**
* Implementation of the {@link HttpMessageReader} interface that delegates to
* a {@link Decoder}.
*
* @author Arjen Poutsma
* @author Sebastien Deleuze
* @author Rossen Stoyanchev
* @since 5.0
*/
public class DecoderHttpMessageReader<T> implements HttpMessageReader<T> {
private final Decoder<T> decoder;
private final List<MediaType> readableMediaTypes;
/**
* Create a {@code CodecHttpMessageConverter} with the given {@link Decoder}.
* @param decoder the decoder to use
*/
public DecoderHttpMessageReader(Decoder<T> decoder) {
this.decoder = decoder;
this.readableMediaTypes = decoder != null ?
MediaType.toMediaTypes(decoder.getDecodableMimeTypes()) :
Collections.emptyList();
}
@Override
public boolean canRead(ResolvableType type, MediaType mediaType) {
return this.decoder != null && this.decoder.canDecode(type, mediaType);
}
@Override
public List<MediaType> getReadableMediaTypes() {
return this.readableMediaTypes;
}
@Override
public Flux<T> read(ResolvableType type, ReactiveHttpInputMessage inputMessage) {
if (this.decoder == null) {
return Flux.error(new IllegalStateException("No decoder set"));
}
MediaType contentType = getContentType(inputMessage);
return this.decoder.decode(inputMessage.getBody(), type, contentType);
}
@Override
public Mono<T> readMono(ResolvableType type, ReactiveHttpInputMessage inputMessage) {
if (this.decoder == null) {
return Mono.error(new IllegalStateException("No decoder set"));
}
MediaType contentType = getContentType(inputMessage);
return this.decoder.decodeToMono(inputMessage.getBody(), type, contentType);
}
private MediaType getContentType(ReactiveHttpInputMessage inputMessage) {
MediaType contentType = inputMessage.getHeaders().getContentType();
return (contentType != null ? contentType : MediaType.APPLICATION_OCTET_STREAM);
}
}

View File

@@ -24,119 +24,52 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.Decoder;
import org.springframework.core.codec.Encoder;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ReactiveHttpInputMessage;
import org.springframework.http.ReactiveHttpOutputMessage;
/**
* Implementation of the {@link HttpMessageConverter} interface that delegates to
* {@link Encoder} and {@link Decoder}.
* Implementation of the {@link HttpMessageWriter} interface that delegates to
* an {@link Encoder}.
*
* @author Arjen Poutsma
* @author Sebastien Deleuze
* @author Rossen Stoyanchev
* @since 5.0
*/
public class CodecHttpMessageConverter<T> implements HttpMessageConverter<T> {
public class EncoderHttpMessageWriter<T> implements HttpMessageWriter<T> {
private final Encoder<T> encoder;
private final Decoder<T> decoder;
private final List<MediaType> readableMediaTypes;
private final List<MediaType> writableMediaTypes;
/**
* Create a {@code CodecHttpMessageConverter} with the given {@link Encoder}. When
* using this constructor, all read-related methods will in {@code false} or an
* {@link IllegalStateException}.
* Create a {@code CodecHttpMessageConverter} with the given {@link Encoder}.
* @param encoder the encoder to use
*/
public CodecHttpMessageConverter(Encoder<T> encoder) {
this(encoder, null);
}
/**
* Create a {@code CodecHttpMessageConverter} with the given {@link Decoder}. When
* using this constructor, all write-related methods will in {@code false} or an
* {@link IllegalStateException}.
* @param decoder the decoder to use
*/
public CodecHttpMessageConverter(Decoder<T> decoder) {
this(null, decoder);
}
/**
* Create a {@code CodecHttpMessageConverter} with the given {@link Encoder} and
* {@link Decoder}.
* @param encoder the encoder to use, can be {@code null}
* @param decoder the decoder to use, can be {@code null}
*/
public CodecHttpMessageConverter(Encoder<T> encoder, Decoder<T> decoder) {
public EncoderHttpMessageWriter(Encoder<T> encoder) {
this.encoder = encoder;
this.decoder = decoder;
this.readableMediaTypes = decoder != null ?
MediaType.toMediaTypes(decoder.getDecodableMimeTypes()) :
Collections.emptyList();
this.writableMediaTypes = encoder != null ?
MediaType.toMediaTypes(encoder.getEncodableMimeTypes()) :
Collections.emptyList();
}
@Override
public boolean canRead(ResolvableType type, MediaType mediaType) {
return this.decoder != null && this.decoder.canDecode(type, mediaType);
}
@Override
public boolean canWrite(ResolvableType type, MediaType mediaType) {
return this.encoder != null && this.encoder.canEncode(type, mediaType);
}
@Override
public List<MediaType> getReadableMediaTypes() {
return this.readableMediaTypes;
}
@Override
public List<MediaType> getWritableMediaTypes() {
return this.writableMediaTypes;
}
@Override
public Flux<T> read(ResolvableType type, ReactiveHttpInputMessage inputMessage) {
if (this.decoder == null) {
return Flux.error(new IllegalStateException("No decoder set"));
}
MediaType contentType = getContentType(inputMessage);
return this.decoder.decode(inputMessage.getBody(), type, contentType);
}
@Override
public Mono<T> readMono(ResolvableType type, ReactiveHttpInputMessage inputMessage) {
if (this.decoder == null) {
return Mono.error(new IllegalStateException("No decoder set"));
}
MediaType contentType = getContentType(inputMessage);
return this.decoder.decodeToMono(inputMessage.getBody(), type, contentType);
}
private MediaType getContentType(ReactiveHttpInputMessage inputMessage) {
MediaType contentType = inputMessage.getHeaders().getContentType();
return (contentType != null ? contentType : MediaType.APPLICATION_OCTET_STREAM);
}
@Override
public Mono<Void> write(Publisher<? extends T> inputStream, ResolvableType type,
MediaType contentType, ReactiveHttpOutputMessage outputMessage) {

View File

@@ -28,13 +28,14 @@ import org.springframework.http.ReactiveHttpInputMessage;
import org.springframework.http.ReactiveHttpOutputMessage;
/**
* Strategy interface that specifies a converter that can convert from and to HTTP
* requests and responses.
* Strategy interface that specifies a reader that can convert from the HTTP
* request body from a stream of bytes to Objects.
*
* @author Rossen Stoyanchev
* @author Arjen Poutsma
* @since 5.0
*/
public interface HttpMessageConverter<T> {
public interface HttpMessageReader<T> {
/**
* Indicates whether the given class can be read by this converter.
@@ -71,31 +72,4 @@ public interface HttpMessageConverter<T> {
*/
Mono<T> readMono(ResolvableType type, ReactiveHttpInputMessage inputMessage);
/**
* Indicates whether the given class can be written by this converter.
* @param type the class to test for writability
* @param mediaType the media type to write, can be {@code null} if not specified.
* Typically the value of an {@code Accept} header.
* @return {@code true} if writable; {@code false} otherwise
*/
boolean canWrite(ResolvableType type, MediaType mediaType);
/**
* Return the list of {@link MediaType} objects that can be written by this converter.
* @return the list of supported readable media types
*/
List<MediaType> getWritableMediaTypes();
/**
* Write an given object to the given output message.
* @param inputStream the input stream to write
* @param type the stream element type to process.
* @param contentType the content type to use when writing. May be {@code null} to
* indicate that the default content type of the converter must be used.
* @param outputMessage the message to write to
* @return the converted {@link Mono} of object
*/
Mono<Void> write(Publisher<? extends T> inputStream, ResolvableType type,
MediaType contentType, ReactiveHttpOutputMessage outputMessage);
}

View File

@@ -0,0 +1,65 @@
/*
* 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.http.converter.reactive;
import java.util.List;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import org.springframework.core.ResolvableType;
import org.springframework.http.MediaType;
import org.springframework.http.ReactiveHttpOutputMessage;
/**
* Strategy interface that specifies a converter that can convert a stream of
* Objects to a stream of bytes to be written to the HTTP response body.
*
* @author Rossen Stoyanchev
* @author Arjen Poutsma
* @since 5.0
*/
public interface HttpMessageWriter<T> {
/**
* Indicates whether the given class can be written by this converter.
* @param type the class to test for writability
* @param mediaType the media type to write, can be {@code null} if not specified.
* Typically the value of an {@code Accept} header.
* @return {@code true} if writable; {@code false} otherwise
*/
boolean canWrite(ResolvableType type, MediaType mediaType);
/**
* Return the list of {@link MediaType} objects that can be written by this converter.
* @return the list of supported readable media types
*/
List<MediaType> getWritableMediaTypes();
/**
* Write an given object to the given output message.
* @param inputStream the input stream to write
* @param type the stream element type to process.
* @param contentType the content type to use when writing. May be {@code null} to
* indicate that the default content type of the converter must be used.
* @param outputMessage the message to write to
* @return the converted {@link Mono} of object
*/
Mono<Void> write(Publisher<? extends T> inputStream, ResolvableType type,
MediaType contentType, ReactiveHttpOutputMessage outputMessage);
}

View File

@@ -37,27 +37,31 @@ import org.springframework.util.MimeTypeUtils;
import org.springframework.util.ResourceUtils;
/**
* Implementation of {@link HttpMessageConverter} that can read and write
* {@link Resource Resources} and supports byte range requests.
* Implementation of {@link HttpMessageWriter} that can write
* {@link Resource Resources}.
*
* <p>For a Resource reader simply use {@link ResourceDecoder} wrapped with
* {@link DecoderHttpMessageReader}.
*
* @author Arjen Poutsma
* @since 5.0
*/
public class ResourceHttpMessageConverter extends CodecHttpMessageConverter<Resource> {
public class ResourceHttpMessageWriter extends EncoderHttpMessageWriter<Resource> {
public ResourceHttpMessageConverter() {
super(new ResourceEncoder(), new ResourceDecoder());
public ResourceHttpMessageWriter() {
super(new ResourceEncoder());
}
public ResourceHttpMessageConverter(int bufferSize) {
super(new ResourceEncoder(bufferSize), new ResourceDecoder());
public ResourceHttpMessageWriter(int bufferSize) {
super(new ResourceEncoder(bufferSize));
}
@Override
public Mono<Void> write(Publisher<? extends Resource> inputStream,
ResolvableType type, MediaType contentType,
ReactiveHttpOutputMessage outputMessage) {
public Mono<Void> write(Publisher<? extends Resource> inputStream, ResolvableType type,
MediaType contentType, ReactiveHttpOutputMessage outputMessage) {
return Mono.from(Flux.from(inputStream).
take(1).
concatMap(resource -> {
@@ -68,11 +72,9 @@ public class ResourceHttpMessageConverter extends CodecHttpMessageConverter<Reso
}));
}
protected void addHeaders(HttpHeaders headers, Resource resource,
MediaType contentType) {
protected void addHeaders(HttpHeaders headers, Resource resource, MediaType contentType) {
if (headers.getContentType() == null) {
if (contentType == null ||
!contentType.isConcrete() ||
if (contentType == null || !contentType.isConcrete() ||
MediaType.APPLICATION_OCTET_STREAM.equals(contentType)) {
contentType = MimeTypeUtils.getMimeType(resource.getFilename()).
map(MediaType::toMediaType).
@@ -87,6 +89,7 @@ public class ResourceHttpMessageConverter extends CodecHttpMessageConverter<Reso
private Mono<Void> writeContent(Resource resource, ResolvableType type,
MediaType contentType, ReactiveHttpOutputMessage outputMessage) {
if (outputMessage instanceof ZeroCopyHttpOutputMessage) {
Optional<File> file = getFile(resource);
if (file.isPresent()) {

View File

@@ -19,7 +19,7 @@ package org.springframework.web.client.reactive;
import java.util.List;
import org.springframework.http.client.reactive.ClientHttpResponse;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.http.converter.reactive.HttpMessageReader;
/**
* Contract to extract the content of a raw {@link ClientHttpResponse} decoding
@@ -36,9 +36,9 @@ public interface BodyExtractor<T> {
/**
* Extract content from the response body
* @param clientResponse the raw HTTP response
* @param messageConverters the message converters that decode the response body
* @param messageReaders the message readers that decode the response body
* @return the relevant content
*/
T extract(ClientHttpResponse clientResponse, List<HttpMessageConverter<?>> messageConverters);
T extract(ClientHttpResponse clientResponse, List<HttpMessageReader<?>> messageReaders);
}

View File

@@ -20,7 +20,7 @@ import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.reactive.ClientHttpResponse;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.http.converter.reactive.HttpMessageReader;
/**
* Default implementation of the {@link ResponseErrorHandler} interface
@@ -33,13 +33,13 @@ import org.springframework.http.converter.reactive.HttpMessageConverter;
public class DefaultResponseErrorHandler implements ResponseErrorHandler {
@Override
public void handleError(ClientHttpResponse response, List<HttpMessageConverter<?>> messageConverters) {
public void handleError(ClientHttpResponse response, List<HttpMessageReader<?>> messageReaders) {
HttpStatus responseStatus = response.getStatusCode();
if (responseStatus.is4xxClientError()) {
throw new WebClientErrorException(response, messageConverters);
throw new WebClientErrorException(response, messageReaders);
}
if (responseStatus.is5xxServerError()) {
throw new WebServerErrorException(response, messageConverters);
throw new WebServerErrorException(response, messageReaders);
}
}
}

View File

@@ -19,7 +19,7 @@ package org.springframework.web.client.reactive;
import java.util.List;
import org.springframework.http.client.reactive.ClientHttpResponse;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.http.converter.reactive.HttpMessageReader;
/**
* Strategy interface used by the {@link WebClient} to handle errors in
@@ -37,6 +37,6 @@ public interface ResponseErrorHandler {
* {@link ClientHttpResponse#getStatusCode() HttpStatus} of the response and
* throw {@link WebClientException}s in case of errors.
*/
void handleError(ClientHttpResponse response, List<HttpMessageConverter<?>> messageConverters);
void handleError(ClientHttpResponse response, List<HttpMessageReader<?>> messageReaders);
}

View File

@@ -26,7 +26,7 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.reactive.ClientHttpResponse;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.http.converter.reactive.HttpMessageReader;
/**
* Static factory methods for {@link ResponseExtractor} and {@link BodyExtractor},
@@ -47,9 +47,9 @@ public class ResponseExtractors {
public static <T> ResponseExtractor<Mono<T>> body(ResolvableType bodyType) {
return (clientResponse, webClientConfig) -> (Mono<T>) clientResponse
.doOnNext(response -> webClientConfig.getResponseErrorHandler()
.handleError(response, webClientConfig.getMessageConverters()))
.handleError(response, webClientConfig.getMessageReaders()))
.flatMap(resp -> decodeResponseBodyAsMono(resp, bodyType,
webClientConfig.getMessageConverters()))
webClientConfig.getMessageReaders()))
.next();
}
@@ -86,8 +86,8 @@ public class ResponseExtractors {
public static <T> ResponseExtractor<Flux<T>> bodyStream(ResolvableType bodyType) {
return (clientResponse, webClientConfig) -> clientResponse
.doOnNext(response -> webClientConfig.getResponseErrorHandler()
.handleError(response, webClientConfig.getMessageConverters()))
.flatMap(resp -> decodeResponseBody(resp, bodyType, webClientConfig.getMessageConverters()));
.handleError(response, webClientConfig.getMessageReaders()))
.flatMap(resp -> decodeResponseBody(resp, bodyType, webClientConfig.getMessageReaders()));
}
/**
@@ -127,7 +127,7 @@ public class ResponseExtractors {
return (clientResponse, webClientConfig) -> clientResponse.then(response ->
Mono.when(
decodeResponseBodyAsMono(response, bodyType,
webClientConfig.getMessageConverters()).defaultIfEmpty(EMPTY_BODY),
webClientConfig.getMessageReaders()).defaultIfEmpty(EMPTY_BODY),
Mono.just(response.getHeaders()),
Mono.just(response.getStatusCode()))
).map(tuple -> {
@@ -153,7 +153,7 @@ public class ResponseExtractors {
public static <T> ResponseExtractor<Mono<ResponseEntity<Flux<T>>>> responseStream(ResolvableType type) {
return (clientResponse, webClientConfig) -> clientResponse
.map(response -> new ResponseEntity<>(
decodeResponseBody(response, type, webClientConfig.getMessageConverters()),
decodeResponseBody(response, type, webClientConfig.getMessageReaders()),
response.getHeaders(), response.getStatusCode()));
}
@@ -175,26 +175,26 @@ public class ResponseExtractors {
@SuppressWarnings("unchecked")
protected static <T> Flux<T> decodeResponseBody(ClientHttpResponse response,
ResolvableType responseType, List<HttpMessageConverter<?>> messageConverters) {
ResolvableType responseType, List<HttpMessageReader<?>> messageReaders) {
MediaType contentType = response.getHeaders().getContentType();
HttpMessageConverter<?> converter = resolveConverter(messageConverters, responseType, contentType);
return (Flux<T>) converter.read(responseType, response);
HttpMessageReader<?> reader = resolveMessageReader(messageReaders, responseType, contentType);
return (Flux<T>) reader.read(responseType, response);
}
@SuppressWarnings("unchecked")
protected static <T> Mono<T> decodeResponseBodyAsMono(ClientHttpResponse response,
ResolvableType responseType, List<HttpMessageConverter<?>> messageConverters) {
ResolvableType responseType, List<HttpMessageReader<?>> messageReaders) {
MediaType contentType = response.getHeaders().getContentType();
HttpMessageConverter<?> converter = resolveConverter(messageConverters, responseType, contentType);
return (Mono<T>) converter.readMono(responseType, response);
HttpMessageReader<?> reader = resolveMessageReader(messageReaders, responseType, contentType);
return (Mono<T>) reader.readMono(responseType, response);
}
protected static HttpMessageConverter<?> resolveConverter(
List<HttpMessageConverter<?>> messageConverters, ResolvableType responseType, MediaType contentType) {
protected static HttpMessageReader<?> resolveMessageReader(List<HttpMessageReader<?>> messageReaders,
ResolvableType responseType, MediaType contentType) {
return messageConverters.stream()
return messageReaders.stream()
.filter(e -> e.canRead(responseType, contentType))
.findFirst()
.orElseThrow(() ->

View File

@@ -25,30 +25,30 @@ import java.util.function.Function;
import java.util.logging.Level;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.Decoder;
import org.springframework.core.codec.Encoder;
import org.springframework.core.codec.ByteBufferDecoder;
import org.springframework.core.codec.ByteBufferEncoder;
import org.springframework.http.codec.json.JacksonJsonDecoder;
import org.springframework.http.codec.json.JacksonJsonEncoder;
import org.springframework.core.codec.ResourceDecoder;
import org.springframework.core.codec.StringDecoder;
import org.springframework.core.codec.StringEncoder;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.client.reactive.ClientHttpRequest;
import org.springframework.http.client.reactive.ClientHttpResponse;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.codec.json.JacksonJsonDecoder;
import org.springframework.http.codec.json.JacksonJsonEncoder;
import org.springframework.http.codec.xml.Jaxb2Decoder;
import org.springframework.http.codec.xml.Jaxb2Encoder;
import org.springframework.http.converter.reactive.CodecHttpMessageConverter;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.http.converter.reactive.ResourceHttpMessageConverter;
import org.springframework.http.converter.reactive.DecoderHttpMessageReader;
import org.springframework.http.converter.reactive.EncoderHttpMessageWriter;
import org.springframework.http.converter.reactive.HttpMessageReader;
import org.springframework.http.converter.reactive.HttpMessageWriter;
import org.springframework.http.converter.reactive.ResourceHttpMessageWriter;
import org.springframework.util.ClassUtils;
import reactor.core.publisher.Mono;
/**
* Reactive Web client supporting the HTTP/1.1 protocol
*
@@ -96,6 +96,7 @@ public final class WebClient {
private final DefaultWebClientConfig webClientConfig;
/**
* Create a {@code WebClient} instance, using the {@link ClientHttpConnector}
* implementation given as an argument to drive the underlying
@@ -114,39 +115,54 @@ public final class WebClient {
public WebClient(ClientHttpConnector clientHttpConnector) {
this.clientHttpConnector = clientHttpConnector;
this.webClientConfig = new DefaultWebClientConfig();
List<HttpMessageConverter<?>> converters = new ArrayList<>();
addDefaultHttpMessageConverters(converters);
this.webClientConfig.setMessageConverters(converters);
this.webClientConfig.setResponseErrorHandler(new DefaultResponseErrorHandler());
}
/**
* Adds default HTTP message converters
* Adds default HTTP message readers.
*/
protected final void addDefaultHttpMessageConverters(
List<HttpMessageConverter<?>> converters) {
converters.add(converter(new ByteBufferEncoder(), new ByteBufferDecoder()));
converters.add(converter(new StringEncoder(), new StringDecoder()));
converters.add(new ResourceHttpMessageConverter());
protected final void addDefaultHttpMessageReaders(List<HttpMessageReader<?>> messageReaders) {
messageReaders.add(new DecoderHttpMessageReader<>(new ByteBufferDecoder()));
messageReaders.add(new DecoderHttpMessageReader<>(new StringDecoder(false)));
messageReaders.add(new DecoderHttpMessageReader<>(new ResourceDecoder()));
if (jaxb2Present) {
converters.add(converter(new Jaxb2Encoder(), new Jaxb2Decoder()));
messageReaders.add(new DecoderHttpMessageReader<>(new Jaxb2Decoder()));
}
if (jackson2Present) {
converters.add(converter(new JacksonJsonEncoder(), new JacksonJsonDecoder()));
messageReaders.add(new DecoderHttpMessageReader<>(new JacksonJsonDecoder()));
}
}
private static <T> HttpMessageConverter<T> converter(Encoder<T> encoder,
Decoder<T> decoder) {
return new CodecHttpMessageConverter<>(encoder, decoder);
}
/**
* Set the list of {@link HttpMessageConverter}s to use for encoding and decoding HTTP
* messages
* Adds default HTTP message writers.
*/
public void setMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
this.webClientConfig.setMessageConverters(messageConverters);
protected final void addDefaultHttpMessageWriters(List<HttpMessageWriter<?>> messageWriters) {
messageWriters.add(new EncoderHttpMessageWriter<>(new ByteBufferEncoder()));
messageWriters.add(new EncoderHttpMessageWriter<>(new StringEncoder()));
messageWriters.add(new ResourceHttpMessageWriter());
if (jaxb2Present) {
messageWriters.add(new EncoderHttpMessageWriter<>(new Jaxb2Encoder()));
}
if (jackson2Present) {
messageWriters.add(new EncoderHttpMessageWriter<>(new JacksonJsonEncoder()));
}
}
/**
* Set the list of {@link HttpMessageReader}s to use for decoding the HTTP
* response body.
*/
public void setMessageReaders(List<HttpMessageReader<?>> messageReaders) {
this.webClientConfig.setMessageReaders(messageReaders);
}
/**
* Set the list of {@link HttpMessageWriter}s to use for encoding the HTTP
* request body.
*/
public void setMessageWriters(List<HttpMessageWriter<?>> messageWrters) {
this.webClientConfig.setMessageWriters(messageWrters);
}
/**
@@ -163,7 +179,7 @@ public final class WebClient {
* Requesting from the exposed {@code Flux} will result in:
* <ul>
* <li>building the actual HTTP request using the provided {@code ClientWebRequestBuilder}</li>
* <li>encoding the HTTP request body with the configured {@code HttpMessageConverter}s</li>
* <li>encoding the HTTP request body with the configured {@code HttpMessageWriter}s</li>
* <li>returning the response with a publisher of the body</li>
* </ul>
*/
@@ -191,17 +207,36 @@ public final class WebClient {
protected class DefaultWebClientConfig implements WebClientConfig {
private List<HttpMessageConverter<?>> messageConverters;
private List<HttpMessageReader<?>> messageReaders;
private List<HttpMessageWriter<?>> messageWriters;
private ResponseErrorHandler responseErrorHandler;
@Override
public List<HttpMessageConverter<?>> getMessageConverters() {
return messageConverters;
public DefaultWebClientConfig() {
this.messageReaders = new ArrayList<>();
addDefaultHttpMessageReaders(this.messageReaders);
this.messageWriters = new ArrayList<>();
addDefaultHttpMessageWriters(this.messageWriters);
}
public void setMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
this.messageConverters = messageConverters;
@Override
public List<HttpMessageReader<?>> getMessageReaders() {
return this.messageReaders;
}
public void setMessageReaders(List<HttpMessageReader<?>> messageReaders) {
this.messageReaders = messageReaders;
}
@Override
public List<HttpMessageWriter<?>> getMessageWriters() {
return this.messageWriters;
}
public void setMessageWriters(List<HttpMessageWriter<?>> messageWriters) {
this.messageWriters = messageWriters;
}
@Override
@@ -218,10 +253,12 @@ public final class WebClient {
private final ClientWebRequest clientWebRequest;
public DefaultRequestCallback(ClientWebRequest clientWebRequest) {
this.clientWebRequest = clientWebRequest;
}
@Override
public Mono<Void> apply(ClientHttpRequest clientHttpRequest) {
clientHttpRequest.getHeaders().putAll(this.clientWebRequest.getHttpHeaders());
@@ -229,13 +266,13 @@ public final class WebClient {
clientHttpRequest.getHeaders().setAccept(
Collections.singletonList(MediaType.ALL));
}
clientWebRequest.getCookies().values()
this.clientWebRequest.getCookies().values()
.stream().flatMap(cookies -> cookies.stream())
.forEach(cookie -> clientHttpRequest.getCookies().add(cookie.getName(), cookie));
if (this.clientWebRequest.getBody() != null) {
return writeRequestBody(this.clientWebRequest.getBody(),
this.clientWebRequest.getElementType(),
clientHttpRequest, webClientConfig.getMessageConverters());
clientHttpRequest, WebClient.this.webClientConfig.getMessageWriters());
}
else {
return clientHttpRequest.setComplete();
@@ -245,22 +282,22 @@ public final class WebClient {
@SuppressWarnings({ "unchecked", "rawtypes" })
protected Mono<Void> writeRequestBody(Publisher<?> content,
ResolvableType requestType, ClientHttpRequest request,
List<HttpMessageConverter<?>> messageConverters) {
List<HttpMessageWriter<?>> messageWriters) {
MediaType contentType = request.getHeaders().getContentType();
Optional<HttpMessageConverter<?>> converter = resolveConverter(messageConverters, requestType, contentType);
if (!converter.isPresent()) {
Optional<HttpMessageWriter<?>> messageWriter = resolveWriter(messageWriters, requestType, contentType);
if (!messageWriter.isPresent()) {
return Mono.error(new IllegalStateException(
"Could not encode request body of type '" + contentType
+ "' with target type '" + requestType.toString() + "'"));
}
return converter.get().write((Publisher) content, requestType, contentType, request);
return messageWriter.get().write((Publisher) content, requestType, contentType, request);
}
protected Optional<HttpMessageConverter<?>> resolveConverter(
List<HttpMessageConverter<?>> messageConverters, ResolvableType type,
MediaType mediaType) {
return messageConverters.stream().filter(e -> e.canWrite(type, mediaType)).findFirst();
protected Optional<HttpMessageWriter<?>> resolveWriter(List<HttpMessageWriter<?>> messageWriters,
ResolvableType type, MediaType mediaType) {
return messageWriters.stream().filter(e -> e.canWrite(type, mediaType)).findFirst();
}
}

View File

@@ -18,7 +18,8 @@ package org.springframework.web.client.reactive;
import java.util.List;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.http.converter.reactive.HttpMessageReader;
import org.springframework.http.converter.reactive.HttpMessageWriter;
/**
* Interface that makes the {@link WebClient} configuration information
@@ -30,9 +31,14 @@ import org.springframework.http.converter.reactive.HttpMessageConverter;
public interface WebClientConfig {
/**
* Return the message converters that can help encoding/decoding the HTTP message body
* Return the message readers that can help decoding the HTTP response body
*/
List<HttpMessageConverter<?>> getMessageConverters();
List<HttpMessageReader<?>> getMessageReaders();
/**
* Return the message writers that can help encode the HTTP request body
*/
List<HttpMessageWriter<?>> getMessageWriters();
/**
* Return the configured {@link ResponseErrorHandler}

View File

@@ -19,7 +19,7 @@ package org.springframework.web.client.reactive;
import java.util.List;
import org.springframework.http.client.reactive.ClientHttpResponse;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.http.converter.reactive.HttpMessageReader;
/**
* Exception thrown when an HTTP 4xx is received.
@@ -33,14 +33,14 @@ public class WebClientErrorException extends WebClientResponseException {
/**
* Construct a new instance of {@code HttpClientErrorException} based on a
* {@link ClientHttpResponse} and {@link HttpMessageConverter}s to optionally
* {@link ClientHttpResponse} and {@link HttpMessageReader}s to optionally
* help decoding the response body
*
* @param response the HTTP response
* @param converters the message converters that may decode the HTTP response body
* @param messageReaders the message converters that may decode the HTTP response body
*/
public WebClientErrorException(ClientHttpResponse response, List<HttpMessageConverter<?>> converters) {
super(initMessage(response), response, converters);
public WebClientErrorException(ClientHttpResponse response, List<HttpMessageReader<?>> messageReaders) {
super(initMessage(response), response, messageReaders);
}
private static String initMessage(ClientHttpResponse response) {

View File

@@ -21,7 +21,7 @@ import java.util.List;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.reactive.ClientHttpResponse;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.http.converter.reactive.HttpMessageReader;
/**
* Base class for exceptions associated with specific HTTP client response
@@ -35,20 +35,20 @@ public class WebClientResponseException extends WebClientException {
private final ClientHttpResponse clientResponse;
private final List<HttpMessageConverter<?>> messageConverters;
private final List<HttpMessageReader<?>> messageReaders;
/**
* Construct a new instance of {@code WebClientResponseException} with the given response data
* @param message the given error message
* @param clientResponse the HTTP response
* @param messageConverters the message converters that maay decode the HTTP response body
* @param messageReaders the message converters that maay decode the HTTP response body
*/
public WebClientResponseException(String message, ClientHttpResponse clientResponse,
List<HttpMessageConverter<?>> messageConverters) {
List<HttpMessageReader<?>> messageReaders) {
super(message);
this.clientResponse = clientResponse;
this.messageConverters = messageConverters;
this.messageReaders = messageReaders;
}
@@ -76,6 +76,6 @@ public class WebClientResponseException extends WebClientException {
* </pre>
*/
public <T> T getResponseBody(BodyExtractor<T> extractor) {
return extractor.extract(this.clientResponse, this.messageConverters);
return extractor.extract(this.clientResponse, this.messageReaders);
}
}

View File

@@ -18,9 +18,8 @@ package org.springframework.web.client.reactive;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.reactive.ClientHttpResponse;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.http.converter.reactive.HttpMessageReader;
/**
* Exception thrown when an HTTP 5xx is received.
@@ -33,13 +32,13 @@ public class WebServerErrorException extends WebClientResponseException {
/**
* Construct a new instance of {@code HttpServerErrorException} based on a
* {@link ClientHttpResponse} and {@link HttpMessageConverter}s to optionally
* {@link ClientHttpResponse} and {@link HttpMessageReader}s to optionally
* help decoding the response body
* @param response the HTTP response
* @param converters the message converters that may decode the HTTP response body
* @param messageReaders the message converters that may decode the HTTP response body
*/
public WebServerErrorException(ClientHttpResponse response, List<HttpMessageConverter<?>> converters) {
super(initMessage(response), response, converters);
public WebServerErrorException(ClientHttpResponse response, List<HttpMessageReader<?>> messageReaders) {
super(initMessage(response), response, messageReaders);
}
private static String initMessage(ClientHttpResponse response) {

View File

@@ -29,7 +29,7 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.reactive.ClientHttpResponse;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.http.converter.reactive.HttpMessageReader;
import org.springframework.web.client.reactive.BodyExtractor;
import org.springframework.web.client.reactive.ResponseExtractor;
import org.springframework.web.client.reactive.WebClientException;
@@ -53,8 +53,8 @@ public class RxJava1ResponseExtractors {
return (clientResponse, webClientConfig) -> (Single<T>) RxJava1Adapter
.publisherToSingle(clientResponse
.doOnNext(response -> webClientConfig.getResponseErrorHandler()
.handleError(response, webClientConfig.getMessageConverters()))
.flatMap(resp -> decodeResponseBodyAsMono(resp, bodyType, webClientConfig.getMessageConverters())));
.handleError(response, webClientConfig.getMessageReaders()))
.flatMap(resp -> decodeResponseBodyAsMono(resp, bodyType, webClientConfig.getMessageReaders())));
}
/**
@@ -95,8 +95,8 @@ public class RxJava1ResponseExtractors {
return (clientResponse, webClientConfig) -> RxJava1Adapter
.publisherToObservable(clientResponse
.doOnNext(response -> webClientConfig.getResponseErrorHandler()
.handleError(response, webClientConfig.getMessageConverters()))
.flatMap(resp -> decodeResponseBody(resp, bodyType, webClientConfig.getMessageConverters())));
.handleError(response, webClientConfig.getMessageReaders()))
.flatMap(resp -> decodeResponseBody(resp, bodyType, webClientConfig.getMessageReaders())));
}
/**
@@ -139,7 +139,7 @@ public class RxJava1ResponseExtractors {
RxJava1Adapter.publisherToSingle(clientResponse
.then(response ->
Mono.when(
decodeResponseBody(response, bodyType, webClientConfig.getMessageConverters()).next(),
decodeResponseBody(response, bodyType, webClientConfig.getMessageReaders()).next(),
Mono.just(response.getHeaders()),
Mono.just(response.getStatusCode())))
.map(tuple ->
@@ -174,7 +174,7 @@ public class RxJava1ResponseExtractors {
return (clientResponse, webClientConfig) -> RxJava1Adapter.publisherToSingle(clientResponse
.map(response -> new ResponseEntity<>(
RxJava1Adapter
.publisherToObservable(decodeResponseBody(response, bodyType, webClientConfig.getMessageConverters())),
.publisherToObservable(decodeResponseBody(response, bodyType, webClientConfig.getMessageReaders())),
response.getHeaders(),
response.getStatusCode())));
}
@@ -189,26 +189,26 @@ public class RxJava1ResponseExtractors {
@SuppressWarnings("unchecked")
protected static <T> Flux<T> decodeResponseBody(ClientHttpResponse response,
ResolvableType responseType, List<HttpMessageConverter<?>> messageConverters) {
ResolvableType responseType, List<HttpMessageReader<?>> messageReaders) {
MediaType contentType = response.getHeaders().getContentType();
HttpMessageConverter<?> converter = resolveConverter(messageConverters, responseType, contentType);
HttpMessageReader<?> converter = resolveMessageReader(messageReaders, responseType, contentType);
return (Flux<T>) converter.read(responseType, response);
}
@SuppressWarnings("unchecked")
protected static <T> Mono<T> decodeResponseBodyAsMono(ClientHttpResponse response,
ResolvableType responseType, List<HttpMessageConverter<?>> messageConverters) {
ResolvableType responseType, List<HttpMessageReader<?>> messageReaders) {
MediaType contentType = response.getHeaders().getContentType();
HttpMessageConverter<?> converter = resolveConverter(messageConverters, responseType, contentType);
HttpMessageReader<?> converter = resolveMessageReader(messageReaders, responseType, contentType);
return (Mono<T>) converter.readMono(responseType, response);
}
protected static HttpMessageConverter<?> resolveConverter(
List<HttpMessageConverter<?>> messageConverters, ResolvableType responseType, MediaType contentType) {
protected static HttpMessageReader<?> resolveMessageReader(List<HttpMessageReader<?>> messageReaders,
ResolvableType responseType, MediaType contentType) {
return messageConverters.stream()
return messageReaders.stream()
.filter(e -> e.canRead(responseType, contentType))
.findFirst()
.orElseThrow(() ->

View File

@@ -1,11 +1,5 @@
package org.springframework.web.client.reactive;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.mockito.BDDMockito.*;
import static org.mockito.Mockito.mock;
import static org.springframework.web.client.reactive.ResponseExtractors.*;
import java.util.Collections;
import java.util.List;
@@ -15,15 +9,21 @@ import reactor.core.publisher.Flux;
import reactor.test.TestSubscriber;
import org.springframework.core.codec.StringDecoder;
import org.springframework.core.codec.StringEncoder;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
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.converter.reactive.CodecHttpMessageConverter;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.http.converter.reactive.DecoderHttpMessageReader;
import org.springframework.http.converter.reactive.HttpMessageReader;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.springframework.web.client.reactive.ResponseExtractors.as;
/**
* Unit tests for {@link DefaultResponseErrorHandler}.
@@ -36,20 +36,20 @@ public class DefaultResponseErrorHandlerTests {
private ClientHttpResponse response;
private List<HttpMessageConverter<?>> messageConverters;
private List<HttpMessageReader<?>> messageReaders;
@Before
public void setUp() throws Exception {
this.errorHandler = new DefaultResponseErrorHandler();
this.response = mock(ClientHttpResponse.class);
this.messageConverters = Collections
.singletonList(new CodecHttpMessageConverter<>(new StringEncoder(), new StringDecoder()));
this.messageReaders = Collections
.singletonList(new DecoderHttpMessageReader<>(new StringDecoder()));
}
@Test
public void noError() throws Exception {
given(this.response.getStatusCode()).willReturn(HttpStatus.OK);
this.errorHandler.handleError(this.response, this.messageConverters);
this.errorHandler.handleError(this.response, this.messageReaders);
}
@Test
@@ -62,7 +62,7 @@ public class DefaultResponseErrorHandlerTests {
given(this.response.getHeaders()).willReturn(headers);
given(this.response.getBody()).willReturn(Flux.just(buffer));
try {
this.errorHandler.handleError(this.response, this.messageConverters);
this.errorHandler.handleError(this.response, this.messageReaders);
fail("expected HttpClientErrorException");
}
catch (WebClientErrorException exc) {
@@ -84,7 +84,7 @@ public class DefaultResponseErrorHandlerTests {
given(this.response.getHeaders()).willReturn(headers);
given(this.response.getBody()).willReturn(Flux.just(buffer));
try {
this.errorHandler.handleError(this.response, this.messageConverters);
this.errorHandler.handleError(this.response, this.messageReaders);
fail("expected HttpServerErrorException");
}
catch (WebServerErrorException exc) {

View File

@@ -1,24 +1,17 @@
package org.springframework.web.client.reactive;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.mockito.BDDMockito.eq;
import static org.mockito.BDDMockito.*;
import static org.mockito.Mockito.mock;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import reactor.core.Exceptions;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.TestSubscriber;
import reactor.core.Exceptions;
import org.springframework.core.codec.StringDecoder;
import org.springframework.core.codec.StringEncoder;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpHeaders;
@@ -27,9 +20,18 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.reactive.ClientHttpResponse;
import org.springframework.http.codec.json.JacksonJsonDecoder;
import org.springframework.http.codec.json.JacksonJsonEncoder;
import org.springframework.http.converter.reactive.CodecHttpMessageConverter;
import org.springframework.http.converter.reactive.HttpMessageConverter;
import org.springframework.http.converter.reactive.DecoderHttpMessageReader;
import org.springframework.http.converter.reactive.HttpMessageReader;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
/**
* Unit tests for {@link ResponseExtractors}.
@@ -42,7 +44,7 @@ public class ResponseExtractorsTests {
private ClientHttpResponse response;
private List<HttpMessageConverter<?>> messageConverters;
private List<HttpMessageReader<?>> messageReaders;
private WebClientConfig webClientConfig;
@@ -53,12 +55,12 @@ public class ResponseExtractorsTests {
this.headers = new HttpHeaders();
this.response = mock(ClientHttpResponse.class);
given(this.response.getHeaders()).willReturn(headers);
this.messageConverters = Arrays.asList(
new CodecHttpMessageConverter<>(new StringEncoder(), new StringDecoder()),
new CodecHttpMessageConverter<>(new JacksonJsonEncoder(), new JacksonJsonDecoder()));
this.messageReaders = Arrays.asList(
new DecoderHttpMessageReader<>(new StringDecoder()),
new DecoderHttpMessageReader<>(new JacksonJsonDecoder()));
this.webClientConfig = mock(WebClientConfig.class);
this.errorHandler = mock(ResponseErrorHandler.class);
given(this.webClientConfig.getMessageConverters()).willReturn(this.messageConverters);
given(this.webClientConfig.getMessageReaders()).willReturn(this.messageReaders);
given(this.webClientConfig.getResponseErrorHandler()).willReturn(this.errorHandler);
}
@@ -182,7 +184,7 @@ public class ResponseExtractorsTests {
.assertValueCount(1)
.assertComplete();
then(this.errorHandler).should().handleError(eq(this.response), eq(this.messageConverters));
then(this.errorHandler).should().handleError(eq(this.response), eq(this.messageReaders));
}