From 36a07aa89747bb5942bd4a1bf7f5192a2c48c5ef Mon Sep 17 00:00:00 2001 From: sdeleuze Date: Mon, 19 Mar 2018 18:16:46 +0100 Subject: [PATCH] Support Protobuf serialization in WebFlux This commit introduces Protobuf support in WebFlux via dedicated codecs. Flux are serialized/deserialized using delimited Protobuf messages with the size of each message specified before the message itself. In that case, a "delimited=true" parameter is added to the content type. Mono are expected to use regular Protobuf message format (without the size prepended before the message). Related HttpMessageReader/Writer are automatically registered when the "com.google.protobuf:protobuf-java" library is detected in the classpath, and can be customized easily if needed via CodecConfigurer, for example to specify protocol extensions via the ExtensionRegistry based constructors. Both "application/x-protobuf" and "application/octet-stream" mime types are supported. Issue: SPR-15776 --- .../http/codec/CodecConfigurer.java | 16 + .../http/codec/EncoderHttpMessageWriter.java | 3 +- .../codec/protobuf/ProtobufCodecSupport.java | 51 ++ .../http/codec/protobuf/ProtobufDecoder.java | 212 ++++++ .../http/codec/protobuf/ProtobufEncoder.java | 103 +++ .../protobuf/ProtobufHttpMessageWriter.java | 108 +++ .../http/codec/support/BaseDefaultCodecs.java | 43 +- .../codec/protobuf/ProtobufDecoderTests.java | 183 +++++ .../codec/protobuf/ProtobufEncoderTests.java | 111 +++ .../support/ClientCodecConfigurerTests.java | 8 +- .../codec/support/CodecConfigurerTests.java | 44 +- .../support/ServerCodecConfigurerTests.java | 8 +- spring-webflux/spring-webflux.gradle | 1 + .../DelegatingWebFluxConfigurationTests.java | 2 +- .../WebFluxConfigurationSupportTests.java | 10 +- .../web/reactive/protobuf/Msg.java | 654 ++++++++++++++++++ .../web/reactive/protobuf/MsgOrBuilder.java | 37 + .../web/reactive/protobuf/OuterSample.java | 62 ++ .../web/reactive/protobuf/SecondMsg.java | 389 +++++++++++ .../reactive/protobuf/SecondMsgOrBuilder.java | 18 + .../annotation/ProtobufIntegrationTests.java | 164 +++++ spring-webflux/src/test/proto/sample.proto | 12 + src/docs/asciidoc/web/webflux.adoc | 4 +- 23 files changed, 2225 insertions(+), 18 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufCodecSupport.java create mode 100644 spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufDecoder.java create mode 100644 spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufEncoder.java create mode 100644 spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufHttpMessageWriter.java create mode 100644 spring-web/src/test/java/org/springframework/http/codec/protobuf/ProtobufDecoderTests.java create mode 100644 spring-web/src/test/java/org/springframework/http/codec/protobuf/ProtobufEncoderTests.java create mode 100644 spring-webflux/src/test/java/org/springframework/web/reactive/protobuf/Msg.java create mode 100644 spring-webflux/src/test/java/org/springframework/web/reactive/protobuf/MsgOrBuilder.java create mode 100644 spring-webflux/src/test/java/org/springframework/web/reactive/protobuf/OuterSample.java create mode 100644 spring-webflux/src/test/java/org/springframework/web/reactive/protobuf/SecondMsg.java create mode 100644 spring-webflux/src/test/java/org/springframework/web/reactive/protobuf/SecondMsgOrBuilder.java create mode 100644 spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ProtobufIntegrationTests.java create mode 100644 spring-webflux/src/test/proto/sample.proto diff --git a/spring-web/src/main/java/org/springframework/http/codec/CodecConfigurer.java b/spring-web/src/main/java/org/springframework/http/codec/CodecConfigurer.java index facc46fb59e..29d5e7cecdc 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/CodecConfigurer.java +++ b/spring-web/src/main/java/org/springframework/http/codec/CodecConfigurer.java @@ -110,6 +110,22 @@ public interface CodecConfigurer { */ void jackson2JsonEncoder(Encoder encoder); + /** + * Override the default Protobuf {@code Decoder}. + * @param decoder the decoder instance to use + * @since 5.1 + * @see org.springframework.http.codec.protobuf.ProtobufDecoder + */ + void protobufDecoder(Decoder decoder); + + /** + * Override the default Protobuf {@code HttpMessageReader}. + * @param decoder the decoder instance to use + * @since 5.1 + * @see org.springframework.http.codec.protobuf.ProtobufHttpMessageWriter + */ + void protobufWriter(HttpMessageWriter decoder); + /** * Whether to log form data at DEBUG level, and headers at TRACE level. * Both may contain sensitive information. diff --git a/spring-web/src/main/java/org/springframework/http/codec/EncoderHttpMessageWriter.java b/spring-web/src/main/java/org/springframework/http/codec/EncoderHttpMessageWriter.java index f7f2ce6109c..b7ca0438a91 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/EncoderHttpMessageWriter.java +++ b/spring-web/src/main/java/org/springframework/http/codec/EncoderHttpMessageWriter.java @@ -160,7 +160,8 @@ public class EncoderHttpMessageWriter implements HttpMessageWriter { private boolean isStreamingMediaType(@Nullable MediaType contentType) { return (contentType != null && this.encoder instanceof HttpMessageEncoder && ((HttpMessageEncoder) this.encoder).getStreamingMediaTypes().stream() - .anyMatch(contentType::isCompatibleWith)); + .anyMatch(streamingMediaType -> contentType.isCompatibleWith(streamingMediaType) && + contentType.getParameters().entrySet().containsAll(streamingMediaType.getParameters().keySet()))); } diff --git a/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufCodecSupport.java b/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufCodecSupport.java new file mode 100644 index 00000000000..d1e08eddc93 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufCodecSupport.java @@ -0,0 +1,51 @@ +/* + * 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. + * 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.protobuf; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.springframework.lang.Nullable; +import org.springframework.util.MimeType; + +/** + * Base class providing support methods for Protobuf encoding and decoding. + * + * @author Sebastien Deleuze + * @since 5.1 + */ +public abstract class ProtobufCodecSupport { + + static final List MIME_TYPES = Collections.unmodifiableList( + Arrays.asList( + new MimeType("application", "x-protobuf"), + new MimeType("application", "octet-stream"))); + + static final String DELIMITED_KEY = "delimited"; + + static final String DELIMITED_VALUE = "true"; + + + protected boolean supportsMimeType(@Nullable MimeType mimeType) { + return (mimeType == null || MIME_TYPES.stream().anyMatch(m -> m.isCompatibleWith(mimeType))); + } + + protected List getMimeTypes() { + return MIME_TYPES; + } +} 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 new file mode 100644 index 00000000000..4e783c396c8 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufDecoder.java @@ -0,0 +1,212 @@ +/* + * 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. + * 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.protobuf; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +import com.google.protobuf.CodedInputStream; +import com.google.protobuf.ExtensionRegistry; +import com.google.protobuf.Message; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.core.ResolvableType; +import org.springframework.core.codec.Decoder; +import org.springframework.core.codec.DecodingException; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.util.Assert; +import org.springframework.util.MimeType; + +/** + * A {@code Decoder} that reads {@link com.google.protobuf.Message}s + * using Google Protocol Buffers. + * + * Flux deserialized via + * {@link #decode(Publisher, ResolvableType, MimeType, Map)} are expected to use + * delimited Protobuf messages + * with the size of each message specified before the message itself. Single values deserialized + * via {@link #decodeToMono(Publisher, ResolvableType, MimeType, Map)} are expected to use + * regular Protobuf message format (without the size prepended before the message). + * + *

To generate {@code Message} Java classes, you need to install the {@code protoc} binary. + * + *

This decoder requires Protobuf 3 or higher, and supports + * {@code "application/x-protobuf"} and {@code "application/octet-stream"} with the official + * {@code "com.google.protobuf:protobuf-java"} library. + * + * @author Sébastien Deleuze + * @since 5.1 + * @see ProtobufEncoder + */ +public class ProtobufDecoder extends ProtobufCodecSupport implements Decoder { + + /** + * The default max size for aggregating messages. + */ + protected static final int DEFAULT_MESSAGE_MAX_SIZE = 64 * 1024; + + private static final ConcurrentHashMap, Method> methodCache = new ConcurrentHashMap<>(); + + private final ExtensionRegistry extensionRegistry; + + private int maxMessageSize = DEFAULT_MESSAGE_MAX_SIZE; + + + /** + * Construct a new {@code ProtobufDecoder}. + */ + public ProtobufDecoder() { + this(ExtensionRegistry.newInstance()); + } + + /** + * Construct a new {@code ProtobufDecoder} with an initializer that allows the + * registration of message extensions. + * @param extensionRegistry a message extension registry + */ + public ProtobufDecoder(ExtensionRegistry extensionRegistry) { + Assert.notNull(extensionRegistry, "ExtensionRegistry must not be null"); + this.extensionRegistry = extensionRegistry; + } + + public void setMaxMessageSize(int maxMessageSize) { + this.maxMessageSize = maxMessageSize; + } + + @Override + public boolean canDecode(ResolvableType elementType, MimeType mimeType) { + return Message.class.isAssignableFrom(elementType.getRawClass()) && supportsMimeType(mimeType); + } + + @Override + public Flux decode(Publisher inputStream, ResolvableType elementType, + MimeType mimeType, Map hints) { + + return Flux.from(inputStream) + .concatMap(new MessageDecoderFunction(elementType, this.maxMessageSize)); + } + + @Override + public Mono decodeToMono(Publisher inputStream, ResolvableType elementType, + MimeType mimeType, Map hints) { + return DataBufferUtils.join(inputStream).map(dataBuffer -> { + try { + Message.Builder builder = getMessageBuilder(elementType.getRawClass()); + builder.mergeFrom(CodedInputStream.newInstance(dataBuffer.asByteBuffer()), this.extensionRegistry); + Message message = builder.build(); + DataBufferUtils.release(dataBuffer); + return message; + } + catch (IOException ex) { + throw new DecodingException("I/O error while parsing input stream", ex); + } + catch (Exception ex) { + throw new DecodingException("Could not read Protobuf message: " + ex.getMessage(), ex); + } + } + ); + } + + /** + * 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); + } + + @Override + public List getDecodableMimeTypes() { + return getMimeTypes(); + } + + + private class MessageDecoderFunction implements Function> { + + private final ResolvableType elementType; + + private final int maxMessageSize; + + private DataBuffer output; + + private int messageBytesToRead; + + public MessageDecoderFunction(ResolvableType elementType, int maxMessageSize) { + this.elementType = elementType; + this.maxMessageSize = maxMessageSize; + } + + // TODO Instead of the recursive call, loop over the current DataBuffer, produce a list of as many messages as are contained, and save any remaining bytes with flatMapIterable + @Override + public Publisher apply(DataBuffer input) { + + try { + if (this.output == null) { + int firstByte = input.read(); + if (firstByte == -1) { + return Flux.error(new DecodingException("Can't parse message size")); + } + this.messageBytesToRead = CodedInputStream.readRawVarint32(firstByte, input.asInputStream()); + if (this.messageBytesToRead > this.maxMessageSize) { + return Flux.error(new DecodingException( + "The number of bytes to read parsed in the incoming stream (" + + this.messageBytesToRead + ") exceeds the configured limit (" + this.maxMessageSize + ")")); + } + this.output = input.factory().allocateBuffer(this.messageBytesToRead); + } + int chunkBytesToRead = this.messageBytesToRead >= input.readableByteCount() ? + input.readableByteCount() : this.messageBytesToRead; + int remainingBytesToRead = input.readableByteCount() - chunkBytesToRead; + this.output.write(input.slice(input.readPosition(), chunkBytesToRead)); + this.messageBytesToRead -= chunkBytesToRead; + Message message = null; + if (this.messageBytesToRead == 0) { + Message.Builder builder = getMessageBuilder(this.elementType.getRawClass()); + builder.mergeFrom(CodedInputStream.newInstance(this.output.asByteBuffer()), extensionRegistry); + message = builder.build(); + DataBufferUtils.release(this.output); + this.output = null; + } + if (remainingBytesToRead > 0) { + return Mono.justOrEmpty(message).concatWith( + apply(input.slice(input.readPosition() + chunkBytesToRead, remainingBytesToRead))); + } + else { + return Mono.justOrEmpty(message); + } + } + catch (IOException ex) { + return Flux.error(new DecodingException("I/O error while parsing input stream", ex)); + } + catch (Exception ex) { + return Flux.error(new DecodingException("Could not read Protobuf message: " + ex.getMessage(), ex)); + } + } + } +} 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 new file mode 100644 index 00000000000..5b06f97e5a9 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufEncoder.java @@ -0,0 +1,103 @@ +/* + * 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. + * 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.protobuf; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.google.protobuf.Message; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.http.MediaType; +import org.springframework.http.codec.HttpMessageEncoder; +import org.springframework.util.MimeType; + +/** + * An {@code Encoder} that writes {@link com.google.protobuf.Message}s + * using Google Protocol Buffers. + * + * Flux are serialized using + * delimited Protobuf messages + * with the size of each message specified before the message itself. Single values are + * serialized using regular Protobuf message format (without the size prepended before the message). + * + *

To generate {@code Message} Java classes, you need to install the {@code protoc} binary. + * + *

This encoder requires Protobuf 3 or higher, and supports + * {@code "application/x-protobuf"} and {@code "application/octet-stream"} with the official + * {@code "com.google.protobuf:protobuf-java"} library. + * + * @author Sébastien Deleuze + * @since 5.1 + * @see ProtobufDecoder + */ +public class ProtobufEncoder extends ProtobufCodecSupport implements HttpMessageEncoder { + + private static final List streamingMediaTypes = MIME_TYPES + .stream() + .map(mimeType -> new MediaType(mimeType.getType(), mimeType.getSubtype(), Collections.singletonMap(DELIMITED_KEY, DELIMITED_VALUE))) + .collect(Collectors.toList()); + + @Override + public boolean canEncode(ResolvableType elementType, MimeType mimeType) { + return Message.class.isAssignableFrom(elementType.getRawClass()) && supportsMimeType(mimeType); + } + + @Override + public Flux encode(Publisher inputStream, + DataBufferFactory bufferFactory, ResolvableType elementType, MimeType mimeType, Map hints) { + return Flux + .from(inputStream) + .map(message -> encodeMessage(message, bufferFactory, !(inputStream instanceof Mono))); + } + + private DataBuffer encodeMessage(Message message, DataBufferFactory bufferFactory, boolean streaming) { + DataBuffer buffer = bufferFactory.allocateBuffer(); + OutputStream outputStream = buffer.asOutputStream(); + try { + if (streaming) { + message.writeDelimitedTo(outputStream); + } + else { + message.writeTo(outputStream); + } + return buffer; + } + catch (IOException ex) { + throw new IllegalStateException("Unexpected I/O error while writing to data buffer", ex); + } + } + + @Override + public List getStreamingMediaTypes() { + return streamingMediaTypes; + } + + @Override + public List getEncodableMimeTypes() { + return getMimeTypes(); + } +} 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 new file mode 100644 index 00000000000..dd55a557fd3 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufHttpMessageWriter.java @@ -0,0 +1,108 @@ +/* + * 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. + * 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.protobuf; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import com.google.protobuf.Descriptors; +import com.google.protobuf.Message; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.core.ResolvableType; +import org.springframework.core.codec.DecodingException; +import org.springframework.http.MediaType; +import org.springframework.http.ReactiveHttpOutputMessage; +import org.springframework.http.codec.EncoderHttpMessageWriter; +import org.springframework.http.codec.HttpMessageEncoder; +import org.springframework.lang.Nullable; + +/** + * {@code HttpMessageWriter} that can write a protobuf {@link Message} and adds + * {@code X-Protobuf-Schema}, {@code X-Protobuf-Message} headers and a + * {@code delimited=true} parameter is added to the content type if a flux is serialized. + * + *

For {@code HttpMessageReader}, just use + * {@code new DecoderHttpMessageReader(new ProtobufDecoder())}. + * + * @author Sébastien Deleuze + * @since 5.1 + * @see ProtobufEncoder + */ +public class ProtobufHttpMessageWriter extends EncoderHttpMessageWriter { + + private static final ConcurrentHashMap, Method> methodCache = new ConcurrentHashMap<>(); + + private static final String X_PROTOBUF_SCHEMA_HEADER = "X-Protobuf-Schema"; + + private static final String X_PROTOBUF_MESSAGE_HEADER = "X-Protobuf-Message"; + + + public ProtobufHttpMessageWriter() { + super(new ProtobufEncoder()); + } + + public ProtobufHttpMessageWriter(ProtobufEncoder encoder) { + super(encoder); + } + + + @SuppressWarnings("unchecked") + @Override + public Mono write(Publisher inputStream, ResolvableType elementType, + @Nullable MediaType mediaType, ReactiveHttpOutputMessage message, Map hints) { + + try { + Message.Builder builder = getMessageBuilder(elementType.getRawClass()); + Descriptors.Descriptor descriptor = builder.getDescriptorForType(); + message.getHeaders().add(X_PROTOBUF_SCHEMA_HEADER, descriptor.getFile().getName()); + message.getHeaders().add(X_PROTOBUF_MESSAGE_HEADER, descriptor.getFullName()); + if (inputStream instanceof Flux) { + if (mediaType == null) { + message.getHeaders().setContentType(((HttpMessageEncoder)getEncoder()).getStreamingMediaTypes().get(0)); + } + else if (!ProtobufEncoder.DELIMITED_VALUE.equals(mediaType.getParameters().get(ProtobufEncoder.DELIMITED_KEY))) { + Map parameters = new HashMap<>(mediaType.getParameters()); + parameters.put(ProtobufEncoder.DELIMITED_KEY, ProtobufEncoder.DELIMITED_VALUE); + message.getHeaders().setContentType(new MediaType(mediaType.getType(), mediaType.getSubtype(), parameters)); + } + } + return super.write(inputStream, elementType, mediaType, message, hints); + } + catch (Exception ex) { + return Mono.error(new DecodingException("Could not read Protobuf message: " + ex.getMessage(), ex)); + } + } + + /** + * 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/support/BaseDefaultCodecs.java b/spring-web/src/main/java/org/springframework/http/codec/support/BaseDefaultCodecs.java index 3abf5b1eb7d..f0ca6752fe0 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/support/BaseDefaultCodecs.java +++ b/spring-web/src/main/java/org/springframework/http/codec/support/BaseDefaultCodecs.java @@ -41,6 +41,8 @@ import org.springframework.http.codec.json.Jackson2JsonDecoder; import org.springframework.http.codec.json.Jackson2JsonEncoder; import org.springframework.http.codec.json.Jackson2SmileDecoder; import org.springframework.http.codec.json.Jackson2SmileEncoder; +import org.springframework.http.codec.protobuf.ProtobufDecoder; +import org.springframework.http.codec.protobuf.ProtobufHttpMessageWriter; import org.springframework.http.codec.xml.Jaxb2XmlDecoder; import org.springframework.http.codec.xml.Jaxb2XmlEncoder; import org.springframework.lang.Nullable; @@ -54,26 +56,37 @@ import org.springframework.util.ClassUtils; */ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs { + private static final ClassLoader classLoader = BaseCodecConfigurer.class.getClassLoader(); + static final boolean jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", BaseCodecConfigurer.class.getClassLoader()) && ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", - BaseCodecConfigurer.class.getClassLoader()); + classLoader); private static final boolean jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", - BaseCodecConfigurer.class.getClassLoader()); + classLoader); private static final boolean jaxb2Present = - ClassUtils.isPresent("javax.xml.bind.Binder", BaseCodecConfigurer.class.getClassLoader()); + ClassUtils.isPresent("javax.xml.bind.Binder", classLoader); + + private static final boolean protobufPresent = + ClassUtils.isPresent("com.google.protobuf.Message", classLoader); @Nullable private Decoder jackson2JsonDecoder; + @Nullable + private Decoder protobufDecoder; + @Nullable private Encoder jackson2JsonEncoder; + @Nullable + private HttpMessageWriter protobufWriter; + private boolean enableLoggingRequestDetails = false; private boolean registerDefaults = true; @@ -89,6 +102,16 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs { this.jackson2JsonEncoder = encoder; } + @Override + public void protobufDecoder(Decoder decoder) { + this.protobufDecoder = decoder; + } + + @Override + public void protobufWriter(HttpMessageWriter writer) { + this.protobufWriter = writer; + } + @Override public void enableLoggingRequestDetails(boolean enable) { this.enableLoggingRequestDetails = enable; @@ -119,6 +142,9 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs { readers.add(new DecoderHttpMessageReader<>(new DataBufferDecoder())); readers.add(new DecoderHttpMessageReader<>(new ResourceDecoder())); readers.add(new DecoderHttpMessageReader<>(StringDecoder.textPlainOnly())); + if (protobufPresent) { + readers.add(new DecoderHttpMessageReader<>(getProtobufDecoder())); + } FormHttpMessageReader formReader = new FormHttpMessageReader(); formReader.setEnableLoggingRequestDetails(this.enableLoggingRequestDetails); @@ -194,6 +220,9 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs { if (!forMultipart) { extendTypedWriters(writers); } + if (protobufPresent) { + writers.add(getProtobufWriter()); + } return writers; } @@ -255,9 +284,17 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs { return (this.jackson2JsonDecoder != null ? this.jackson2JsonDecoder : new Jackson2JsonDecoder()); } + protected Decoder getProtobufDecoder() { + return (this.protobufDecoder != null ? this.protobufDecoder : new ProtobufDecoder()); + } + protected Encoder getJackson2JsonEncoder() { return (this.jackson2JsonEncoder != null ? this.jackson2JsonEncoder : new Jackson2JsonEncoder()); } + protected HttpMessageWriter getProtobufWriter() { + return (this.protobufWriter != null ? this.protobufWriter : new ProtobufHttpMessageWriter()); + } + } diff --git a/spring-web/src/test/java/org/springframework/http/codec/protobuf/ProtobufDecoderTests.java b/spring-web/src/test/java/org/springframework/http/codec/protobuf/ProtobufDecoderTests.java new file mode 100644 index 00000000000..55a3018f897 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/codec/protobuf/ProtobufDecoderTests.java @@ -0,0 +1,183 @@ +/* + * 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. + * 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.protobuf; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import com.google.protobuf.Message; +import org.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import org.springframework.core.ResolvableType; +import org.springframework.core.codec.DecodingException; +import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.http.MediaType; +import org.springframework.protobuf.Msg; +import org.springframework.protobuf.SecondMsg; +import org.springframework.util.MimeType; + +import static java.util.Collections.emptyMap; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.springframework.core.ResolvableType.forClass; + +/** + * Unit tests for {@link ProtobufDecoder}. + * TODO Make tests more readable + * TODO Add a test where an input DataBuffer is larger than a message + * + * @author Sebastien Deleuze + */ +public class ProtobufDecoderTests extends AbstractDataBufferAllocatingTestCase { + + private final static MimeType PROTOBUF_MIME_TYPE = new MimeType("application", "x-protobuf"); + + private final Msg testMsg = Msg.newBuilder().setFoo("Foo").setBlah(SecondMsg.newBuilder().setBlah(123).build()).build(); + + private ProtobufDecoder decoder; + + + @Before + public void setup() { + this.decoder = new ProtobufDecoder(); + } + + @Test(expected = IllegalArgumentException.class) + public void extensionRegistryNull() { + new ProtobufDecoder(null); + } + + @Test + public void canDecode() { + assertTrue(this.decoder.canDecode(forClass(Msg.class), null)); + assertTrue(this.decoder.canDecode(forClass(Msg.class), PROTOBUF_MIME_TYPE)); + assertTrue(this.decoder.canDecode(forClass(Msg.class), MediaType.APPLICATION_OCTET_STREAM)); + assertFalse(this.decoder.canDecode(forClass(Msg.class), MediaType.APPLICATION_JSON)); + assertFalse(this.decoder.canDecode(forClass(Object.class), PROTOBUF_MIME_TYPE)); + } + + @Test + public void decodeToMono() { + byte[] body = this.testMsg.toByteArray(); + Flux source = Flux.just(this.bufferFactory.wrap(body)); + ResolvableType elementType = forClass(Msg.class); + Mono mono = this.decoder.decodeToMono(source, elementType, null, + emptyMap()); + + StepVerifier.create(mono) + .expectNext(this.testMsg) + .verifyComplete(); + } + + @Test + public void decodeChunksToMono() { + byte[] body = this.testMsg.toByteArray(); + List chunks = new ArrayList<>(); + chunks.add(this.bufferFactory.wrap(Arrays.copyOfRange(body, 0, 4))); + chunks.add(this.bufferFactory.wrap(Arrays.copyOfRange(body, 4, body.length))); + Flux source = Flux.fromIterable(chunks); + ResolvableType elementType = forClass(Msg.class); + Mono mono = this.decoder.decodeToMono(source, elementType, null, + emptyMap()); + + StepVerifier.create(mono) + .expectNext(this.testMsg) + .verifyComplete(); + } + + @Test + public void decode() throws IOException { + Msg testMsg2 = Msg.newBuilder().setFoo("Bar").setBlah(SecondMsg.newBuilder().setBlah(456).build()).build(); + + DataBuffer buffer = bufferFactory.allocateBuffer(); + OutputStream outputStream = buffer.asOutputStream(); + this.testMsg.writeDelimitedTo(outputStream); + + DataBuffer buffer2 = bufferFactory.allocateBuffer(); + OutputStream outputStream2 = buffer2.asOutputStream(); + testMsg2.writeDelimitedTo(outputStream2); + + Flux source = Flux.just(buffer, buffer2); + ResolvableType elementType = forClass(Msg.class); + Flux messages = this.decoder.decode(source, elementType, null, emptyMap()); + + StepVerifier.create(messages) + .expectNext(this.testMsg) + .expectNext(testMsg2) + .verifyComplete(); + + DataBufferUtils.release(buffer); + DataBufferUtils.release(buffer2); + } + + @Test + public void decodeChunks() throws IOException { + Msg testMsg2 = Msg.newBuilder().setFoo("Bar").setBlah(SecondMsg.newBuilder().setBlah(456).build()).build(); + List chunks = new ArrayList<>(); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + this.testMsg.writeDelimitedTo(outputStream); + byte[] byteArray = outputStream.toByteArray(); + ByteArrayOutputStream outputStream2 = new ByteArrayOutputStream(); + testMsg2.writeDelimitedTo(outputStream2); + byte[] byteArray2 = outputStream2.toByteArray(); + + chunks.add(this.bufferFactory.wrap(Arrays.copyOfRange(byteArray, 0, 4))); + byte[] chunk2 = Arrays.copyOfRange(byteArray, 4, byteArray.length); + byte[] chunk3 = Arrays.copyOfRange(byteArray2, 0, 4); + byte[] combined = new byte[chunk2.length + chunk3.length]; + for (int i = 0; i < combined.length; ++i) + { + combined[i] = i < chunk2.length ? chunk2[i] : chunk3[i - chunk2.length]; + } + chunks.add(this.bufferFactory.wrap(combined)); + chunks.add(this.bufferFactory.wrap(Arrays.copyOfRange(byteArray2, 4, byteArray2.length))); + + Flux source = Flux.fromIterable(chunks); + ResolvableType elementType = forClass(Msg.class); + Flux messages = this.decoder.decode(source, elementType, null, emptyMap()); + + StepVerifier.create(messages) + .expectNext(this.testMsg) + .expectNext(testMsg2) + .verifyComplete(); + } + + @Test + public void exceedMaxSize() { + this.decoder.setMaxMessageSize(1); + byte[] body = this.testMsg.toByteArray(); + Flux source = Flux.just(this.bufferFactory.wrap(body)); + ResolvableType elementType = forClass(Msg.class); + Flux messages = this.decoder.decode(source, elementType, null, + emptyMap()); + + StepVerifier.create(messages) + .verifyError(DecodingException.class); + } + +} diff --git a/spring-web/src/test/java/org/springframework/http/codec/protobuf/ProtobufEncoderTests.java b/spring-web/src/test/java/org/springframework/http/codec/protobuf/ProtobufEncoderTests.java new file mode 100644 index 00000000000..b3a3ed90713 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/codec/protobuf/ProtobufEncoderTests.java @@ -0,0 +1,111 @@ +/* + * 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. + * 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.protobuf; + +import java.io.IOException; +import java.io.UncheckedIOException; + +import com.google.protobuf.Message; +import org.junit.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.http.MediaType; +import org.springframework.protobuf.Msg; +import org.springframework.protobuf.SecondMsg; +import org.springframework.util.MimeType; + +import static java.util.Collections.emptyMap; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.springframework.core.ResolvableType.forClass; + +/** + * Unit tests for {@link ProtobufEncoder}. + * + * @author Sebastien Deleuze + */ +public class ProtobufEncoderTests extends AbstractDataBufferAllocatingTestCase { + + private final static MimeType PROTOBUF_MIME_TYPE = new MimeType("application", "x-protobuf"); + + private final Msg testMsg = Msg.newBuilder().setFoo("Foo").setBlah(SecondMsg.newBuilder().setBlah(123).build()).build(); + + private final ProtobufEncoder encoder = new ProtobufEncoder(); + + @Test + public void canEncode() { + assertTrue(this.encoder.canEncode(forClass(Msg.class), null)); + assertTrue(this.encoder.canEncode(forClass(Msg.class), PROTOBUF_MIME_TYPE)); + assertTrue(this.encoder.canEncode(forClass(Msg.class), MediaType.APPLICATION_OCTET_STREAM)); + assertFalse(this.encoder.canEncode(forClass(Msg.class), MediaType.APPLICATION_JSON)); + assertFalse(this.encoder.canEncode(forClass(Object.class), PROTOBUF_MIME_TYPE)); + } + + @Test + public void encode() { + Mono message = Mono.just(this.testMsg); + ResolvableType elementType = forClass(Msg.class); + Flux output = this.encoder.encode(message, this.bufferFactory, elementType, PROTOBUF_MIME_TYPE, emptyMap()); + StepVerifier.create(output) + .consumeNextWith(dataBuffer -> { + try { + assertEquals(this.testMsg, Msg.parseFrom(dataBuffer.asInputStream())); + DataBufferUtils.release(dataBuffer); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }) + .verifyComplete(); + } + + @Test + public void encodeStream() { + Msg testMsg2 = Msg.newBuilder().setFoo("Bar").setBlah(SecondMsg.newBuilder().setBlah(456).build()).build(); + Flux messages = Flux.just(this.testMsg, testMsg2); + ResolvableType elementType = forClass(Msg.class); + Flux output = this.encoder.encode(messages, this.bufferFactory, elementType, PROTOBUF_MIME_TYPE, emptyMap()); + StepVerifier.create(output) + .consumeNextWith(dataBuffer -> { + try { + assertEquals(this.testMsg, Msg.parseDelimitedFrom(dataBuffer.asInputStream())); + DataBufferUtils.release(dataBuffer); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }) + .consumeNextWith(dataBuffer -> { + try { + assertEquals(testMsg2, Msg.parseDelimitedFrom(dataBuffer.asInputStream())); + DataBufferUtils.release(dataBuffer); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }) + .verifyComplete(); + } + +} diff --git a/spring-web/src/test/java/org/springframework/http/codec/support/ClientCodecConfigurerTests.java b/spring-web/src/test/java/org/springframework/http/codec/support/ClientCodecConfigurerTests.java index 98bbad413b1..617082b25a7 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/support/ClientCodecConfigurerTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/support/ClientCodecConfigurerTests.java @@ -53,6 +53,8 @@ import org.springframework.http.codec.json.Jackson2JsonEncoder; import org.springframework.http.codec.json.Jackson2SmileDecoder; import org.springframework.http.codec.json.Jackson2SmileEncoder; import org.springframework.http.codec.multipart.MultipartHttpMessageWriter; +import org.springframework.http.codec.protobuf.ProtobufDecoder; +import org.springframework.http.codec.protobuf.ProtobufHttpMessageWriter; import org.springframework.http.codec.xml.Jaxb2XmlDecoder; import org.springframework.http.codec.xml.Jaxb2XmlEncoder; import org.springframework.util.MimeTypeUtils; @@ -75,12 +77,13 @@ public class ClientCodecConfigurerTests { @Test public void defaultReaders() { List> readers = this.configurer.getReaders(); - assertEquals(11, readers.size()); + assertEquals(12, readers.size()); assertEquals(ByteArrayDecoder.class, getNextDecoder(readers).getClass()); assertEquals(ByteBufferDecoder.class, getNextDecoder(readers).getClass()); assertEquals(DataBufferDecoder.class, getNextDecoder(readers).getClass()); assertEquals(ResourceDecoder.class, getNextDecoder(readers).getClass()); assertStringDecoder(getNextDecoder(readers), true); + assertEquals(ProtobufDecoder.class, getNextDecoder(readers).getClass()); assertEquals(FormHttpMessageReader.class, readers.get(this.index.getAndIncrement()).getClass()); // SPR-16804 assertEquals(Jackson2JsonDecoder.class, getNextDecoder(readers).getClass()); assertEquals(Jackson2SmileDecoder.class, getNextDecoder(readers).getClass()); @@ -92,13 +95,14 @@ public class ClientCodecConfigurerTests { @Test public void defaultWriters() { List> writers = this.configurer.getWriters(); - assertEquals(10, writers.size()); + assertEquals(11, writers.size()); assertEquals(ByteArrayEncoder.class, getNextEncoder(writers).getClass()); assertEquals(ByteBufferEncoder.class, getNextEncoder(writers).getClass()); assertEquals(DataBufferEncoder.class, getNextEncoder(writers).getClass()); assertEquals(ResourceHttpMessageWriter.class, writers.get(index.getAndIncrement()).getClass()); assertStringEncoder(getNextEncoder(writers), true); assertEquals(MultipartHttpMessageWriter.class, writers.get(this.index.getAndIncrement()).getClass()); + assertEquals(ProtobufHttpMessageWriter.class, writers.get(index.getAndIncrement()).getClass()); assertEquals(Jackson2JsonEncoder.class, getNextEncoder(writers).getClass()); assertEquals(Jackson2SmileEncoder.class, getNextEncoder(writers).getClass()); assertEquals(Jaxb2XmlEncoder.class, getNextEncoder(writers).getClass()); diff --git a/spring-web/src/test/java/org/springframework/http/codec/support/CodecConfigurerTests.java b/spring-web/src/test/java/org/springframework/http/codec/support/CodecConfigurerTests.java index d0a08fa94e8..352995855e9 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/support/CodecConfigurerTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/support/CodecConfigurerTests.java @@ -19,6 +19,7 @@ package org.springframework.http.codec.support; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; +import com.google.protobuf.ExtensionRegistry; import org.junit.Test; import org.springframework.core.ResolvableType; @@ -45,6 +46,9 @@ import org.springframework.http.codec.json.Jackson2JsonDecoder; import org.springframework.http.codec.json.Jackson2JsonEncoder; import org.springframework.http.codec.json.Jackson2SmileDecoder; import org.springframework.http.codec.json.Jackson2SmileEncoder; +import org.springframework.http.codec.protobuf.ProtobufDecoder; +import org.springframework.http.codec.protobuf.ProtobufEncoder; +import org.springframework.http.codec.protobuf.ProtobufHttpMessageWriter; import org.springframework.http.codec.xml.Jaxb2XmlDecoder; import org.springframework.http.codec.xml.Jaxb2XmlEncoder; import org.springframework.util.MimeTypeUtils; @@ -56,6 +60,7 @@ import static org.springframework.core.ResolvableType.forClass; /** * Unit tests for {@link BaseDefaultCodecs}. * @author Rossen Stoyanchev + * @author Sebastien Deleuze */ public class CodecConfigurerTests { @@ -67,12 +72,13 @@ public class CodecConfigurerTests { @Test public void defaultReaders() { List> readers = this.configurer.getReaders(); - assertEquals(10, readers.size()); + assertEquals(11, readers.size()); assertEquals(ByteArrayDecoder.class, getNextDecoder(readers).getClass()); assertEquals(ByteBufferDecoder.class, getNextDecoder(readers).getClass()); assertEquals(DataBufferDecoder.class, getNextDecoder(readers).getClass()); assertEquals(ResourceDecoder.class, getNextDecoder(readers).getClass()); assertStringDecoder(getNextDecoder(readers), true); + assertEquals(ProtobufDecoder.class, getNextDecoder(readers).getClass()); assertEquals(FormHttpMessageReader.class, readers.get(this.index.getAndIncrement()).getClass()); assertEquals(Jackson2JsonDecoder.class, getNextDecoder(readers).getClass()); assertEquals(Jackson2SmileDecoder.class, getNextDecoder(readers).getClass()); @@ -83,12 +89,13 @@ public class CodecConfigurerTests { @Test public void defaultWriters() { List> writers = this.configurer.getWriters(); - assertEquals(9, writers.size()); + assertEquals(10, writers.size()); assertEquals(ByteArrayEncoder.class, getNextEncoder(writers).getClass()); assertEquals(ByteBufferEncoder.class, getNextEncoder(writers).getClass()); assertEquals(DataBufferEncoder.class, getNextEncoder(writers).getClass()); assertEquals(ResourceHttpMessageWriter.class, writers.get(index.getAndIncrement()).getClass()); assertStringEncoder(getNextEncoder(writers), true); + assertEquals(ProtobufHttpMessageWriter.class, writers.get(index.getAndIncrement()).getClass()); assertEquals(Jackson2JsonEncoder.class, getNextEncoder(writers).getClass()); assertEquals(Jackson2SmileEncoder.class, getNextEncoder(writers).getClass()); assertEquals(Jaxb2XmlEncoder.class, getNextEncoder(writers).getClass()); @@ -117,12 +124,13 @@ public class CodecConfigurerTests { List> readers = this.configurer.getReaders(); - assertEquals(14, readers.size()); + assertEquals(15, readers.size()); assertEquals(ByteArrayDecoder.class, getNextDecoder(readers).getClass()); assertEquals(ByteBufferDecoder.class, getNextDecoder(readers).getClass()); assertEquals(DataBufferDecoder.class, getNextDecoder(readers).getClass()); assertEquals(ResourceDecoder.class, getNextDecoder(readers).getClass()); assertEquals(StringDecoder.class, getNextDecoder(readers).getClass()); + assertEquals(ProtobufDecoder.class, getNextDecoder(readers).getClass()); assertEquals(FormHttpMessageReader.class, readers.get(this.index.getAndIncrement()).getClass()); assertSame(customDecoder1, getNextDecoder(readers)); assertSame(customReader1, readers.get(this.index.getAndIncrement())); @@ -156,12 +164,13 @@ public class CodecConfigurerTests { List> writers = this.configurer.getWriters(); - assertEquals(13, writers.size()); + assertEquals(14, writers.size()); assertEquals(ByteArrayEncoder.class, getNextEncoder(writers).getClass()); assertEquals(ByteBufferEncoder.class, getNextEncoder(writers).getClass()); assertEquals(DataBufferEncoder.class, getNextEncoder(writers).getClass()); assertEquals(ResourceHttpMessageWriter.class, writers.get(index.getAndIncrement()).getClass()); assertEquals(CharSequenceEncoder.class, getNextEncoder(writers).getClass()); + assertEquals(ProtobufHttpMessageWriter.class, writers.get(index.getAndIncrement()).getClass()); assertSame(customEncoder1, getNextEncoder(writers)); assertSame(customWriter1, writers.get(this.index.getAndIncrement())); assertEquals(Jackson2JsonEncoder.class, getNextEncoder(writers).getClass()); @@ -247,6 +256,19 @@ public class CodecConfigurerTests { .filter(e -> e == decoder).orElse(null)); } + @Test + public void protobufDecoderOverride() { + ProtobufDecoder decoder = new ProtobufDecoder(ExtensionRegistry.newInstance()); + this.configurer.defaultCodecs().protobufDecoder(decoder); + + assertSame(decoder, this.configurer.getReaders().stream() + .filter(writer -> writer instanceof DecoderHttpMessageReader) + .map(writer -> ((DecoderHttpMessageReader) writer).getDecoder()) + .filter(e -> ProtobufDecoder.class.equals(e.getClass())) + .findFirst() + .filter(e -> e == decoder).orElse(null)); + } + @Test public void jackson2EncoderOverride() { Jackson2JsonEncoder encoder = new Jackson2JsonEncoder(); @@ -260,6 +282,20 @@ public class CodecConfigurerTests { .filter(e -> e == encoder).orElse(null)); } + @Test + public void protobufWriterOverride() { + ProtobufEncoder encoder = new ProtobufEncoder(); + ProtobufHttpMessageWriter messageWriter = new ProtobufHttpMessageWriter(encoder); + this.configurer.defaultCodecs().protobufWriter(messageWriter); + + assertSame(encoder, this.configurer.getWriters().stream() + .filter(writer -> writer instanceof EncoderHttpMessageWriter) + .map(writer -> ((EncoderHttpMessageWriter) writer).getEncoder()) + .filter(e -> ProtobufEncoder.class.equals(e.getClass())) + .findFirst() + .filter(e -> e == encoder).orElse(null)); + } + private Decoder getNextDecoder(List> readers) { HttpMessageReader reader = readers.get(this.index.getAndIncrement()); diff --git a/spring-web/src/test/java/org/springframework/http/codec/support/ServerCodecConfigurerTests.java b/spring-web/src/test/java/org/springframework/http/codec/support/ServerCodecConfigurerTests.java index cee6aec2138..15408dc9066 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/support/ServerCodecConfigurerTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/support/ServerCodecConfigurerTests.java @@ -54,6 +54,8 @@ import org.springframework.http.codec.json.Jackson2SmileDecoder; import org.springframework.http.codec.json.Jackson2SmileEncoder; import org.springframework.http.codec.multipart.MultipartHttpMessageReader; import org.springframework.http.codec.multipart.SynchronossPartHttpMessageReader; +import org.springframework.http.codec.protobuf.ProtobufDecoder; +import org.springframework.http.codec.protobuf.ProtobufHttpMessageWriter; import org.springframework.http.codec.xml.Jaxb2XmlDecoder; import org.springframework.http.codec.xml.Jaxb2XmlEncoder; import org.springframework.util.MimeTypeUtils; @@ -76,12 +78,13 @@ public class ServerCodecConfigurerTests { @Test public void defaultReaders() { List> readers = this.configurer.getReaders(); - assertEquals(12, readers.size()); + assertEquals(13, readers.size()); assertEquals(ByteArrayDecoder.class, getNextDecoder(readers).getClass()); assertEquals(ByteBufferDecoder.class, getNextDecoder(readers).getClass()); assertEquals(DataBufferDecoder.class, getNextDecoder(readers).getClass()); assertEquals(ResourceDecoder.class, getNextDecoder(readers).getClass()); assertStringDecoder(getNextDecoder(readers), true); + assertEquals(ProtobufDecoder.class, getNextDecoder(readers).getClass()); assertEquals(FormHttpMessageReader.class, readers.get(this.index.getAndIncrement()).getClass()); assertEquals(SynchronossPartHttpMessageReader.class, readers.get(this.index.getAndIncrement()).getClass()); assertEquals(MultipartHttpMessageReader.class, readers.get(this.index.getAndIncrement()).getClass()); @@ -94,12 +97,13 @@ public class ServerCodecConfigurerTests { @Test public void defaultWriters() { List> writers = this.configurer.getWriters(); - assertEquals(10, writers.size()); + assertEquals(11, writers.size()); assertEquals(ByteArrayEncoder.class, getNextEncoder(writers).getClass()); assertEquals(ByteBufferEncoder.class, getNextEncoder(writers).getClass()); assertEquals(DataBufferEncoder.class, getNextEncoder(writers).getClass()); assertEquals(ResourceHttpMessageWriter.class, writers.get(index.getAndIncrement()).getClass()); assertStringEncoder(getNextEncoder(writers), true); + assertEquals(ProtobufHttpMessageWriter.class, writers.get(index.getAndIncrement()).getClass()); assertEquals(Jackson2JsonEncoder.class, getNextEncoder(writers).getClass()); assertEquals(Jackson2SmileEncoder.class, getNextEncoder(writers).getClass()); assertEquals(Jaxb2XmlEncoder.class, getNextEncoder(writers).getClass()); diff --git a/spring-webflux/spring-webflux.gradle b/spring-webflux/spring-webflux.gradle index 091f98bba76..12e92ba3df7 100644 --- a/spring-webflux/spring-webflux.gradle +++ b/spring-webflux/spring-webflux.gradle @@ -39,6 +39,7 @@ dependencies { } optional("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}") optional("org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}") + optional("com.google.protobuf:protobuf-java-util:3.6.0") testCompile("javax.xml.bind:jaxb-api:2.3.0") testCompile("org.hibernate:hibernate-validator:6.0.11.Final") testCompile "io.reactivex.rxjava2:rxjava:${rxjava2Version}" diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/config/DelegatingWebFluxConfigurationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/config/DelegatingWebFluxConfigurationTests.java index 941b948fde5..f770a7927fb 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/config/DelegatingWebFluxConfigurationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/config/DelegatingWebFluxConfigurationTests.java @@ -95,7 +95,7 @@ public class DelegatingWebFluxConfigurationTests { assertNotNull(initializer); assertTrue(initializer.getValidator() instanceof LocalValidatorFactoryBean); assertSame(formatterRegistry.getValue(), initializer.getConversionService()); - assertEquals(12, codecsConfigurer.getValue().getReaders().size()); + assertEquals(13, codecsConfigurer.getValue().getReaders().size()); } @Test diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/config/WebFluxConfigurationSupportTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/config/WebFluxConfigurationSupportTests.java index ea9a9221b67..108e92d516a 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/config/WebFluxConfigurationSupportTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/config/WebFluxConfigurationSupportTests.java @@ -24,6 +24,7 @@ import java.util.List; import java.util.Map; import javax.xml.bind.annotation.XmlRootElement; +import com.google.protobuf.Message; import org.junit.Test; import org.springframework.context.ApplicationContext; @@ -148,7 +149,7 @@ public class WebFluxConfigurationSupportTests { assertNotNull(adapter); List> readers = adapter.getMessageReaders(); - assertEquals(12, readers.size()); + assertEquals(13, readers.size()); ResolvableType multiValueMapType = forClassWithGenerics(MultiValueMap.class, String.class, String.class); @@ -156,6 +157,7 @@ public class WebFluxConfigurationSupportTests { assertHasMessageReader(readers, forClass(ByteBuffer.class), APPLICATION_OCTET_STREAM); assertHasMessageReader(readers, forClass(String.class), TEXT_PLAIN); assertHasMessageReader(readers, forClass(Resource.class), IMAGE_PNG); + assertHasMessageReader(readers, forClass(Message.class), new MediaType("application", "x-protobuf")); assertHasMessageReader(readers, multiValueMapType, APPLICATION_FORM_URLENCODED); assertHasMessageReader(readers, forClass(TestBean.class), APPLICATION_XML); assertHasMessageReader(readers, forClass(TestBean.class), APPLICATION_JSON); @@ -202,12 +204,13 @@ public class WebFluxConfigurationSupportTests { assertEquals(0, handler.getOrder()); List> writers = handler.getMessageWriters(); - assertEquals(10, writers.size()); + assertEquals(11, writers.size()); assertHasMessageWriter(writers, forClass(byte[].class), APPLICATION_OCTET_STREAM); assertHasMessageWriter(writers, forClass(ByteBuffer.class), APPLICATION_OCTET_STREAM); assertHasMessageWriter(writers, forClass(String.class), TEXT_PLAIN); assertHasMessageWriter(writers, forClass(Resource.class), IMAGE_PNG); + assertHasMessageWriter(writers, forClass(Message.class), new MediaType("application", "x-protobuf")); assertHasMessageWriter(writers, forClass(TestBean.class), APPLICATION_XML); assertHasMessageWriter(writers, forClass(TestBean.class), APPLICATION_JSON); assertHasMessageWriter(writers, forClass(TestBean.class), new MediaType("application", "x-jackson-smile")); @@ -229,12 +232,13 @@ public class WebFluxConfigurationSupportTests { assertEquals(100, handler.getOrder()); List> writers = handler.getMessageWriters(); - assertEquals(10, writers.size()); + assertEquals(11, writers.size()); assertHasMessageWriter(writers, forClass(byte[].class), APPLICATION_OCTET_STREAM); assertHasMessageWriter(writers, forClass(ByteBuffer.class), APPLICATION_OCTET_STREAM); assertHasMessageWriter(writers, forClass(String.class), TEXT_PLAIN); assertHasMessageWriter(writers, forClass(Resource.class), IMAGE_PNG); + assertHasMessageWriter(writers, forClass(Message.class), new MediaType("application", "x-protobuf")); assertHasMessageWriter(writers, forClass(TestBean.class), APPLICATION_XML); assertHasMessageWriter(writers, forClass(TestBean.class), APPLICATION_JSON); assertHasMessageWriter(writers, forClass(TestBean.class), new MediaType("application", "x-jackson-smile")); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/protobuf/Msg.java b/spring-webflux/src/test/java/org/springframework/web/reactive/protobuf/Msg.java new file mode 100644 index 00000000000..7b8800fdd10 --- /dev/null +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/protobuf/Msg.java @@ -0,0 +1,654 @@ +/* + * 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. + * 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. + */ + +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: sample.proto + +package org.springframework.web.reactive.protobuf; + +/** + * Protobuf type {@code Msg} + */ +public final class Msg extends + com.google.protobuf.GeneratedMessage + implements MsgOrBuilder { + // Use Msg.newBuilder() to construct. + private Msg(com.google.protobuf.GeneratedMessage.Builder builder) { + super(builder); + this.unknownFields = builder.getUnknownFields(); + } + private Msg(boolean noInit) { this.unknownFields = com.google.protobuf.UnknownFieldSet.getDefaultInstance(); } + + private static final Msg defaultInstance; + public static Msg getDefaultInstance() { + return defaultInstance; + } + + public Msg getDefaultInstanceForType() { + return defaultInstance; + } + + private final com.google.protobuf.UnknownFieldSet unknownFields; + @Override + public final com.google.protobuf.UnknownFieldSet + getUnknownFields() { + return this.unknownFields; + } + private Msg( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + initFields(); + @SuppressWarnings("unused") + int mutable_bitField0_ = 0; + com.google.protobuf.UnknownFieldSet.Builder unknownFields = + com.google.protobuf.UnknownFieldSet.newBuilder(); + try { + boolean done = false; + while (!done) { + int tag = input.readTag(); + switch (tag) { + case 0: + done = true; + break; + default: { + if (!parseUnknownField(input, unknownFields, + extensionRegistry, tag)) { + done = true; + } + break; + } + case 10: { + bitField0_ |= 0x00000001; + foo_ = input.readBytes(); + break; + } + case 18: { + SecondMsg.Builder subBuilder = null; + if (((bitField0_ & 0x00000002) == 0x00000002)) { + subBuilder = blah_.toBuilder(); + } + blah_ = input.readMessage(SecondMsg.PARSER, extensionRegistry); + if (subBuilder != null) { + subBuilder.mergeFrom(blah_); + blah_ = subBuilder.buildPartial(); + } + bitField0_ |= 0x00000002; + break; + } + } + } + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.setUnfinishedMessage(this); + } catch (java.io.IOException e) { + throw new com.google.protobuf.InvalidProtocolBufferException( + e.getMessage()).setUnfinishedMessage(this); + } finally { + this.unknownFields = unknownFields.build(); + makeExtensionsImmutable(); + } + } + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return OuterSample.internal_static_Msg_descriptor; + } + + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return OuterSample.internal_static_Msg_fieldAccessorTable + .ensureFieldAccessorsInitialized( + Msg.class, Msg.Builder.class); + } + + public static com.google.protobuf.Parser PARSER = + new com.google.protobuf.AbstractParser() { + public Msg parsePartialFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return new Msg(input, extensionRegistry); + } + }; + + @Override + public com.google.protobuf.Parser getParserForType() { + return PARSER; + } + + private int bitField0_; + // optional string foo = 1; + public static final int FOO_FIELD_NUMBER = 1; + private Object foo_; + /** + * optional string foo = 1; + */ + public boolean hasFoo() { + return ((bitField0_ & 0x00000001) == 0x00000001); + } + /** + * optional string foo = 1; + */ + public String getFoo() { + Object ref = foo_; + if (ref instanceof String) { + return (String) ref; + } else { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + String s = bs.toStringUtf8(); + if (bs.isValidUtf8()) { + foo_ = s; + } + return s; + } + } + /** + * optional string foo = 1; + */ + public com.google.protobuf.ByteString + getFooBytes() { + Object ref = foo_; + if (ref instanceof String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (String) ref); + foo_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + + // optional .SecondMsg blah = 2; + public static final int BLAH_FIELD_NUMBER = 2; + private SecondMsg blah_; + /** + * optional .SecondMsg blah = 2; + */ + public boolean hasBlah() { + return ((bitField0_ & 0x00000002) == 0x00000002); + } + /** + * optional .SecondMsg blah = 2; + */ + public SecondMsg getBlah() { + return blah_; + } + /** + * optional .SecondMsg blah = 2; + */ + public SecondMsgOrBuilder getBlahOrBuilder() { + return blah_; + } + + private void initFields() { + foo_ = ""; + blah_ = SecondMsg.getDefaultInstance(); + } + private byte memoizedIsInitialized = -1; + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized != -1) return isInitialized == 1; + + memoizedIsInitialized = 1; + return true; + } + + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + getSerializedSize(); + if (((bitField0_ & 0x00000001) == 0x00000001)) { + output.writeBytes(1, getFooBytes()); + } + if (((bitField0_ & 0x00000002) == 0x00000002)) { + output.writeMessage(2, blah_); + } + getUnknownFields().writeTo(output); + } + + private int memoizedSerializedSize = -1; + public int getSerializedSize() { + int size = memoizedSerializedSize; + if (size != -1) return size; + + size = 0; + if (((bitField0_ & 0x00000001) == 0x00000001)) { + size += com.google.protobuf.CodedOutputStream + .computeBytesSize(1, getFooBytes()); + } + if (((bitField0_ & 0x00000002) == 0x00000002)) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(2, blah_); + } + size += getUnknownFields().getSerializedSize(); + memoizedSerializedSize = size; + return size; + } + + private static final long serialVersionUID = 0L; + @Override + protected Object writeReplace() + throws java.io.ObjectStreamException { + return super.writeReplace(); + } + + public static Msg parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static Msg parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static Msg parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static Msg parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static Msg parseFrom(java.io.InputStream input) + throws java.io.IOException { + return PARSER.parseFrom(input); + } + public static Msg parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return PARSER.parseFrom(input, extensionRegistry); + } + public static Msg parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + return PARSER.parseDelimitedFrom(input); + } + public static Msg parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return PARSER.parseDelimitedFrom(input, extensionRegistry); + } + public static Msg parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return PARSER.parseFrom(input); + } + public static Msg parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return PARSER.parseFrom(input, extensionRegistry); + } + + public static Builder newBuilder() { return Builder.create(); } + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder(Msg prototype) { + return newBuilder().mergeFrom(prototype); + } + public Builder toBuilder() { return newBuilder(this); } + + @Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + /** + * Protobuf type {@code Msg} + */ + public static final class Builder extends + com.google.protobuf.GeneratedMessage.Builder + implements MsgOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return OuterSample.internal_static_Msg_descriptor; + } + + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return OuterSample.internal_static_Msg_fieldAccessorTable + .ensureFieldAccessorsInitialized( + Msg.class, Msg.Builder.class); + } + + // Construct using org.springframework.protobuf.Msg.newBuilder() + private Builder() { + maybeForceBuilderInitialization(); + } + + private Builder( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + super(parent); + maybeForceBuilderInitialization(); + } + private void maybeForceBuilderInitialization() { + if (com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders) { + getBlahFieldBuilder(); + } + } + private static Builder create() { + return new Builder(); + } + + public Builder clear() { + super.clear(); + foo_ = ""; + bitField0_ = (bitField0_ & ~0x00000001); + if (blahBuilder_ == null) { + blah_ = SecondMsg.getDefaultInstance(); + } else { + blahBuilder_.clear(); + } + bitField0_ = (bitField0_ & ~0x00000002); + return this; + } + + public Builder clone() { + return create().mergeFrom(buildPartial()); + } + + public com.google.protobuf.Descriptors.Descriptor + getDescriptorForType() { + return OuterSample.internal_static_Msg_descriptor; + } + + public Msg getDefaultInstanceForType() { + return Msg.getDefaultInstance(); + } + + public Msg build() { + Msg result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + public Msg buildPartial() { + Msg result = new Msg(this); + int from_bitField0_ = bitField0_; + int to_bitField0_ = 0; + if (((from_bitField0_ & 0x00000001) == 0x00000001)) { + to_bitField0_ |= 0x00000001; + } + result.foo_ = foo_; + if (((from_bitField0_ & 0x00000002) == 0x00000002)) { + to_bitField0_ |= 0x00000002; + } + if (blahBuilder_ == null) { + result.blah_ = blah_; + } else { + result.blah_ = blahBuilder_.build(); + } + result.bitField0_ = to_bitField0_; + onBuilt(); + return result; + } + + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof Msg) { + return mergeFrom((Msg)other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(Msg other) { + if (other == Msg.getDefaultInstance()) return this; + if (other.hasFoo()) { + bitField0_ |= 0x00000001; + foo_ = other.foo_; + onChanged(); + } + if (other.hasBlah()) { + mergeBlah(other.getBlah()); + } + this.mergeUnknownFields(other.getUnknownFields()); + return this; + } + + public final boolean isInitialized() { + return true; + } + + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + Msg parsedMessage = null; + try { + parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry); + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + parsedMessage = (Msg) e.getUnfinishedMessage(); + throw e; + } finally { + if (parsedMessage != null) { + mergeFrom(parsedMessage); + } + } + return this; + } + private int bitField0_; + + // optional string foo = 1; + private Object foo_ = ""; + /** + * optional string foo = 1; + */ + public boolean hasFoo() { + return ((bitField0_ & 0x00000001) == 0x00000001); + } + /** + * optional string foo = 1; + */ + public String getFoo() { + Object ref = foo_; + if (!(ref instanceof String)) { + String s = ((com.google.protobuf.ByteString) ref) + .toStringUtf8(); + foo_ = s; + return s; + } else { + return (String) ref; + } + } + /** + * optional string foo = 1; + */ + public com.google.protobuf.ByteString + getFooBytes() { + Object ref = foo_; + if (ref instanceof String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (String) ref); + foo_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + /** + * optional string foo = 1; + */ + public Builder setFoo( + String value) { + if (value == null) { + throw new NullPointerException(); + } + bitField0_ |= 0x00000001; + foo_ = value; + onChanged(); + return this; + } + /** + * optional string foo = 1; + */ + public Builder clearFoo() { + bitField0_ = (bitField0_ & ~0x00000001); + foo_ = getDefaultInstance().getFoo(); + onChanged(); + return this; + } + /** + * optional string foo = 1; + */ + public Builder setFooBytes( + com.google.protobuf.ByteString value) { + if (value == null) { + throw new NullPointerException(); + } + bitField0_ |= 0x00000001; + foo_ = value; + onChanged(); + return this; + } + + // optional .SecondMsg blah = 2; + private SecondMsg blah_ = SecondMsg.getDefaultInstance(); + private com.google.protobuf.SingleFieldBuilder< + SecondMsg, SecondMsg.Builder, + SecondMsgOrBuilder> blahBuilder_; + /** + * optional .SecondMsg blah = 2; + */ + public boolean hasBlah() { + return ((bitField0_ & 0x00000002) == 0x00000002); + } + /** + * optional .SecondMsg blah = 2; + */ + public SecondMsg getBlah() { + if (blahBuilder_ == null) { + return blah_; + } else { + return blahBuilder_.getMessage(); + } + } + /** + * optional .SecondMsg blah = 2; + */ + public Builder setBlah(SecondMsg value) { + if (blahBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + blah_ = value; + onChanged(); + } else { + blahBuilder_.setMessage(value); + } + bitField0_ |= 0x00000002; + return this; + } + /** + * optional .SecondMsg blah = 2; + */ + public Builder setBlah( + SecondMsg.Builder builderForValue) { + if (blahBuilder_ == null) { + blah_ = builderForValue.build(); + onChanged(); + } else { + blahBuilder_.setMessage(builderForValue.build()); + } + bitField0_ |= 0x00000002; + return this; + } + /** + * optional .SecondMsg blah = 2; + */ + public Builder mergeBlah(SecondMsg value) { + if (blahBuilder_ == null) { + if (((bitField0_ & 0x00000002) == 0x00000002) && + blah_ != SecondMsg.getDefaultInstance()) { + blah_ = + SecondMsg.newBuilder(blah_).mergeFrom(value).buildPartial(); + } else { + blah_ = value; + } + onChanged(); + } else { + blahBuilder_.mergeFrom(value); + } + bitField0_ |= 0x00000002; + return this; + } + /** + * optional .SecondMsg blah = 2; + */ + public Builder clearBlah() { + if (blahBuilder_ == null) { + blah_ = SecondMsg.getDefaultInstance(); + onChanged(); + } else { + blahBuilder_.clear(); + } + bitField0_ = (bitField0_ & ~0x00000002); + return this; + } + /** + * optional .SecondMsg blah = 2; + */ + public SecondMsg.Builder getBlahBuilder() { + bitField0_ |= 0x00000002; + onChanged(); + return getBlahFieldBuilder().getBuilder(); + } + /** + * optional .SecondMsg blah = 2; + */ + public SecondMsgOrBuilder getBlahOrBuilder() { + if (blahBuilder_ != null) { + return blahBuilder_.getMessageOrBuilder(); + } else { + return blah_; + } + } + /** + * optional .SecondMsg blah = 2; + */ + private com.google.protobuf.SingleFieldBuilder< + SecondMsg, SecondMsg.Builder, + SecondMsgOrBuilder> + getBlahFieldBuilder() { + if (blahBuilder_ == null) { + blahBuilder_ = new com.google.protobuf.SingleFieldBuilder<>( + blah_, + getParentForChildren(), + isClean()); + blah_ = null; + } + return blahBuilder_; + } + + // @@protoc_insertion_point(builder_scope:Msg) + } + + static { + defaultInstance = new Msg(true); + defaultInstance.initFields(); + } + + // @@protoc_insertion_point(class_scope:Msg) +} + diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/protobuf/MsgOrBuilder.java b/spring-webflux/src/test/java/org/springframework/web/reactive/protobuf/MsgOrBuilder.java new file mode 100644 index 00000000000..83466057675 --- /dev/null +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/protobuf/MsgOrBuilder.java @@ -0,0 +1,37 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: sample.proto + +package org.springframework.web.reactive.protobuf; + +public interface MsgOrBuilder + extends com.google.protobuf.MessageOrBuilder { + + // optional string foo = 1; + /** + * optional string foo = 1; + */ + boolean hasFoo(); + /** + * optional string foo = 1; + */ + String getFoo(); + /** + * optional string foo = 1; + */ + com.google.protobuf.ByteString + getFooBytes(); + + // optional .SecondMsg blah = 2; + /** + * optional .SecondMsg blah = 2; + */ + boolean hasBlah(); + /** + * optional .SecondMsg blah = 2; + */ + SecondMsg getBlah(); + /** + * optional .SecondMsg blah = 2; + */ + SecondMsgOrBuilder getBlahOrBuilder(); +} diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/protobuf/OuterSample.java b/spring-webflux/src/test/java/org/springframework/web/reactive/protobuf/OuterSample.java new file mode 100644 index 00000000000..c6e59e4ecf7 --- /dev/null +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/protobuf/OuterSample.java @@ -0,0 +1,62 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: sample.proto + +package org.springframework.web.reactive.protobuf; + +public class OuterSample { + private OuterSample() {} + public static void registerAllExtensions( + com.google.protobuf.ExtensionRegistry registry) { + } + static com.google.protobuf.Descriptors.Descriptor + internal_static_Msg_descriptor; + static + com.google.protobuf.GeneratedMessage.FieldAccessorTable + internal_static_Msg_fieldAccessorTable; + static com.google.protobuf.Descriptors.Descriptor + internal_static_SecondMsg_descriptor; + static + com.google.protobuf.GeneratedMessage.FieldAccessorTable + internal_static_SecondMsg_fieldAccessorTable; + + public static com.google.protobuf.Descriptors.FileDescriptor + getDescriptor() { + return descriptor; + } + private static com.google.protobuf.Descriptors.FileDescriptor + descriptor; + static { + String[] descriptorData = { + "\n\014sample.proto\",\n\003Msg\022\013\n\003foo\030\001 \001(\t\022\030\n\004bl" + + "ah\030\002 \001(\0132\n.SecondMsg\"\031\n\tSecondMsg\022\014\n\004bla" + + "h\030\001 \001(\005B-\n\034org.springframework.protobufB" + + "\013OuterSampleP\001" + }; + com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner = + new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() { + public com.google.protobuf.ExtensionRegistry assignDescriptors( + com.google.protobuf.Descriptors.FileDescriptor root) { + descriptor = root; + internal_static_Msg_descriptor = + getDescriptor().getMessageTypes().get(0); + internal_static_Msg_fieldAccessorTable = new + com.google.protobuf.GeneratedMessage.FieldAccessorTable( + internal_static_Msg_descriptor, + new String[] { "Foo", "Blah", }); + internal_static_SecondMsg_descriptor = + getDescriptor().getMessageTypes().get(1); + internal_static_SecondMsg_fieldAccessorTable = new + com.google.protobuf.GeneratedMessage.FieldAccessorTable( + internal_static_SecondMsg_descriptor, + new String[] { "Blah", }); + return null; + } + }; + com.google.protobuf.Descriptors.FileDescriptor + .internalBuildGeneratedFileFrom(descriptorData, + new com.google.protobuf.Descriptors.FileDescriptor[] { + }, assigner); + } + + // @@protoc_insertion_point(outer_class_scope) +} diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/protobuf/SecondMsg.java b/spring-webflux/src/test/java/org/springframework/web/reactive/protobuf/SecondMsg.java new file mode 100644 index 00000000000..98b91f45507 --- /dev/null +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/protobuf/SecondMsg.java @@ -0,0 +1,389 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: sample.proto + +package org.springframework.web.reactive.protobuf; + +/** + * Protobuf type {@code SecondMsg} + */ +public final class SecondMsg extends + com.google.protobuf.GeneratedMessage + implements SecondMsgOrBuilder { + // Use SecondMsg.newBuilder() to construct. + private SecondMsg(com.google.protobuf.GeneratedMessage.Builder builder) { + super(builder); + this.unknownFields = builder.getUnknownFields(); + } + private SecondMsg(boolean noInit) { this.unknownFields = com.google.protobuf.UnknownFieldSet.getDefaultInstance(); } + + private static final SecondMsg defaultInstance; + public static SecondMsg getDefaultInstance() { + return defaultInstance; + } + + public SecondMsg getDefaultInstanceForType() { + return defaultInstance; + } + + private final com.google.protobuf.UnknownFieldSet unknownFields; + @Override + public final com.google.protobuf.UnknownFieldSet + getUnknownFields() { + return this.unknownFields; + } + private SecondMsg( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + initFields(); + @SuppressWarnings("unused") + int mutable_bitField0_ = 0; + com.google.protobuf.UnknownFieldSet.Builder unknownFields = + com.google.protobuf.UnknownFieldSet.newBuilder(); + try { + boolean done = false; + while (!done) { + int tag = input.readTag(); + switch (tag) { + case 0: + done = true; + break; + default: { + if (!parseUnknownField(input, unknownFields, + extensionRegistry, tag)) { + done = true; + } + break; + } + case 8: { + bitField0_ |= 0x00000001; + blah_ = input.readInt32(); + break; + } + } + } + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.setUnfinishedMessage(this); + } catch (java.io.IOException e) { + throw new com.google.protobuf.InvalidProtocolBufferException( + e.getMessage()).setUnfinishedMessage(this); + } finally { + this.unknownFields = unknownFields.build(); + makeExtensionsImmutable(); + } + } + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return OuterSample.internal_static_SecondMsg_descriptor; + } + + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return OuterSample.internal_static_SecondMsg_fieldAccessorTable + .ensureFieldAccessorsInitialized( + SecondMsg.class, SecondMsg.Builder.class); + } + + public static com.google.protobuf.Parser PARSER = + new com.google.protobuf.AbstractParser() { + public SecondMsg parsePartialFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return new SecondMsg(input, extensionRegistry); + } + }; + + @Override + public com.google.protobuf.Parser getParserForType() { + return PARSER; + } + + private int bitField0_; + // optional int32 blah = 1; + public static final int BLAH_FIELD_NUMBER = 1; + private int blah_; + /** + * optional int32 blah = 1; + */ + public boolean hasBlah() { + return ((bitField0_ & 0x00000001) == 0x00000001); + } + /** + * optional int32 blah = 1; + */ + public int getBlah() { + return blah_; + } + + private void initFields() { + blah_ = 0; + } + private byte memoizedIsInitialized = -1; + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized != -1) return isInitialized == 1; + + memoizedIsInitialized = 1; + return true; + } + + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + getSerializedSize(); + if (((bitField0_ & 0x00000001) == 0x00000001)) { + output.writeInt32(1, blah_); + } + getUnknownFields().writeTo(output); + } + + private int memoizedSerializedSize = -1; + public int getSerializedSize() { + int size = memoizedSerializedSize; + if (size != -1) return size; + + size = 0; + if (((bitField0_ & 0x00000001) == 0x00000001)) { + size += com.google.protobuf.CodedOutputStream + .computeInt32Size(1, blah_); + } + size += getUnknownFields().getSerializedSize(); + memoizedSerializedSize = size; + return size; + } + + private static final long serialVersionUID = 0L; + @Override + protected Object writeReplace() + throws java.io.ObjectStreamException { + return super.writeReplace(); + } + + public static SecondMsg parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static SecondMsg parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static SecondMsg parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static SecondMsg parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static SecondMsg parseFrom(java.io.InputStream input) + throws java.io.IOException { + return PARSER.parseFrom(input); + } + public static SecondMsg parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return PARSER.parseFrom(input, extensionRegistry); + } + public static SecondMsg parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + return PARSER.parseDelimitedFrom(input); + } + public static SecondMsg parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return PARSER.parseDelimitedFrom(input, extensionRegistry); + } + public static SecondMsg parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return PARSER.parseFrom(input); + } + public static SecondMsg parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return PARSER.parseFrom(input, extensionRegistry); + } + + public static Builder newBuilder() { return Builder.create(); } + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder(SecondMsg prototype) { + return newBuilder().mergeFrom(prototype); + } + public Builder toBuilder() { return newBuilder(this); } + + @Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + /** + * Protobuf type {@code SecondMsg} + */ + public static final class Builder extends + com.google.protobuf.GeneratedMessage.Builder + implements SecondMsgOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return OuterSample.internal_static_SecondMsg_descriptor; + } + + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return OuterSample.internal_static_SecondMsg_fieldAccessorTable + .ensureFieldAccessorsInitialized( + SecondMsg.class, SecondMsg.Builder.class); + } + + // Construct using org.springframework.protobuf.SecondMsg.newBuilder() + private Builder() { + maybeForceBuilderInitialization(); + } + + private Builder( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + super(parent); + maybeForceBuilderInitialization(); + } + private void maybeForceBuilderInitialization() { + if (com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders) { + } + } + private static Builder create() { + return new Builder(); + } + + public Builder clear() { + super.clear(); + blah_ = 0; + bitField0_ = (bitField0_ & ~0x00000001); + return this; + } + + public Builder clone() { + return create().mergeFrom(buildPartial()); + } + + public com.google.protobuf.Descriptors.Descriptor + getDescriptorForType() { + return OuterSample.internal_static_SecondMsg_descriptor; + } + + public SecondMsg getDefaultInstanceForType() { + return SecondMsg.getDefaultInstance(); + } + + public SecondMsg build() { + SecondMsg result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + public SecondMsg buildPartial() { + SecondMsg result = new SecondMsg(this); + int from_bitField0_ = bitField0_; + int to_bitField0_ = 0; + if (((from_bitField0_ & 0x00000001) == 0x00000001)) { + to_bitField0_ |= 0x00000001; + } + result.blah_ = blah_; + result.bitField0_ = to_bitField0_; + onBuilt(); + return result; + } + + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof SecondMsg) { + return mergeFrom((SecondMsg)other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(SecondMsg other) { + if (other == SecondMsg.getDefaultInstance()) return this; + if (other.hasBlah()) { + setBlah(other.getBlah()); + } + this.mergeUnknownFields(other.getUnknownFields()); + return this; + } + + public final boolean isInitialized() { + return true; + } + + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + SecondMsg parsedMessage = null; + try { + parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry); + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + parsedMessage = (SecondMsg) e.getUnfinishedMessage(); + throw e; + } finally { + if (parsedMessage != null) { + mergeFrom(parsedMessage); + } + } + return this; + } + private int bitField0_; + + // optional int32 blah = 1; + private int blah_ ; + /** + * optional int32 blah = 1; + */ + public boolean hasBlah() { + return ((bitField0_ & 0x00000001) == 0x00000001); + } + /** + * optional int32 blah = 1; + */ + public int getBlah() { + return blah_; + } + /** + * optional int32 blah = 1; + */ + public Builder setBlah(int value) { + bitField0_ |= 0x00000001; + blah_ = value; + onChanged(); + return this; + } + /** + * optional int32 blah = 1; + */ + public Builder clearBlah() { + bitField0_ = (bitField0_ & ~0x00000001); + blah_ = 0; + onChanged(); + return this; + } + + // @@protoc_insertion_point(builder_scope:SecondMsg) + } + + static { + defaultInstance = new SecondMsg(true); + defaultInstance.initFields(); + } + + // @@protoc_insertion_point(class_scope:SecondMsg) +} + diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/protobuf/SecondMsgOrBuilder.java b/spring-webflux/src/test/java/org/springframework/web/reactive/protobuf/SecondMsgOrBuilder.java new file mode 100644 index 00000000000..29117f85b95 --- /dev/null +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/protobuf/SecondMsgOrBuilder.java @@ -0,0 +1,18 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: sample.proto + +package org.springframework.web.reactive.protobuf; + +public interface SecondMsgOrBuilder + extends com.google.protobuf.MessageOrBuilder { + + // optional int32 blah = 1; + /** + * optional int32 blah = 1; + */ + boolean hasBlah(); + /** + * optional int32 blah = 1; + */ + int getBlah(); +} diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ProtobufIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ProtobufIntegrationTests.java new file mode 100644 index 00000000000..60a0d9c3dfa --- /dev/null +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ProtobufIntegrationTests.java @@ -0,0 +1,164 @@ +/* + * 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. + * 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.time.Duration; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +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.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.reactive.config.EnableWebFlux; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.protobuf.Msg; +import org.springframework.web.reactive.protobuf.SecondMsg; + +/** + * Integration tests for Protobuf support. + * + * @author Sebastien Deleuze + */ +public class ProtobufIntegrationTests extends AbstractRequestMappingIntegrationTests { + + public static final Msg TEST_MSG = Msg.newBuilder().setFoo("Foo").setBlah(SecondMsg.newBuilder().setBlah(123).build()).build(); + + private WebClient webClient; + + + @Override + protected ApplicationContext initApplicationContext() { + AnnotationConfigApplicationContext wac = new AnnotationConfigApplicationContext(); + wac.register(TestConfiguration .class); + wac.refresh(); + return wac; + } + + @Override + @Before + public void setup() throws Exception { + super.setup(); + this.webClient = WebClient.create("http://localhost:" + this.port); + } + + @Test + public void value() { + Mono result = this.webClient.get() + .uri("/message") + .exchange() + .doOnNext(response -> { + Assert.assertFalse(response.headers().contentType().get().getParameters().containsKey("delimited")); + Assert.assertEquals("sample.proto", response.headers().header("X-Protobuf-Schema").get(0)); + Assert.assertEquals("Msg", response.headers().header("X-Protobuf-Message").get(0)); + }) + .flatMap(response -> response.bodyToMono(Msg.class)); + + StepVerifier.create(result) + .expectNext(TEST_MSG) + .verifyComplete(); + } + + @Test + public void values() { + Flux result = this.webClient.get() + .uri("/messages") + .exchange() + .doOnNext(response -> { + Assert.assertEquals("true", response.headers().contentType().get().getParameters().get("delimited")); + Assert.assertEquals("sample.proto", response.headers().header("X-Protobuf-Schema").get(0)); + Assert.assertEquals("Msg", response.headers().header("X-Protobuf-Message").get(0)); + }) + .flatMapMany(response -> response.bodyToFlux(Msg.class)); + + StepVerifier.create(result) + .expectNext(TEST_MSG) + .expectNext(TEST_MSG) + .expectNext(TEST_MSG) + .verifyComplete(); + } + + @Test + public void streaming() { + Flux result = this.webClient.get() + .uri("/message-stream") + .exchange() + .doOnNext(response -> { + Assert.assertEquals("true", response.headers().contentType().get().getParameters().get("delimited")); + Assert.assertEquals("sample.proto", response.headers().header("X-Protobuf-Schema").get(0)); + Assert.assertEquals("Msg", response.headers().header("X-Protobuf-Message").get(0)); + }) + .flatMapMany(response -> response.bodyToFlux(Msg.class)); + + StepVerifier.create(result) + .expectNext(Msg.newBuilder().setFoo("Foo").setBlah(SecondMsg.newBuilder().setBlah(0).build()).build()) + .expectNext(Msg.newBuilder().setFoo("Foo").setBlah(SecondMsg.newBuilder().setBlah(1).build()).build()) + .thenCancel() + .verify(); + } + + @Test + public void empty() { + Mono result = this.webClient.get() + .uri("/empty") + .retrieve() + .bodyToMono(Msg.class); + + StepVerifier.create(result) + .verifyComplete(); + } + + @RestController + @SuppressWarnings("unused") + static class ProtobufController { + + @GetMapping("/message") + Mono message() { + return Mono.just(TEST_MSG); + } + + @GetMapping("/messages") + Flux messages() { + return Flux.just(TEST_MSG, TEST_MSG, TEST_MSG); + } + + @GetMapping(value = "/message-stream", produces = "application/x-protobuf;delimited=true") + Flux messageStream() { + return testInterval(Duration.ofMillis(50), 5).map(l -> Msg.newBuilder().setFoo("Foo").setBlah(SecondMsg.newBuilder().setBlah(l.intValue()).build()).build()); + } + + @GetMapping("/empty") + Mono empty() { + return Mono.empty(); + } + + } + + @Configuration + @EnableWebFlux + @ComponentScan(resourcePattern = "**/ProtobufIntegrationTests*.class") + @SuppressWarnings("unused") + static class TestConfiguration { + } +} diff --git a/spring-webflux/src/test/proto/sample.proto b/spring-webflux/src/test/proto/sample.proto new file mode 100644 index 00000000000..9c0f71449e8 --- /dev/null +++ b/spring-webflux/src/test/proto/sample.proto @@ -0,0 +1,12 @@ +option java_package = "org.springframework.web.reactive.protobuf"; +option java_outer_classname = "OuterSample"; +option java_multiple_files = true; + +message Msg { + optional string foo = 1; + optional SecondMsg blah = 2; +} + +message SecondMsg { + optional int32 blah = 1; +} diff --git a/src/docs/asciidoc/web/webflux.adoc b/src/docs/asciidoc/web/webflux.adoc index f52b7476e88..334acc2a75f 100644 --- a/src/docs/asciidoc/web/webflux.adoc +++ b/src/docs/asciidoc/web/webflux.adoc @@ -661,8 +661,8 @@ use in an application. The `spring-core` module has encoders and decoders for `byte[]`, `ByteBuffer`, `DataBuffer`, `Resource`, and `String`. The `spring-web` module adds encoders and decoders for Jackson -JSON, Jackson Smile, JAXB2, along with other web-specific HTTP message readers and writers -for form data, multipart requests, and server-sent events. +JSON, Jackson Smile, JAXB2, Protocol Buffers, along with other web-specific HTTP message +readers and writers for form data, multipart requests, and server-sent events. [[webflux-codecs-jackson]]