diff --git a/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufDecoder.java b/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufDecoder.java index 442e255d02c..81fa9ce5600 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufDecoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufDecoder.java @@ -173,6 +173,19 @@ public class ProtobufDecoder extends ProtobufCodecSupport implements DecoderThis method uses a ConcurrentHashMap for caching method lookups. + */ + protected static Message.Builder getMessageBuilder(Class clazz) throws Exception { + Method method = methodCache.get(clazz); + if (method == null) { + method = clazz.getMethod("newBuilder"); + methodCache.put(clazz, method); + } + return (Message.Builder) method.invoke(clazz); + } + /** * Use merge methods on {@link Message.Builder} to read a single message * from the given {@code DataBuffer}. @@ -184,20 +197,6 @@ public class ProtobufDecoder extends ProtobufCodecSupport implements DecoderThis method uses a ConcurrentHashMap for caching method lookups. - */ - private static Message.Builder getMessageBuilder(Class clazz) throws Exception { - Method method = methodCache.get(clazz); - if (method == null) { - method = clazz.getMethod("newBuilder"); - methodCache.put(clazz, method); - } - return (Message.Builder) method.invoke(clazz); - } - @Override public List getDecodableMimeTypes() { return getMimeTypes(); diff --git a/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufEncoder.java b/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufEncoder.java index 6230591ba2a..227c57c5f8b 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufEncoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufEncoder.java @@ -54,6 +54,7 @@ import org.springframework.util.MimeType; * * @author Sebastien Deleuze * @since 5.1 + * @see ProtobufHttpMessageWriter * @see ProtobufDecoder */ public class ProtobufEncoder extends ProtobufCodecSupport implements HttpMessageEncoder { diff --git a/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufHttpMessageWriter.java b/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufHttpMessageWriter.java index 103c5dea6fc..1ec9f50e3b0 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufHttpMessageWriter.java +++ b/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufHttpMessageWriter.java @@ -95,6 +95,19 @@ public class ProtobufHttpMessageWriter extends EncoderHttpMessageWriter } } + /** + * Create a new {@code Message.Builder} instance for the given class. + *

This method uses a ConcurrentHashMap for caching method lookups. + */ + protected static Message.Builder getMessageBuilder(Class clazz) throws Exception { + Method method = methodCache.get(clazz); + if (method == null) { + method = clazz.getMethod("newBuilder"); + methodCache.put(clazz, method); + } + return (Message.Builder) method.invoke(clazz); + } + /** * Return the {@code MediaType} to use when the input Publisher is multivalued. * @since 7.0 @@ -118,17 +131,4 @@ public class ProtobufHttpMessageWriter extends EncoderHttpMessageWriter protected void extendHeaders(ReactiveHttpOutputMessage message, Map hints) { } - /** - * Create a new {@code Message.Builder} instance for the given class. - *

This method uses a ConcurrentHashMap for caching method lookups. - */ - private static Message.Builder getMessageBuilder(Class clazz) throws Exception { - Method method = methodCache.get(clazz); - if (method == null) { - method = clazz.getMethod("newBuilder"); - methodCache.put(clazz, method); - } - return (Message.Builder) method.invoke(clazz); - } - } diff --git a/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufJsonDecoder.java b/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufJsonDecoder.java index 0bb13897dea..de5dd6cee05 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufJsonDecoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufJsonDecoder.java @@ -161,7 +161,7 @@ public class ProtobufJsonDecoder implements Decoder { * Create a new {@code Message.Builder} instance for the given class. *

This method uses a ConcurrentHashMap for caching method lookups. */ - private static Message.Builder getMessageBuilder(Class clazz) throws Exception { + protected static Message.Builder getMessageBuilder(Class clazz) throws Exception { Method method = methodCache.get(clazz); if (method == null) { method = clazz.getMethod("newBuilder"); diff --git a/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufJsonEncoder.java b/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufJsonEncoder.java index ef42819cd3c..ae549111946 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufJsonEncoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufJsonEncoder.java @@ -49,6 +49,7 @@ import org.springframework.util.MimeType; * * @author Brian Clozel * @since 6.2 + * @see ProtobufHttpMessageWriter * @see ProtobufJsonDecoder */ public class ProtobufJsonEncoder implements HttpMessageEncoder { @@ -59,7 +60,8 @@ public class ProtobufJsonEncoder implements HttpMessageEncoder { private static final List defaultMimeTypes = List.of( MediaType.APPLICATION_JSON, - new MediaType("application", "*+json")); + new MediaType("application", "*+json"), + MediaType.APPLICATION_NDJSON); private final JsonFormat.Printer printer; @@ -106,7 +108,10 @@ public class ProtobufJsonEncoder implements HttpMessageEncoder { } @Override - public Flux encode(Publisher inputStream, DataBufferFactory bufferFactory, ResolvableType elementType, @Nullable MimeType mimeType, @Nullable Map hints) { + public Flux encode( + Publisher inputStream, DataBufferFactory bufferFactory, + ResolvableType elementType, @Nullable MimeType mimeType, @Nullable Map hints) { + if (inputStream instanceof Mono) { return Mono.from(inputStream) .map(value -> encodeValue(value, bufferFactory, elementType, mimeType, hints)) @@ -120,17 +125,21 @@ public class ProtobufJsonEncoder implements HttpMessageEncoder { .map(value -> { byte[] prefix = helper.getPrefix(); byte[] delimiter = helper.getDelimiter(); + DataBuffer delimiterBuffer = bufferFactory.wrap(delimiter); DataBuffer dataBuffer = encodeValue(value, bufferFactory, MESSAGE_TYPE, mimeType, hints); return (prefix.length > 0 ? - bufferFactory.join(List.of(bufferFactory.wrap(prefix), bufferFactory.wrap(delimiter), dataBuffer)) : - bufferFactory.join(List.of(bufferFactory.wrap(delimiter), dataBuffer))); + bufferFactory.join(List.of(bufferFactory.wrap(prefix), delimiterBuffer, dataBuffer)) : + bufferFactory.join(List.of(delimiterBuffer, dataBuffer))); }) .switchIfEmpty(Mono.fromCallable(() -> bufferFactory.wrap(helper.getPrefix()))) .concatWith(Mono.fromCallable(() -> bufferFactory.wrap(helper.getSuffix()))); } @Override - public DataBuffer encodeValue(Message message, DataBufferFactory bufferFactory, ResolvableType valueType, @Nullable MimeType mimeType, @Nullable Map hints) { + public DataBuffer encodeValue( + Message message, DataBufferFactory bufferFactory, ResolvableType valueType, + @Nullable MimeType mimeType, @Nullable Map hints) { + FastByteArrayOutputStream bos = new FastByteArrayOutputStream(); OutputStreamWriter writer = new OutputStreamWriter(bos, StandardCharsets.UTF_8); try { @@ -144,6 +153,7 @@ public class ProtobufJsonEncoder implements HttpMessageEncoder { } } + private static class JsonArrayJoinHelper { private static final byte[] COMMA_SEPARATOR = {','}; diff --git a/spring-web/src/test/java/org/springframework/http/codec/protobuf/ProtobufJsonEncoderTests.java b/spring-web/src/test/java/org/springframework/http/codec/protobuf/ProtobufJsonEncoderTests.java index ed7b6d4224a..2924b0aa731 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/protobuf/ProtobufJsonEncoderTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/protobuf/ProtobufJsonEncoderTests.java @@ -43,22 +43,28 @@ import static org.springframework.core.ResolvableType.forClass; */ class ProtobufJsonEncoderTests extends AbstractEncoderTests { - private Msg msg1 = - Msg.newBuilder().setFoo("Foo").setBlah(SecondMsg.newBuilder().setBlah(123).build()).build(); + private Msg msg1 = Msg.newBuilder() + .setFoo("Foo") + .setBlah(SecondMsg.newBuilder().setBlah(123).build()) + .build(); + + private Msg msg2 = Msg.newBuilder() + .setFoo("Bar") + .setBlah(SecondMsg.newBuilder().setBlah(456).build()) + .build(); - private Msg msg2 = - Msg.newBuilder().setFoo("Bar").setBlah(SecondMsg.newBuilder().setBlah(456).build()).build(); public ProtobufJsonEncoderTests() { super(new ProtobufJsonEncoder(JsonFormat.printer().omittingInsignificantWhitespace())); } + @Override @Test protected void canEncode() throws Exception { assertThat(this.encoder.canEncode(forClass(Msg.class), null)).isFalse(); assertThat(this.encoder.canEncode(forClass(Msg.class), MediaType.APPLICATION_JSON)).isTrue(); - assertThat(this.encoder.canEncode(forClass(Msg.class), MediaType.APPLICATION_NDJSON)).isFalse(); + assertThat(this.encoder.canEncode(forClass(Msg.class), MediaType.APPLICATION_NDJSON)).isTrue(); assertThat(this.encoder.canEncode(forClass(Object.class), MediaType.APPLICATION_JSON)).isFalse(); } @@ -69,7 +75,7 @@ class ProtobufJsonEncoderTests extends AbstractEncoderTests ResolvableType inputType = forClass(Msg.class); testEncode(input, inputType, MediaType.APPLICATION_JSON, null, step -> step - .assertNext(dataBuffer -> assertBufferEqualsJson(dataBuffer, "{\"foo\":\"Foo\",\"blah\":{\"blah\":123}}")) + .assertNext(buffer -> assertBufferEqualsJson(buffer, "{\"foo\":\"Foo\",\"blah\":{\"blah\":123}}")) .verifyComplete()); testEncodeError(input, inputType, MediaType.APPLICATION_JSON, null); testEncodeCancel(input, inputType, MediaType.APPLICATION_JSON, null); @@ -91,9 +97,9 @@ class ProtobufJsonEncoderTests extends AbstractEncoderTests ResolvableType inputType = forClass(Msg.class); testEncode(input, inputType, MediaType.APPLICATION_JSON, null, step -> step - .assertNext(dataBuffer -> assertBufferEqualsJson(dataBuffer, "[{\"foo\":\"Foo\",\"blah\":{\"blah\":123}}")) - .assertNext(dataBuffer -> assertBufferEqualsJson(dataBuffer, ",{\"foo\":\"Bar\",\"blah\":{\"blah\":456}}")) - .assertNext(dataBuffer -> assertBufferEqualsJson(dataBuffer, "]")) + .assertNext(buffer -> assertBufferEqualsJson(buffer, "[{\"foo\":\"Foo\",\"blah\":{\"blah\":123}}")) + .assertNext(buffer -> assertBufferEqualsJson(buffer, ",{\"foo\":\"Bar\",\"blah\":{\"blah\":456}}")) + .assertNext(buffer -> assertBufferEqualsJson(buffer, "]")) .verifyComplete()); }