Add NDJSON and deprecate application/stream+json

Closes gh-21283
This commit is contained in:
Rossen Stoyanchev
2020-07-28 17:53:12 +03:00
parent 354635ede0
commit 683cc2eb7f
17 changed files with 144 additions and 46 deletions

View File

@@ -88,7 +88,7 @@ public class MediaTypeBenchmark {
"application/problem+json",
"application/xhtml+xml",
"application/rss+xml",
"application/stream+json",
"application/x-ndjson",
"application/xml;q=0.9",
"application/atom+xml",
"application/cbor",

View File

@@ -216,16 +216,36 @@ public class MediaType extends MimeType implements Serializable {
*/
public static final String APPLICATION_RSS_XML_VALUE = "application/rss+xml";
/**
* Public constant media type for {@code application/x-ndjson}.
* @since 5.3
*/
public static final MediaType APPLICATION_NDJSON;
/**
* A String equivalent of {@link MediaType#APPLICATION_NDJSON}.
* @since 5.3
*/
public static final String APPLICATION_NDJSON_VALUE = "application/x-ndjson";
/**
* Public constant media type for {@code application/stream+json}.
* @deprecated as of 5.3, see notice on {@link #APPLICATION_STREAM_JSON_VALUE}.
* @since 5.0
*/
@Deprecated
public static final MediaType APPLICATION_STREAM_JSON;
/**
* A String equivalent of {@link MediaType#APPLICATION_STREAM_JSON}.
* @deprecated as of 5.3 since it originates from the W3C Activity Streams
* specification which has a more specific purpose and has been since
* replaced with a different mime type. Use {@link #APPLICATION_NDJSON} as
* a replacement or any other line-delimited JSON format (e.g. JSON Lines,
* JSON Text Sequences).
* @since 5.0
*/
@Deprecated
public static final String APPLICATION_STREAM_JSON_VALUE = "application/stream+json";
/**
@@ -378,6 +398,7 @@ public class MediaType extends MimeType implements Serializable {
APPLICATION_FORM_URLENCODED = new MediaType("application", "x-www-form-urlencoded");
APPLICATION_JSON = new MediaType("application", "json");
APPLICATION_JSON_UTF8 = new MediaType("application", "json", StandardCharsets.UTF_8);
APPLICATION_NDJSON = new MediaType("application", "x-ndjson");
APPLICATION_OCTET_STREAM = new MediaType("application", "octet-stream");
APPLICATION_PDF = new MediaType("application", "pdf");
APPLICATION_PROBLEM_JSON = new MediaType("application", "problem+json");

View File

@@ -67,15 +67,9 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple
private static final byte[] NEWLINE_SEPARATOR = {'\n'};
private static final Map<MediaType, byte[]> STREAM_SEPARATORS;
private static final Map<String, JsonEncoding> ENCODINGS;
static {
STREAM_SEPARATORS = new HashMap<>(4);
STREAM_SEPARATORS.put(MediaType.APPLICATION_STREAM_JSON, NEWLINE_SEPARATOR);
STREAM_SEPARATORS.put(MediaType.parseMediaType("application/stream+x-jackson-smile"), new byte[0]);
ENCODINGS = new HashMap<>(JsonEncoding.values().length + 1);
for (JsonEncoding encoding : JsonEncoding.values()) {
ENCODINGS.put(encoding.getJavaName(), encoding);
@@ -98,9 +92,6 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple
/**
* Configure "streaming" media types for which flushing should be performed
* automatically vs at the end of the stream.
* <p>By default this is set to {@link MediaType#APPLICATION_STREAM_JSON}.
* @param mediaTypes one or more media types to add to the list
* @see HttpMessageEncoder#getStreamingMediaTypes()
*/
public void setStreamingMediaTypes(List<MediaType> mediaTypes) {
this.streamingMediaTypes.clear();
@@ -138,7 +129,7 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple
.flux();
}
else {
byte[] separator = streamSeparator(mimeType);
byte[] separator = getStreamingMediaTypeSeparator(mimeType);
if (separator != null) { // streaming
try {
ObjectWriter writer = createObjectWriter(elementType, mimeType, hints);
@@ -268,11 +259,18 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple
return writer;
}
/**
* Return the separator to use for the given mime type.
* <p>By default, this method returns new line {@code "\n"} if the given
* mime type is one of the configured {@link #setStreamingMediaTypes(List)
* streaming} mime types.
* @since 5.3
*/
@Nullable
private byte[] streamSeparator(@Nullable MimeType mimeType) {
protected byte[] getStreamingMediaTypeSeparator(@Nullable MimeType mimeType) {
for (MediaType streamingMediaType : this.streamingMediaTypes) {
if (streamingMediaType.isCompatibleWith(mimeType)) {
return STREAM_SEPARATORS.getOrDefault(streamingMediaType, NEWLINE_SEPARATOR);
return NEWLINE_SEPARATOR;
}
}
return null;

View File

@@ -35,6 +35,7 @@ import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.Hints;
import org.springframework.http.HttpLogging;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.lang.Nullable;
@@ -72,8 +73,9 @@ public abstract class Jackson2CodecSupport {
private static final List<MimeType> DEFAULT_MIME_TYPES = Collections.unmodifiableList(
Arrays.asList(
new MimeType("application", "json"),
new MimeType("application", "*+json")));
MediaType.APPLICATION_JSON,
new MediaType("application", "*+json"),
MediaType.APPLICATION_NDJSON));
protected final Log logger = HttpLogging.forLogName(getClass());

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2020 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,7 +16,7 @@
package org.springframework.http.codec.json;
import java.util.Collections;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@@ -54,9 +54,10 @@ public class Jackson2JsonEncoder extends AbstractJackson2Encoder {
this(Jackson2ObjectMapperBuilder.json().build());
}
@SuppressWarnings("deprecation")
public Jackson2JsonEncoder(ObjectMapper mapper, MimeType... mimeTypes) {
super(mapper, mimeTypes);
setStreamingMediaTypes(Collections.singletonList(MediaType.APPLICATION_STREAM_JSON));
setStreamingMediaTypes(Arrays.asList(MediaType.APPLICATION_NDJSON, MediaType.APPLICATION_STREAM_JSON));
this.ssePrettyPrinter = initSsePrettyPrinter();
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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.
@@ -25,6 +25,7 @@ import reactor.core.publisher.Flux;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.MimeType;
@@ -43,6 +44,11 @@ public class Jackson2SmileEncoder extends AbstractJackson2Encoder {
new MimeType("application", "x-jackson-smile"),
new MimeType("application", "*+x-jackson-smile")};
private static final MimeType STREAM_MIME_TYPE =
MediaType.parseMediaType("application/stream+x-jackson-smile");
private static final byte[] STREAM_SEPARATOR = new byte[0];
public Jackson2SmileEncoder() {
this(Jackson2ObjectMapperBuilder.smile().build(), DEFAULT_SMILE_MIME_TYPES);
@@ -54,4 +60,22 @@ public class Jackson2SmileEncoder extends AbstractJackson2Encoder {
setStreamingMediaTypes(Collections.singletonList(new MediaType("application", "stream+x-jackson-smile")));
}
/**
* Return the separator to use for the given mime type.
* <p>By default, this method returns a single byte 0 if the given
* mime type is one of the configured {@link #setStreamingMediaTypes(List)
* streaming} mime types.
* @since 5.3
*/
@Nullable
@Override
protected byte[] getStreamingMediaTypeSeparator(@Nullable MimeType mimeType) {
for (MediaType streamingMediaType : getStreamingMediaTypes()) {
if (streamingMediaType.isCompatibleWith(mimeType)) {
return STREAM_SEPARATOR;
}
}
return null;
}
}

View File

@@ -51,6 +51,7 @@ import org.springframework.web.testfixture.xml.Pojo;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.http.MediaType.APPLICATION_NDJSON;
import static org.springframework.http.MediaType.APPLICATION_STREAM_JSON;
import static org.springframework.http.MediaType.APPLICATION_XML;
import static org.springframework.http.codec.json.Jackson2CodecSupport.JSON_VIEW_HINT;
@@ -77,6 +78,7 @@ public class Jackson2JsonDecoderTests extends AbstractDecoderTests<Jackson2JsonD
@Test
public void canDecode() {
assertThat(decoder.canDecode(ResolvableType.forClass(Pojo.class), APPLICATION_JSON)).isTrue();
assertThat(decoder.canDecode(ResolvableType.forClass(Pojo.class), APPLICATION_NDJSON)).isTrue();
assertThat(decoder.canDecode(ResolvableType.forClass(Pojo.class), APPLICATION_STREAM_JSON)).isTrue();
assertThat(decoder.canDecode(ResolvableType.forClass(Pojo.class), null)).isTrue();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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.
@@ -47,6 +47,7 @@ import static java.util.Collections.singletonMap;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.http.MediaType.APPLICATION_NDJSON;
import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM;
import static org.springframework.http.MediaType.APPLICATION_STREAM_JSON;
import static org.springframework.http.MediaType.APPLICATION_XML;
@@ -66,6 +67,7 @@ public class Jackson2JsonEncoderTests extends AbstractEncoderTests<Jackson2JsonE
public void canEncode() {
ResolvableType pojoType = ResolvableType.forClass(Pojo.class);
assertThat(this.encoder.canEncode(pojoType, APPLICATION_JSON)).isTrue();
assertThat(this.encoder.canEncode(pojoType, APPLICATION_NDJSON)).isTrue();
assertThat(this.encoder.canEncode(pojoType, APPLICATION_STREAM_JSON)).isTrue();
assertThat(this.encoder.canEncode(pojoType, null)).isTrue();

View File

@@ -197,6 +197,43 @@ public class Jackson2TokenizerTests extends AbstractLeakCheckingTests {
testTokenize(asList("[1", ",2,", "3]"), asList("1", "2", "3"), true);
}
@Test
void tokenizeStream() {
// NDJSON (Newline Delimited JSON), JSON Lines
testTokenize(
asList(
"{\"id\":1,\"name\":\"Robert\"}",
"\n",
"{\"id\":2,\"name\":\"Raide\"}",
"\n",
"{\"id\":3,\"name\":\"Ford\"}"
),
asList(
"{\"id\":1,\"name\":\"Robert\"}",
"{\"id\":2,\"name\":\"Raide\"}",
"{\"id\":3,\"name\":\"Ford\"}"
),
true);
// JSON Sequence with newline separator
testTokenize(
asList(
"\n",
"{\"id\":1,\"name\":\"Robert\"}",
"\n",
"{\"id\":2,\"name\":\"Raide\"}",
"\n",
"{\"id\":3,\"name\":\"Ford\"}"
),
asList(
"{\"id\":1,\"name\":\"Robert\"}",
"{\"id\":2,\"name\":\"Raide\"}",
"{\"id\":3,\"name\":\"Ford\"}"
),
true);
}
private void testTokenize(List<String> input, List<String> output, boolean tokenize) {
StepVerifier.FirstStep<String> builder = StepVerifier.create(decode(input, tokenize, -1));
output.forEach(expected -> builder.assertNext(actual -> {