diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageWriterResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageWriterResultHandlerTests.java index 535b847ed3..2fac801f30 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageWriterResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageWriterResultHandlerTests.java @@ -148,7 +148,6 @@ public class MessageWriterResultHandlerTests { } @Test // SPR-13318 - @Ignore public void jacksonTypeWithSubType() throws Exception { SimpleBean body = new SimpleBean(123L, "foo"); ResolvableType type = ResolvableType.forClass(Identifiable.class); @@ -159,7 +158,6 @@ public class MessageWriterResultHandlerTests { } @Test // SPR-13318 - @Ignore public void jacksonTypeWithSubTypeOfListElement() throws Exception { List body = Arrays.asList(new SimpleBean(123L, "foo"), new SimpleBean(456L, "bar")); ResolvableType type = ResolvableType.forClassWithGenerics(List.class, Identifiable.class); diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJacksonJsonCodec.java b/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJacksonJsonCodec.java new file mode 100644 index 0000000000..d76f97f8a0 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJacksonJsonCodec.java @@ -0,0 +1,127 @@ +/* + * 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.json; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; + +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.type.TypeFactory; + +import org.springframework.core.ResolvableType; +import org.springframework.util.MimeType; + +/** + * @author Sebastien Deleuze + */ +public class AbstractJacksonJsonCodec { + + protected static final List JSON_MIME_TYPES = Arrays.asList( + new MimeType("application", "json", StandardCharsets.UTF_8), + new MimeType("application", "*+json", StandardCharsets.UTF_8)); + + + protected final ObjectMapper mapper; + + protected AbstractJacksonJsonCodec(ObjectMapper mapper) { + this.mapper = mapper; + } + + /** + * Return the Jackson {@link JavaType} for the specified type and context class. + *

The default implementation returns {@code typeFactory.constructType(type, contextClass)}, + * but this can be overridden in subclasses, to allow for custom generic collection handling. + * For instance: + *

+	 * protected JavaType getJavaType(Type type) {
+	 *   if (type instanceof Class && List.class.isAssignableFrom((Class)type)) {
+	 *     return TypeFactory.collectionType(ArrayList.class, MyBean.class);
+	 *   } else {
+	 *     return super.getJavaType(type);
+	 *   }
+	 * }
+	 * 
+ * @param type the generic type to return the Jackson JavaType for + * @param contextClass a context class for the target type, for example a class + * in which the target type appears in a method signature (can be {@code null}) + * @return the Jackson JavaType + */ + protected JavaType getJavaType(Type type, Class contextClass) { + TypeFactory typeFactory = this.mapper.getTypeFactory(); + if (contextClass != null) { + ResolvableType resolvedType = ResolvableType.forType(type); + if (type instanceof TypeVariable) { + ResolvableType resolvedTypeVariable = resolveVariable( + (TypeVariable) type, ResolvableType.forClass(contextClass)); + if (resolvedTypeVariable != ResolvableType.NONE) { + return typeFactory.constructType(resolvedTypeVariable.resolve()); + } + } + else if (type instanceof ParameterizedType && resolvedType.hasUnresolvableGenerics()) { + ParameterizedType parameterizedType = (ParameterizedType) type; + Class[] generics = new Class[parameterizedType.getActualTypeArguments().length]; + Type[] typeArguments = parameterizedType.getActualTypeArguments(); + for (int i = 0; i < typeArguments.length; i++) { + Type typeArgument = typeArguments[i]; + if (typeArgument instanceof TypeVariable) { + ResolvableType resolvedTypeArgument = resolveVariable( + (TypeVariable) typeArgument, ResolvableType.forClass(contextClass)); + if (resolvedTypeArgument != ResolvableType.NONE) { + generics[i] = resolvedTypeArgument.resolve(); + } + else { + generics[i] = ResolvableType.forType(typeArgument).resolve(); + } + } + else { + generics[i] = ResolvableType.forType(typeArgument).resolve(); + } + } + return typeFactory.constructType(ResolvableType. + forClassWithGenerics(resolvedType.getRawClass(), generics).getType()); + } + } + return typeFactory.constructType(type); + } + + private ResolvableType resolveVariable(TypeVariable typeVariable, ResolvableType contextType) { + ResolvableType resolvedType; + if (contextType.hasGenerics()) { + resolvedType = ResolvableType.forType(typeVariable, contextType); + if (resolvedType.resolve() != null) { + return resolvedType; + } + } + resolvedType = resolveVariable(typeVariable, contextType.getSuperType()); + if (resolvedType.resolve() != null) { + return resolvedType; + } + for (ResolvableType ifc : contextType.getInterfaces()) { + resolvedType = resolveVariable(typeVariable, ifc); + if (resolvedType.resolve() != null) { + return resolvedType; + } + } + return ResolvableType.NONE; + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/JacksonJsonEncoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/JacksonJsonEncoder.java index e1a6cb3aaf..f3a08f8809 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/JacksonJsonEncoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/JacksonJsonEncoder.java @@ -19,8 +19,9 @@ package org.springframework.http.codec.json; import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; +import java.util.List; +import com.fasterxml.jackson.annotation.JsonView; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; @@ -29,11 +30,13 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.core.codec.CodecException; -import org.springframework.core.codec.AbstractEncoder; +import org.springframework.core.codec.Encoder; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; import org.springframework.util.Assert; import org.springframework.util.MimeType; @@ -45,7 +48,7 @@ import org.springframework.util.MimeType; * @since 5.0 * @see JacksonJsonDecoder */ -public class JacksonJsonEncoder extends AbstractEncoder { +public class JacksonJsonEncoder extends AbstractJacksonJsonCodec implements Encoder { private static final ByteBuffer START_ARRAY_BUFFER = ByteBuffer.wrap(new byte[]{'['}); @@ -54,21 +57,26 @@ public class JacksonJsonEncoder extends AbstractEncoder { private static final ByteBuffer END_ARRAY_BUFFER = ByteBuffer.wrap(new byte[]{']'}); - private final ObjectMapper mapper; - - public JacksonJsonEncoder() { - this(new ObjectMapper()); + super(Jackson2ObjectMapperBuilder.json().build()); } public JacksonJsonEncoder(ObjectMapper mapper) { - super(new MimeType("application", "json", StandardCharsets.UTF_8), - new MimeType("application", "*+json", StandardCharsets.UTF_8)); - Assert.notNull(mapper, "'mapper' must not be null"); + super(mapper); + } - this.mapper = mapper; + @Override + public boolean canEncode(ResolvableType elementType, MimeType mimeType, Object... hints) { + if (mimeType == null) { + return true; + } + return JSON_MIME_TYPES.stream().anyMatch(m -> m.isCompatibleWith(mimeType)); } + @Override + public List getEncodableMimeTypes() { + return JSON_MIME_TYPES; + } @Override public Flux encode(Publisher inputStream, DataBufferFactory bufferFactory, @@ -97,7 +105,29 @@ public class JacksonJsonEncoder extends AbstractEncoder { private DataBuffer encodeValue(Object value, DataBufferFactory bufferFactory, ResolvableType type) { TypeFactory typeFactory = this.mapper.getTypeFactory(); JavaType javaType = typeFactory.constructType(type.getType()); - ObjectWriter writer = this.mapper.writerFor(javaType); + MethodParameter returnType = (type.getSource() instanceof MethodParameter ? + (MethodParameter)type.getSource() : null); + + if (type != null && value != null && type.isAssignableFrom(value.getClass())) { + javaType = getJavaType(type.getType(), null); + } + ObjectWriter writer; + + if (returnType != null && returnType.getMethodAnnotation(JsonView.class) != null) { + JsonView annotation = returnType.getMethodAnnotation(JsonView.class); + Class[] classes = annotation.value(); + if (classes.length != 1) { + throw new IllegalArgumentException( + "@JsonView only supported for response body advice with exactly 1 class argument: " + returnType); + } + writer = this.mapper.writerWithView(classes[0]); + } + else { + writer = this.mapper.writer(); + } + if (javaType != null && javaType.isContainerType()) { + writer = writer.forType(javaType); + } DataBuffer buffer = bufferFactory.allocateBuffer(); OutputStream outputStream = buffer.asOutputStream(); diff --git a/spring-web/src/test/java/org/springframework/http/codec/json/JacksonJsonEncoderTests.java b/spring-web/src/test/java/org/springframework/http/codec/json/JacksonJsonEncoderTests.java index 5b6fdec1c4..899603be73 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/json/JacksonJsonEncoderTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/json/JacksonJsonEncoderTests.java @@ -18,9 +18,11 @@ package org.springframework.http.codec.json; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.annotation.JsonView; import org.junit.Before; import org.junit.Test; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import reactor.test.TestSubscriber; import org.springframework.core.ResolvableType; @@ -92,6 +94,22 @@ public class JacksonJsonEncoderTests extends AbstractDataBufferAllocatingTestCas stringConsumer("]")); } + @Test + public void jsonView() throws Exception { + JacksonViewBean bean = new JacksonViewBean(); + bean.setWithView1("with"); + bean.setWithView2("with"); + bean.setWithoutView("without"); + + ResolvableType type = ResolvableType.forMethodReturnType(JacksonController.class.getMethod("foo")); + Flux output = this.encoder.encode(Mono.just(bean), this.bufferFactory, type, null); + + TestSubscriber.subscribe(output) + .assertComplete() + .assertNoError() + .assertValuesWith(stringConsumer("{\"withView1\":\"with\"}")); + } + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") private static class ParentClass { @@ -105,4 +123,52 @@ public class JacksonJsonEncoderTests extends AbstractDataBufferAllocatingTestCas private static class Bar extends ParentClass { } + 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 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; + } + } + + private static class JacksonController { + + @JsonView(MyJacksonView1.class) + public JacksonViewBean foo() { + return null; + } + } + }