/*
* 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.ByteArrayOutputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
import io.reactivex.Flowable;
import org.junit.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import rx.Completable;
import rx.Observable;
import org.springframework.core.MethodParameter;
import org.springframework.core.codec.ByteBufferEncoder;
import org.springframework.core.codec.CharSequenceEncoder;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.codec.EncoderHttpMessageWriter;
import org.springframework.http.codec.ResourceHttpMessageWriter;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.http.codec.json.Jackson2JsonEncoder;
import org.springframework.http.codec.xml.Jaxb2XmlEncoder;
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
import org.springframework.mock.http.server.reactive.test.MockServerWebExchange;
import org.springframework.util.ObjectUtils;
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.springframework.core.io.buffer.support.DataBufferTestUtils.dumpString;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8;
import static org.springframework.web.method.ResolvableMethod.on;
import static org.springframework.web.reactive.HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE;
/**
* Unit tests for {@link AbstractMessageWriterResultHandler}.
* @author Rossen Stoyanchev
*/
public class MessageWriterResultHandlerTests {
private final AbstractMessageWriterResultHandler resultHandler = initResultHandler();
private final MockServerWebExchange exchange = MockServerHttpRequest.get("/path").toExchange();
private AbstractMessageWriterResultHandler initResultHandler(HttpMessageWriter<?>... writers) {
List<HttpMessageWriter<?>> writerList;
if (ObjectUtils.isEmpty(writers)) {
writerList = new ArrayList<>();
writerList.add(new EncoderHttpMessageWriter<>(new ByteBufferEncoder()));
writerList.add(new EncoderHttpMessageWriter<>(CharSequenceEncoder.allMimeTypes()));
writerList.add(new ResourceHttpMessageWriter());
writerList.add(new EncoderHttpMessageWriter<>(new Jaxb2XmlEncoder()));
writerList.add(new EncoderHttpMessageWriter<>(new Jackson2JsonEncoder()));
}
else {
writerList = Arrays.asList(writers);
}
RequestedContentTypeResolver resolver = new RequestedContentTypeResolverBuilder().build();
return new AbstractMessageWriterResultHandler(writerList, resolver) {};
}
@Test // SPR-12894
public void useDefaultContentType() throws Exception {
Resource body = new ClassPathResource("logo.png", getClass());
MethodParameter type = on(TestController.class).resolveReturnType(Resource.class);
this.resultHandler.writeBody(body, type, this.exchange).block(Duration.ofSeconds(5));
assertEquals("image/png", this.exchange.getResponse().getHeaders().getFirst("Content-Type"));
}
@Test // SPR-13631
public void useDefaultCharset() throws Exception {
this.exchange.getAttributes().put(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE,
Collections.singleton(APPLICATION_JSON));
String body = "foo";
MethodParameter type = on(TestController.class).resolveReturnType(String.class);
this.resultHandler.writeBody(body, type, this.exchange).block(Duration.ofSeconds(5));
assertEquals(APPLICATION_JSON_UTF8, this.exchange.getResponse().getHeaders().getContentType());
}
@Test
public void voidReturnType() throws Exception {
testVoid(null, on(TestController.class).resolveReturnType(void.class));
testVoid(Mono.empty(), on(TestController.class).resolveReturnType(Mono.class, Void.class));
testVoid(Flux.empty(), on(TestController.class).resolveReturnType(Flux.class, Void.class));
testVoid(Completable.complete(), on(TestController.class).resolveReturnType(Completable.class));
testVoid(Observable.empty(), on(TestController.class).resolveReturnType(Observable.class, Void.class));
MethodParameter type = on(TestController.class).resolveReturnType(io.reactivex.Completable.class);
testVoid(io.reactivex.Completable.complete(), type);
type = on(TestController.class).resolveReturnType(io.reactivex.Observable.class, Void.class);
testVoid(io.reactivex.Observable.empty(), type);
type = on(TestController.class).resolveReturnType(Flowable.class, Void.class);
testVoid(Flowable.empty(), type);
}
private void testVoid(Object body, MethodParameter returnType) {
this.resultHandler.writeBody(body, returnType, this.exchange).block(Duration.ofSeconds(5));
assertNull(this.exchange.getResponse().getHeaders().get("Content-Type"));
StepVerifier.create(this.exchange.getResponse().getBody())
.expectErrorMatches(ex -> ex.getMessage().startsWith("The body is not set.")).verify();
}
@Test // SPR-13135
public void unsupportedReturnType() throws Exception {
ByteArrayOutputStream body = new ByteArrayOutputStream();
MethodParameter type = on(TestController.class).resolveReturnType(OutputStream.class);
HttpMessageWriter<?> writer = new EncoderHttpMessageWriter<>(new ByteBufferEncoder());
Mono<Void> mono = initResultHandler(writer).writeBody(body, type, this.exchange);
StepVerifier.create(mono).expectError(IllegalStateException.class).verify();
}
@Test // SPR-12811
public void jacksonTypeOfListElement() throws Exception {
MethodParameter returnType = on(TestController.class).resolveReturnType(List.class, ParentClass.class);
List<ParentClass> body = Arrays.asList(new Foo("foo"), new Bar("bar"));
this.resultHandler.writeBody(body, returnType, this.exchange).block(Duration.ofSeconds(5));
assertEquals(APPLICATION_JSON_UTF8, this.exchange.getResponse().getHeaders().getContentType());
assertResponseBody("[{\"type\":\"foo\",\"parentProperty\":\"foo\"}," +
"{\"type\":\"bar\",\"parentProperty\":\"bar\"}]");
}
@Test // SPR-13318
public void jacksonTypeWithSubType() throws Exception {
SimpleBean body = new SimpleBean(123L, "foo");
MethodParameter type = on(TestController.class).resolveReturnType(Identifiable.class);
this.resultHandler.writeBody(body, type, this.exchange).block(Duration.ofSeconds(5));
assertEquals(APPLICATION_JSON_UTF8, this.exchange.getResponse().getHeaders().getContentType());
assertResponseBody("{\"id\":123,\"name\":\"foo\"}");
}
@Test // SPR-13318
public void jacksonTypeWithSubTypeOfListElement() throws Exception {
MethodParameter returnType = on(TestController.class).resolveReturnType(List.class, Identifiable.class);
List<SimpleBean> body = Arrays.asList(new SimpleBean(123L, "foo"), new SimpleBean(456L, "bar"));
this.resultHandler.writeBody(body, returnType, this.exchange).block(Duration.ofSeconds(5));
assertEquals(APPLICATION_JSON_UTF8, this.exchange.getResponse().getHeaders().getContentType());
assertResponseBody("[{\"id\":123,\"name\":\"foo\"},{\"id\":456,\"name\":\"bar\"}]");
}
private void assertResponseBody(String responseBody) {
StepVerifier.create(this.exchange.getResponse().getBody())
.consumeNextWith(buf -> assertEquals(responseBody, dumpString(buf, StandardCharsets.UTF_8)))
.expectComplete()
.verify();
}
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
@SuppressWarnings("unused")
private static class ParentClass {
private String parentProperty;
public ParentClass() {
}
ParentClass(String parentProperty) {
this.parentProperty = parentProperty;
}
public String getParentProperty() {
return parentProperty;
}
public void setParentProperty(String parentProperty) {
this.parentProperty = parentProperty;
}
}
@JsonTypeName("foo")
private static class Foo extends ParentClass {
public Foo(String parentProperty) {
super(parentProperty);
}
}
@JsonTypeName("bar")
private static class Bar extends ParentClass {
Bar(String parentProperty) {
super(parentProperty);
}
}
private interface Identifiable extends Serializable {
@SuppressWarnings("unused")
Long getId();
}
@SuppressWarnings({ "serial" })
private static class SimpleBean implements Identifiable {
private Long id;
private String name;
SimpleBean(Long id, String name) {
this.id = id;
this.name = name;
}
@Override
public Long getId() {
return id;
}
@SuppressWarnings("unused")
public String getName() {
return name;
}
}
@SuppressWarnings("unused")
private static class TestController {
Resource resource() { return null; }
String string() { return null; }
void voidReturn() { }
Mono<Void> monoVoid() { return null; }
Completable completable() { return null; }
io.reactivex.Completable rxJava2Completable() { return null; }
Flux<Void> fluxVoid() { return null; }
Observable<Void> observableVoid() { return null; }
io.reactivex.Observable<Void> rxJava2ObservableVoid() { return null; }
Flowable<Void> flowableVoid() { return null; }
OutputStream outputStream() { return null; }
List<ParentClass> listParentClass() { return null; }
Identifiable identifiable() { return null; }
List<Identifiable> listIdentifiable() { return null; }
}
}