diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Decoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Decoder.java index 3c233eaf2d..0acdbdedb9 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Decoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Decoder.java @@ -18,6 +18,7 @@ package org.springframework.http.codec.json; import java.io.IOException; import java.lang.annotation.Annotation; +import java.util.List; import java.util.Map; import com.fasterxml.jackson.core.JsonFactory; @@ -133,6 +134,13 @@ public abstract class AbstractJackson2Decoder extends Jackson2CodecSupport imple return getHints(actualType); } + @Override + public List getDecodableMimeTypes() { + return getMimeTypes(); + } + + // Jackson2CodecSupport ... + @Override protected A getAnnotation(MethodParameter parameter, Class annotType) { return parameter.getParameterAnnotation(annotType); diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java index 6c28dd1ea5..049a9863ac 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -60,6 +60,8 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple protected final List streamingMediaTypes = new ArrayList<>(1); + protected boolean streamingLineSeparator = true; + /** * Constructor with a Jackson {@link ObjectMapper} to use. @@ -104,7 +106,9 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple else if (this.streamingMediaTypes.stream().anyMatch(mediaType -> mediaType.isCompatibleWith(mimeType))) { return Flux.from(inputStream).map(value -> { DataBuffer buffer = encodeValue(value, mimeType, bufferFactory, elementType, hints); - buffer.write(new byte[]{'\n'}); + if (streamingLineSeparator) { + buffer.write(new byte[]{'\n'}); + } return buffer; }); } @@ -156,6 +160,11 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple // HttpMessageEncoder... + @Override + public List getEncodableMimeTypes() { + return getMimeTypes(); + } + @Override public List getStreamingMediaTypes() { return Collections.unmodifiableList(this.streamingMediaTypes); @@ -168,6 +177,8 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple return (actualType != null ? getHints(actualType) : Collections.emptyMap()); } + // Jackson2CodecSupport ... + @Override protected A getAnnotation(MethodParameter parameter, Class annotType) { return parameter.getMethodAnnotation(annotType); diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonDecoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonDecoder.java index eba19bd743..593ce36932 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonDecoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonDecoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -16,8 +16,6 @@ package org.springframework.http.codec.json; -import java.util.List; - import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; @@ -42,10 +40,4 @@ public class Jackson2JsonDecoder extends AbstractJackson2Decoder { super(mapper, mimeTypes); } - - @Override - public List getDecodableMimeTypes() { - return getMimeTypes(); - } - } diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonEncoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonEncoder.java index 0945430287..b46a943a12 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonEncoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonEncoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -44,7 +44,7 @@ import org.springframework.util.MimeType; * @see Jackson2JsonDecoder */ public class Jackson2JsonEncoder extends AbstractJackson2Encoder { - + @Nullable private final PrettyPrinter ssePrettyPrinter; @@ -76,9 +76,4 @@ public class Jackson2JsonEncoder extends AbstractJackson2Encoder { writer.with(this.ssePrettyPrinter) : writer); } - @Override - public List getEncodableMimeTypes() { - return getMimeTypes(); - } - } diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2SmileDecoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2SmileDecoder.java index d5a4cf1b38..0c116c0287 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2SmileDecoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2SmileDecoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -16,6 +16,8 @@ package org.springframework.http.codec.json; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -38,11 +40,13 @@ import org.springframework.util.MimeType; */ public class Jackson2SmileDecoder extends AbstractJackson2Decoder { - private static final MimeType SMILE_MIME_TYPE = new MediaType("application", "x-jackson-smile"); + private static final MimeType[] DEFAULT_SMILE_MIME_TYPES = new MimeType[] { + new MimeType("application", "x-jackson-smile", StandardCharsets.UTF_8), + new MimeType("application", "*+x-jackson-smile", StandardCharsets.UTF_8)}; public Jackson2SmileDecoder() { - this(Jackson2ObjectMapperBuilder.smile().build(), SMILE_MIME_TYPE); + this(Jackson2ObjectMapperBuilder.smile().build(), DEFAULT_SMILE_MIME_TYPES); } public Jackson2SmileDecoder(ObjectMapper mapper, MimeType... mimeTypes) { @@ -50,9 +54,4 @@ public class Jackson2SmileDecoder extends AbstractJackson2Decoder { Assert.isAssignable(SmileFactory.class, mapper.getFactory().getClass()); } - @Override - public List getDecodableMimeTypes() { - return Collections.singletonList(SMILE_MIME_TYPE); - } - } diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2SmileEncoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2SmileEncoder.java index 1f5658848d..37308f47c9 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2SmileEncoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2SmileEncoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -16,6 +16,7 @@ package org.springframework.http.codec.json; +import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.List; @@ -39,23 +40,20 @@ import org.springframework.util.MimeType; */ public class Jackson2SmileEncoder extends AbstractJackson2Encoder { - private static final MimeType SMILE_MIME_TYPE = new MediaType("application", "x-jackson-smile"); + private static final MimeType[] DEFAULT_SMILE_MIME_TYPES = new MimeType[] { + new MimeType("application", "x-jackson-smile", StandardCharsets.UTF_8), + new MimeType("application", "*+x-jackson-smile", StandardCharsets.UTF_8)}; public Jackson2SmileEncoder() { - this(Jackson2ObjectMapperBuilder.smile().build(), new MediaType("application", "x-jackson-smile")); + this(Jackson2ObjectMapperBuilder.smile().build(), DEFAULT_SMILE_MIME_TYPES); } public Jackson2SmileEncoder(ObjectMapper mapper, MimeType... mimeTypes) { super(mapper, mimeTypes); Assert.isAssignable(SmileFactory.class, mapper.getFactory().getClass()); this.streamingMediaTypes.add(new MediaType("application", "stream+x-jackson-smile")); - } - - - @Override - public List getEncodableMimeTypes() { - return Collections.singletonList(SMILE_MIME_TYPE); + this.streamingLineSeparator = false; } } diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2Tokenizer.java b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2Tokenizer.java index 696b559f4f..15aba7c5c6 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2Tokenizer.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2Tokenizer.java @@ -125,7 +125,9 @@ class Jackson2Tokenizer { while (true) { JsonToken token = this.parser.nextToken(); - if (token == null || token == JsonToken.NOT_AVAILABLE) { + // SPR-16151: Smile data format uses null to separate documents + if ((token == JsonToken.NOT_AVAILABLE) || + (token == null && (token = this.parser.nextToken()) == null)) { break; } updateDepth(token); diff --git a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonDecoderTests.java b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonDecoderTests.java index 544323b99f..8dc4fc70db 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonDecoderTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonDecoderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -52,6 +52,8 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.springframework.core.ResolvableType.forClass; import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8; +import static org.springframework.http.MediaType.APPLICATION_STREAM_JSON; import static org.springframework.http.MediaType.APPLICATION_XML; import static org.springframework.http.codec.json.Jackson2JsonDecoder.JSON_VIEW_HINT; import static org.springframework.http.codec.json.JacksonViewBean.MyJacksonView1; @@ -70,6 +72,8 @@ public class Jackson2JsonDecoderTests extends AbstractDataBufferAllocatingTestCa Jackson2JsonDecoder decoder = new Jackson2JsonDecoder(); assertTrue(decoder.canDecode(forClass(Pojo.class), APPLICATION_JSON)); + assertTrue(decoder.canDecode(forClass(Pojo.class), APPLICATION_JSON_UTF8)); + assertTrue(decoder.canDecode(forClass(Pojo.class), APPLICATION_STREAM_JSON)); assertTrue(decoder.canDecode(forClass(Pojo.class), null)); assertFalse(decoder.canDecode(forClass(String.class), null)); @@ -130,7 +134,7 @@ public class Jackson2JsonDecoderTests extends AbstractDataBufferAllocatingTestCa } @Test - public void decodeToFlux() throws Exception { + public void decodeArrayToFlux() throws Exception { Flux source = Flux.just(stringBuffer( "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]")); @@ -144,6 +148,21 @@ public class Jackson2JsonDecoderTests extends AbstractDataBufferAllocatingTestCa .verifyComplete(); } + @Test + public void decodeStreamToFlux() throws Exception { + Flux source = Flux.just(stringBuffer("{\"bar\":\"b1\",\"foo\":\"f1\"}"), + stringBuffer("{\"bar\":\"b2\",\"foo\":\"f2\"}")); + + ResolvableType elementType = forClass(Pojo.class); + Flux flux = new Jackson2JsonDecoder().decode(source, elementType, APPLICATION_STREAM_JSON, + emptyMap()); + + StepVerifier.create(flux) + .expectNext(new Pojo("f1", "b1")) + .expectNext(new Pojo("f2", "b2")) + .verifyComplete(); + } + @Test public void decodeEmptyArrayToFlux() throws Exception { Flux source = Flux.just(stringBuffer("[]")); diff --git a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonEncoderTests.java b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonEncoderTests.java index 8848412544..de5426460e 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonEncoderTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonEncoderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -57,6 +57,8 @@ public class Jackson2JsonEncoderTests extends AbstractDataBufferAllocatingTestCa public void canEncode() { ResolvableType pojoType = ResolvableType.forClass(Pojo.class); assertTrue(this.encoder.canEncode(pojoType, APPLICATION_JSON)); + assertTrue(this.encoder.canEncode(pojoType, APPLICATION_JSON_UTF8)); + assertTrue(this.encoder.canEncode(pojoType, APPLICATION_STREAM_JSON)); assertTrue(this.encoder.canEncode(pojoType, null)); // SPR-15464 diff --git a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2SmileDecoderTests.java b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2SmileDecoderTests.java index 1a20f91314..6989ae4ee6 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2SmileDecoderTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2SmileDecoderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -38,6 +38,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.springframework.core.ResolvableType.forClass; import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.http.MediaType.APPLICATION_STREAM_JSON; /** * Unit tests for {@link Jackson2SmileDecoder}. @@ -47,12 +48,14 @@ import static org.springframework.http.MediaType.APPLICATION_JSON; public class Jackson2SmileDecoderTests extends AbstractDataBufferAllocatingTestCase { private final static MimeType SMILE_MIME_TYPE = new MimeType("application", "x-jackson-smile"); + private final static MimeType STREAM_SMILE_MIME_TYPE = new MimeType("application", "stream+x-jackson-smile"); private final Jackson2SmileDecoder decoder = new Jackson2SmileDecoder(); @Test public void canDecode() { assertTrue(decoder.canDecode(forClass(Pojo.class), SMILE_MIME_TYPE)); + assertTrue(decoder.canDecode(forClass(Pojo.class), STREAM_SMILE_MIME_TYPE)); assertTrue(decoder.canDecode(forClass(Pojo.class), null)); assertFalse(decoder.canDecode(forClass(String.class), null)); @@ -100,7 +103,7 @@ public class Jackson2SmileDecoderTests extends AbstractDataBufferAllocatingTestC } @Test - public void decodeToFlux() throws Exception { + public void decodeListToFlux() throws Exception { ObjectMapper mapper = Jackson2ObjectMapperBuilder.smile().build(); List list = asList(new Pojo("f1", "b1"), new Pojo("f2", "b2")); byte[] serializedList = mapper.writer().writeValueAsBytes(list); @@ -115,4 +118,20 @@ public class Jackson2SmileDecoderTests extends AbstractDataBufferAllocatingTestC .verifyComplete(); } + @Test + public void decodeStreamToFlux() throws Exception { + ObjectMapper mapper = Jackson2ObjectMapperBuilder.smile().build(); + List list = asList(new Pojo("f1", "b1"), new Pojo("f2", "b2")); + byte[] serializedList = mapper.writer().writeValueAsBytes(list); + Flux source = Flux.just(this.bufferFactory.wrap(serializedList)); + + ResolvableType elementType = forClass(Pojo.class); + Flux flux = decoder.decode(source, elementType, STREAM_SMILE_MIME_TYPE, emptyMap()); + + StepVerifier.create(flux) + .expectNext(new Pojo("f1", "b1")) + .expectNext(new Pojo("f2", "b2")) + .verifyComplete(); + } + } diff --git a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2SmileEncoderTests.java b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2SmileEncoderTests.java index be7cc1351a..9e6959df45 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2SmileEncoderTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2SmileEncoderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -49,6 +49,7 @@ import static org.springframework.http.MediaType.APPLICATION_XML; public class Jackson2SmileEncoderTests extends AbstractDataBufferAllocatingTestCase { private final static MimeType SMILE_MIME_TYPE = new MimeType("application", "x-jackson-smile"); + private final static MimeType STREAM_SMILE_MIME_TYPE = new MimeType("application", "stream+x-jackson-smile"); private final Jackson2SmileEncoder encoder = new Jackson2SmileEncoder(); @@ -57,6 +58,7 @@ public class Jackson2SmileEncoderTests extends AbstractDataBufferAllocatingTestC public void canEncode() { ResolvableType pojoType = ResolvableType.forClass(Pojo.class); assertTrue(this.encoder.canEncode(pojoType, SMILE_MIME_TYPE)); + assertTrue(this.encoder.canEncode(pojoType, STREAM_SMILE_MIME_TYPE)); assertTrue(this.encoder.canEncode(pojoType, null)); // SPR-15464 @@ -96,8 +98,7 @@ public class Jackson2SmileEncoderTests extends AbstractDataBufferAllocatingTestC new Pojo("foofoofoo", "barbarbar") ); ResolvableType type = ResolvableType.forClass(Pojo.class); - MediaType mediaType = new MediaType("application", "stream+x-jackson-smile"); - Flux output = this.encoder.encode(source, this.bufferFactory, type, mediaType, emptyMap()); + Flux output = this.encoder.encode(source, this.bufferFactory, type, STREAM_SMILE_MIME_TYPE, emptyMap()); ObjectMapper mapper = Jackson2ObjectMapperBuilder.smile().build(); StepVerifier.create(output) diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/JsonStreamingIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/JacksonStreamingIntegrationTests.java similarity index 80% rename from spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/JsonStreamingIntegrationTests.java rename to spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/JacksonStreamingIntegrationTests.java index 2f05fd37de..8a8c0ca410 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/JsonStreamingIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/JacksonStreamingIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -26,6 +26,7 @@ import reactor.test.StepVerifier; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; import org.springframework.http.server.reactive.AbstractHttpHandlerIntegrationTests; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.web.bind.annotation.RequestMapping; @@ -41,7 +42,7 @@ import static org.springframework.http.MediaType.APPLICATION_STREAM_JSON_VALUE; /** * @author Sebastien Deleuze */ -public class JsonStreamingIntegrationTests extends AbstractHttpHandlerIntegrationTests { +public class JacksonStreamingIntegrationTests extends AbstractHttpHandlerIntegrationTests { private AnnotationConfigApplicationContext wac; @@ -80,11 +81,26 @@ public class JsonStreamingIntegrationTests extends AbstractHttpHandlerIntegratio .verify(); } + @Test + public void smileStreaming() throws Exception { + Flux result = this.webClient.get() + .uri("/stream") + .accept(new MediaType("application", "stream+x-jackson-smile")) + .exchange() + .flatMapMany(response -> response.bodyToFlux(Person.class)); + + StepVerifier.create(result) + .expectNext(new Person("foo 0")) + .expectNext(new Person("foo 1")) + .thenCancel() + .verify(); + } + @RestController @SuppressWarnings("unused") - static class JsonStreamingController { + static class JacksonStreamingController { - @RequestMapping(value = "/stream", produces = APPLICATION_STREAM_JSON_VALUE) + @RequestMapping(value = "/stream", produces = { APPLICATION_STREAM_JSON_VALUE, "application/stream+x-jackson-smile" }) Flux person() { return Flux.interval(Duration.ofMillis(100)).map(l -> new Person("foo " + l)); } @@ -97,8 +113,8 @@ public class JsonStreamingIntegrationTests extends AbstractHttpHandlerIntegratio static class TestConfiguration { @Bean - public JsonStreamingController jsonStreamingController() { - return new JsonStreamingController(); + public JacksonStreamingController jsonStreamingController() { + return new JacksonStreamingController(); } }