diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java index 2d2314010a0..d1feb9f3758 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java @@ -60,6 +60,8 @@ import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuild import org.springframework.web.reactive.handler.AbstractHandlerMapping; import org.springframework.web.reactive.result.SimpleHandlerAdapter; import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver; +import org.springframework.http.codec.Jackson2ServerHttpMessageReader; +import org.springframework.http.codec.Jackson2ServerHttpMessageWriter; import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.reactive.result.method.annotation.ResponseBodyResultHandler; @@ -286,7 +288,7 @@ public class WebReactiveConfiguration implements ApplicationContextAware { readers.add(new DecoderHttpMessageReader<>(new Jaxb2XmlDecoder())); } if (jackson2Present) { - readers.add(new DecoderHttpMessageReader<>(new Jackson2JsonDecoder())); + readers.add(new Jackson2ServerHttpMessageReader(new DecoderHttpMessageReader<>(new Jackson2JsonDecoder()))); } } @@ -404,10 +406,10 @@ public class WebReactiveConfiguration implements ApplicationContextAware { } if (jackson2Present) { Jackson2JsonEncoder jacksonEncoder = new Jackson2JsonEncoder(); - writers.add(new EncoderHttpMessageWriter<>(jacksonEncoder)); + writers.add(new Jackson2ServerHttpMessageWriter(new EncoderHttpMessageWriter<>(jacksonEncoder))); sseDataEncoders.add(jacksonEncoder); } - writers.add(new ServerSentEventHttpMessageWriter(sseDataEncoders)); + writers.add(new Jackson2ServerHttpMessageWriter(new ServerSentEventHttpMessageWriter(sseDataEncoders))); } /** * Override this to modify the list of message writers after it has been diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/JacksonHintsIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/JacksonHintsIntegrationTests.java new file mode 100644 index 00000000000..b3c62b5b7aa --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/JacksonHintsIntegrationTests.java @@ -0,0 +1,190 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.reactive.result.method.annotation; + +import java.util.Arrays; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonView; +import static org.junit.Assert.assertEquals; +import org.junit.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.reactive.config.WebReactiveConfiguration; + +/** + * @author Sebastien Deleuze + */ +public class JacksonHintsIntegrationTests extends AbstractRequestMappingIntegrationTests { + + @Override + protected ApplicationContext initApplicationContext() { + AnnotationConfigApplicationContext wac = new AnnotationConfigApplicationContext(); + wac.register(WebConfig.class); + wac.refresh(); + return wac; + } + + @Test + public void jsonViewResponse() throws Exception { + String expected = "{\"withView1\":\"with\"}"; + assertEquals(expected, performGet("/response/raw", MediaType.APPLICATION_JSON_UTF8, String.class).getBody()); + } + + @Test + public void jsonViewWithMonoResponse() throws Exception { + String expected = "{\"withView1\":\"with\"}"; + assertEquals(expected, performGet("/response/mono", MediaType.APPLICATION_JSON_UTF8, String.class).getBody()); + } + + @Test + public void jsonViewWithFluxResponse() throws Exception { + String expected = "[{\"withView1\":\"with\"},{\"withView1\":\"with\"}]"; + assertEquals(expected, performGet("/response/flux", MediaType.APPLICATION_JSON_UTF8, String.class).getBody()); + } + + @Test + public void jsonViewWithRequest() throws Exception { + String expected = "{\"withView1\":\"with\",\"withView2\":null,\"withoutView\":null}"; + assertEquals(expected, performPost("/request/raw", MediaType.APPLICATION_JSON, + new JacksonViewBean("with", "with", "without"), MediaType.APPLICATION_JSON_UTF8, String.class).getBody()); + } + + @Test + public void jsonViewWithMonoRequest() throws Exception { + String expected = "{\"withView1\":\"with\",\"withView2\":null,\"withoutView\":null}"; + assertEquals(expected, performPost("/request/mono", MediaType.APPLICATION_JSON, + new JacksonViewBean("with", "with", "without"), MediaType.APPLICATION_JSON_UTF8, String.class).getBody()); + } + + @Test + public void jsonViewWithFluxRequest() throws Exception { + String expected = "[{\"withView1\":\"with\",\"withView2\":null,\"withoutView\":null}," + + "{\"withView1\":\"with\",\"withView2\":null,\"withoutView\":null}]"; + List beans = Arrays.asList(new JacksonViewBean("with", "with", "without"), new JacksonViewBean("with", "with", "without")); + assertEquals(expected, performPost("/request/flux", MediaType.APPLICATION_JSON, beans, + MediaType.APPLICATION_JSON_UTF8, String.class).getBody()); + } + + + @Configuration + @ComponentScan(resourcePattern = "**/JacksonHintsIntegrationTests*.class") + @SuppressWarnings({"unused", "WeakerAccess"}) + static class WebConfig extends WebReactiveConfiguration { + } + + @RestController + @SuppressWarnings("unused") + private static class JsonViewRestController { + + @GetMapping("/response/raw") + @JsonView(MyJacksonView1.class) + public JacksonViewBean rawResponse() { + return new JacksonViewBean("with", "with", "without"); + } + + @GetMapping("/response/mono") + @JsonView(MyJacksonView1.class) + public Mono monoResponse() { + return Mono.just(new JacksonViewBean("with", "with", "without")); + } + + @GetMapping("/response/flux") + @JsonView(MyJacksonView1.class) + public Flux fluxResponse() { + return Flux.just(new JacksonViewBean("with", "with", "without"), new JacksonViewBean("with", "with", "without")); + } + + @PostMapping("/request/raw") + public JacksonViewBean rawRequest(@JsonView(MyJacksonView1.class) @RequestBody JacksonViewBean bean) { + return bean; + } + + @PostMapping("/request/mono") + public Mono monoRequest(@JsonView(MyJacksonView1.class) @RequestBody Mono mono) { + return mono; + } + + @PostMapping("/request/flux") + public Flux fluxRequest(@JsonView(MyJacksonView1.class) @RequestBody Flux flux) { + return flux; + } + + } + + private interface MyJacksonView1 {} + + private interface MyJacksonView2 {} + + + @SuppressWarnings("unused") + private static class JacksonViewBean { + + @JsonView(MyJacksonView1.class) + private String withView1; + + @JsonView(MyJacksonView2.class) + private String withView2; + + private String withoutView; + + + public JacksonViewBean() { + } + + public JacksonViewBean(String withView1, String withView2, String withoutView) { + this.withView1 = withView1; + this.withView2 = withView2; + this.withoutView = withoutView; + } + + public String getWithView1() { + return withView1; + } + + public void setWithView1(String withView1) { + this.withView1 = withView1; + } + + public String getWithView2() { + return withView2; + } + + public void setWithView2(String withView2) { + this.withView2 = withView2; + } + + public String getWithoutView() { + return withoutView; + } + + public void setWithoutView(String withoutView) { + this.withoutView = withoutView; + } + } + +} \ No newline at end of file diff --git a/spring-web/src/main/java/org/springframework/http/codec/Jackson2ServerHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/Jackson2ServerHttpMessageReader.java new file mode 100644 index 00000000000..0e3b39fac3f --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/codec/Jackson2ServerHttpMessageReader.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.codec; + +import java.util.Collections; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonView; + +import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; +import org.springframework.http.MediaType; +import org.springframework.http.codec.json.AbstractJackson2Codec; +import org.springframework.http.server.reactive.ServerHttpRequest; + +/** + * {@link ServerHttpMessageReader} that resolves those annotation or request based Jackson 2 hints: + *
    + *
  • {@code @JsonView} + {@code @RequestBody} annotated handler method parameter
  • + *
+ * + * @author Sebastien Deleuze + * @since 5.0 + * @see com.fasterxml.jackson.annotation.JsonView + */ +public class Jackson2ServerHttpMessageReader extends AbstractServerHttpMessageReader { + + public Jackson2ServerHttpMessageReader(HttpMessageReader reader) { + super(reader); + } + + @Override + protected Map resolveReadHintsInternal(ResolvableType streamType, + ResolvableType elementType, MediaType mediaType, ServerHttpRequest request) { + + Object source = streamType.getSource(); + MethodParameter parameter = (source instanceof MethodParameter ? (MethodParameter)source : null); + if (parameter != null) { + JsonView annotation = parameter.getParameterAnnotation(JsonView.class); + if (annotation != null) { + Class[] classes = annotation.value(); + if (classes.length != 1) { + throw new IllegalArgumentException( + "@JsonView only supported for read hints with exactly 1 class argument: " + parameter); + } + return Collections.singletonMap(AbstractJackson2Codec.JSON_VIEW_HINT, classes[0]); + } + } + return Collections.emptyMap(); + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/codec/Jackson2ServerHttpMessageWriter.java b/spring-web/src/main/java/org/springframework/http/codec/Jackson2ServerHttpMessageWriter.java new file mode 100644 index 00000000000..2caead5bc45 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/codec/Jackson2ServerHttpMessageWriter.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.codec; + +import java.util.Collections; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonView; + +import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; +import org.springframework.http.MediaType; +import org.springframework.http.codec.json.AbstractJackson2Codec; +import org.springframework.http.server.reactive.ServerHttpRequest; + +/** + * {@link ServerHttpMessageWriter} that resolves those annotation or request based Jackson 2 hints: + *
    + *
  • {@code @JsonView} annotated handler method
  • + *
+ * + * @author Sebastien Deleuze + * @since 5.0 + * @see com.fasterxml.jackson.annotation.JsonView + */ +public class Jackson2ServerHttpMessageWriter extends AbstractServerHttpMessageWriter { + + public Jackson2ServerHttpMessageWriter(HttpMessageWriter writer) { + super(writer); + } + + @Override + protected Map resolveWriteHintsInternal(ResolvableType streamType, + ResolvableType elementType, MediaType mediaType, ServerHttpRequest request) { + + Object source = streamType.getSource(); + MethodParameter returnValue = (source instanceof MethodParameter ? (MethodParameter)source : null); + if (returnValue != null) { + JsonView annotation = returnValue.getMethodAnnotation(JsonView.class); + if (annotation != null) { + Class[] classes = annotation.value(); + if (classes.length != 1) { + throw new IllegalArgumentException( + "@JsonView only supported for write hints with exactly 1 class argument: " + returnValue); + } + return Collections.singletonMap(AbstractJackson2Codec.JSON_VIEW_HINT, classes[0]); + } + } + return Collections.emptyMap(); + } + +}