/*
* Copyright 2002-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.reactive.result.method.annotation;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import javax.xml.bind.annotation.XmlRootElement;
import io.reactivex.Flowable;
import io.reactivex.Maybe;
import org.junit.Before;
import org.junit.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import rx.Observable;
import rx.Single;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.Decoder;
import org.springframework.http.MediaType;
import org.springframework.http.codec.DecoderHttpMessageReader;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.codec.json.Jackson2JsonDecoder;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.method.ResolvableMethod;
import org.springframework.web.reactive.BindingContext;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.ServerWebInputException;
import org.springframework.web.server.UnsupportedMediaTypeStatusException;
import static org.junit.Assert.*;
import static org.springframework.core.ResolvableType.*;
import static org.springframework.mock.http.server.reactive.test.MockServerHttpRequest.*;
/**
* Unit tests for {@link AbstractMessageReaderArgumentResolver}.
*
* @author Rossen Stoyanchev
*/
public class MessageReaderArgumentResolverTests {
private AbstractMessageReaderArgumentResolver resolver = resolver(new Jackson2JsonDecoder());
private BindingContext bindingContext;
private ResolvableMethod testMethod = ResolvableMethod.on(getClass()).named("handle").build();
@Before
public void setup() throws Exception {
ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
initializer.setValidator(new TestBeanValidator());
this.bindingContext = new BindingContext(initializer);
}
@Test
public void missingContentType() throws Exception {
ServerWebExchange exchange = post("/path").body("{\"bar\":\"BARBAR\",\"foo\":\"FOOFOO\"}").toExchange();
ResolvableType type = forClassWithGenerics(Mono.class, TestBean.class);
MethodParameter param = this.testMethod.arg(type);
Mono<Object> result = this.resolver.readBody(param, true, this.bindingContext, exchange);
StepVerifier.create(result).expectError(UnsupportedMediaTypeStatusException.class).verify();
}
// More extensive "empty body" tests in RequestBody- and HttpEntityArgumentResolverTests
@Test @SuppressWarnings("unchecked") // SPR-9942
public void emptyBody() throws Exception {
ServerWebExchange exchange = post("/path").contentType(MediaType.APPLICATION_JSON).toExchange();
ResolvableType type = forClassWithGenerics(Mono.class, TestBean.class);
MethodParameter param = this.testMethod.arg(type);
Mono<TestBean> result = (Mono<TestBean>) this.resolver.readBody(
param, true, this.bindingContext, exchange).block();
StepVerifier.create(result).expectError(ServerWebInputException.class).verify();
}
@Test
public void monoTestBean() throws Exception {
String body = "{\"bar\":\"BARBAR\",\"foo\":\"FOOFOO\"}";
ResolvableType type = forClassWithGenerics(Mono.class, TestBean.class);
MethodParameter param = this.testMethod.arg(type);
Mono<Object> mono = resolveValue(param, body);
assertEquals(new TestBean("FOOFOO", "BARBAR"), mono.block());
}
@Test
public void fluxTestBean() throws Exception {
String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]";
ResolvableType type = forClassWithGenerics(Flux.class, TestBean.class);
MethodParameter param = this.testMethod.arg(type);
Flux<TestBean> flux = resolveValue(param, body);
assertEquals(Arrays.asList(new TestBean("f1", "b1"), new TestBean("f2", "b2")),
flux.collectList().block());
}
@Test
public void singleTestBean() throws Exception {
String body = "{\"bar\":\"b1\",\"foo\":\"f1\"}";
ResolvableType type = forClassWithGenerics(Single.class, TestBean.class);
MethodParameter param = this.testMethod.arg(type);
Single<TestBean> single = resolveValue(param, body);
assertEquals(new TestBean("f1", "b1"), single.toBlocking().value());
}
@Test
public void rxJava2SingleTestBean() throws Exception {
String body = "{\"bar\":\"b1\",\"foo\":\"f1\"}";
ResolvableType type = forClassWithGenerics(io.reactivex.Single.class, TestBean.class);
MethodParameter param = this.testMethod.arg(type);
io.reactivex.Single<TestBean> single = resolveValue(param, body);
assertEquals(new TestBean("f1", "b1"), single.blockingGet());
}
@Test
public void rxJava2MaybeTestBean() throws Exception {
String body = "{\"bar\":\"b1\",\"foo\":\"f1\"}";
ResolvableType type = forClassWithGenerics(Maybe.class, TestBean.class);
MethodParameter param = this.testMethod.arg(type);
Maybe<TestBean> maybe = resolveValue(param, body);
assertEquals(new TestBean("f1", "b1"), maybe.blockingGet());
}
@Test
public void observableTestBean() throws Exception {
String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]";
ResolvableType type = forClassWithGenerics(Observable.class, TestBean.class);
MethodParameter param = this.testMethod.arg(type);
Observable<?> observable = resolveValue(param, body);
assertEquals(Arrays.asList(new TestBean("f1", "b1"), new TestBean("f2", "b2")),
observable.toList().toBlocking().first());
}
@Test
public void rxJava2ObservableTestBean() throws Exception {
String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]";
ResolvableType type = forClassWithGenerics(io.reactivex.Observable.class, TestBean.class);
MethodParameter param = this.testMethod.arg(type);
io.reactivex.Observable<?> observable = resolveValue(param, body);
assertEquals(Arrays.asList(new TestBean("f1", "b1"), new TestBean("f2", "b2")),
observable.toList().blockingGet());
}
@Test
public void flowableTestBean() throws Exception {
String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]";
ResolvableType type = forClassWithGenerics(Flowable.class, TestBean.class);
MethodParameter param = this.testMethod.arg(type);
Flowable<?> flowable = resolveValue(param, body);
assertEquals(Arrays.asList(new TestBean("f1", "b1"), new TestBean("f2", "b2")),
flowable.toList().blockingGet());
}
@Test
public void futureTestBean() throws Exception {
String body = "{\"bar\":\"b1\",\"foo\":\"f1\"}";
ResolvableType type = forClassWithGenerics(CompletableFuture.class, TestBean.class);
MethodParameter param = this.testMethod.arg(type);
CompletableFuture<?> future = resolveValue(param, body);
assertEquals(new TestBean("f1", "b1"), future.get());
}
@Test
public void testBean() throws Exception {
String body = "{\"bar\":\"b1\",\"foo\":\"f1\"}";
MethodParameter param = this.testMethod.arg(TestBean.class);
TestBean value = resolveValue(param, body);
assertEquals(new TestBean("f1", "b1"), value);
}
@Test
public void map() throws Exception {
String body = "{\"bar\":\"b1\",\"foo\":\"f1\"}";
Map<String, String> map = new HashMap<>();
map.put("foo", "f1");
map.put("bar", "b1");
ResolvableType type = forClassWithGenerics(Map.class, String.class, String.class);
MethodParameter param = this.testMethod.arg(type);
Map<String, String> actual = resolveValue(param, body);
assertEquals(map, actual);
}
@Test
public void list() throws Exception {
String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]";
ResolvableType type = forClassWithGenerics(List.class, TestBean.class);
MethodParameter param = this.testMethod.arg(type);
List<?> list = resolveValue(param, body);
assertEquals(Arrays.asList(new TestBean("f1", "b1"), new TestBean("f2", "b2")), list);
}
@Test
public void monoList() throws Exception {
String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]";
ResolvableType type = forClassWithGenerics(Mono.class, forClassWithGenerics(List.class, TestBean.class));
MethodParameter param = this.testMethod.arg(type);
Mono<?> mono = resolveValue(param, body);
List<?> list = (List<?>) mono.block(Duration.ofSeconds(5));
assertEquals(Arrays.asList(new TestBean("f1", "b1"), new TestBean("f2", "b2")), list);
}
@Test
public void array() throws Exception {
String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]";
MethodParameter param = this.testMethod.arg(TestBean[].class);
TestBean[] value = resolveValue(param, body);
assertArrayEquals(new TestBean[] {new TestBean("f1", "b1"), new TestBean("f2", "b2")}, value);
}
@Test
@SuppressWarnings("unchecked")
public void validateMonoTestBean() throws Exception {
String body = "{\"bar\":\"b1\"}";
ResolvableType type = forClassWithGenerics(Mono.class, TestBean.class);
MethodParameter param = this.testMethod.arg(type);
Mono<TestBean> mono = resolveValue(param, body);
StepVerifier.create(mono).expectNextCount(0).expectError(ServerWebInputException.class).verify();
}
@Test
@SuppressWarnings("unchecked")
public void validateFluxTestBean() throws Exception {
String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\"}]";
ResolvableType type = forClassWithGenerics(Flux.class, TestBean.class);
MethodParameter param = this.testMethod.arg(type);
Flux<TestBean> flux = resolveValue(param, body);
StepVerifier.create(flux)
.expectNext(new TestBean("f1", "b1"))
.expectError(ServerWebInputException.class)
.verify();
}
@Test // SPR-9964
public void parameterizedMethodArgument() throws Exception {
Method method = AbstractParameterizedController.class.getMethod("handleDto", Identifiable.class);
HandlerMethod handlerMethod = new HandlerMethod(new ConcreteParameterizedController(), method);
MethodParameter methodParam = handlerMethod.getMethodParameters()[0];
SimpleBean simpleBean = resolveValue(methodParam, "{\"name\" : \"Jad\"}");
assertEquals("Jad", simpleBean.getName());
}
@SuppressWarnings("unchecked")
private <T> T resolveValue(MethodParameter param, String body) {
ServerWebExchange exchange = post("/path").contentType(MediaType.APPLICATION_JSON).body(body).toExchange();
Mono<Object> result = this.resolver.readBody(param, true, this.bindingContext, exchange);
Object value = result.block(Duration.ofSeconds(5));
assertNotNull(value);
assertTrue("Unexpected return value type: " + value,
param.getParameterType().isAssignableFrom(value.getClass()));
return (T) value;
}
@SuppressWarnings("Convert2MethodRef")
private AbstractMessageReaderArgumentResolver resolver(Decoder<?>... decoders) {
List<HttpMessageReader<?>> readers = new ArrayList<>();
Arrays.asList(decoders).forEach(decoder -> readers.add(new DecoderHttpMessageReader<>(decoder)));
return new AbstractMessageReaderArgumentResolver(readers) {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return false;
}
@Override
public Mono<Object> resolveArgument(MethodParameter parameter, BindingContext bindingContext, ServerWebExchange exchange) {
return null;
}
};
}
@SuppressWarnings("unused")
private void handle(
@Validated Mono<TestBean> monoTestBean,
@Validated Flux<TestBean> fluxTestBean,
Single<TestBean> singleTestBean,
io.reactivex.Single<TestBean> rxJava2SingleTestBean,
Maybe<TestBean> rxJava2MaybeTestBean,
Observable<TestBean> observableTestBean,
io.reactivex.Observable<TestBean> rxJava2ObservableTestBean,
Flowable<TestBean> flowableTestBean,
CompletableFuture<TestBean> futureTestBean,
TestBean testBean,
Map<String, String> map,
List<TestBean> list,
Mono<List<TestBean>> monoList,
Set<TestBean> set,
TestBean[] array) {
}
@XmlRootElement
@SuppressWarnings("unused")
private static class TestBean {
private String foo;
private String bar;
public TestBean() {
}
TestBean(String foo, String bar) {
this.foo = foo;
this.bar = bar;
}
public String getFoo() {
return this.foo;
}
public void setFoo(String foo) {
this.foo = foo;
}
public String getBar() {
return this.bar;
}
public void setBar(String bar) {
this.bar = bar;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o instanceof TestBean) {
TestBean other = (TestBean) o;
return this.foo.equals(other.foo) && this.bar.equals(other.bar);
}
return false;
}
@Override
public int hashCode() {
return 31 * foo.hashCode() + bar.hashCode();
}
@Override
public String toString() {
return "TestBean[foo='" + this.foo + "\'" + ", bar='" + this.bar + "\']";
}
}
private static class TestBeanValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return clazz.equals(TestBean.class);
}
@Override
public void validate(Object target, Errors errors) {
TestBean testBean = (TestBean) target;
if (testBean.getFoo() == null) {
errors.rejectValue("foo", "nullValue");
}
}
}
private static abstract class AbstractParameterizedController<DTO extends Identifiable> {
@SuppressWarnings("unused")
public void handleDto(DTO dto) {}
}
private static class ConcreteParameterizedController extends AbstractParameterizedController<SimpleBean> {
}
private interface Identifiable extends Serializable {
Long getId();
void setId(Long id);
}
@SuppressWarnings({"serial", "unused"})
private static class SimpleBean implements Identifiable {
private Long id;
private String name;
@Override
public Long getId() {
return id;
}
@Override
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}