From 7a16cfff8e944f084d0b9237687e31adaab31d68 Mon Sep 17 00:00:00 2001 From: Parviz ROzikov Date: Tue, 26 Nov 2019 09:54:49 +0700 Subject: [PATCH] #24022 - added protobuf MessageConverter --- spring-messaging/spring-messaging.gradle | 1 + .../converter/AbstractMessageConverter.java | 27 +- .../MappingJackson2MessageConverter.java | 2 +- .../MarshallingMessageConverter.java | 2 +- .../converter/ProtobufMessageConverter.java | 324 +++++++++ .../AbstractMessageBrokerConfiguration.java | 22 +- .../converter/MessageConverterTests.java | 17 +- .../ProtobufMessageConverterTest.java | 142 ++++ .../messaging/protobuf/Msg.java | 654 ++++++++++++++++++ .../messaging/protobuf/MsgOrBuilder.java | 37 + .../messaging/protobuf/OuterSample.java | 63 ++ .../messaging/protobuf/SecondMsg.java | 389 +++++++++++ .../protobuf/SecondMsgOrBuilder.java | 18 + .../MessageBrokerConfigurationTests.java | 10 +- spring-messaging/src/test/proto/sample.proto | 12 + spring-websocket/spring-websocket.gradle | 1 + .../MessageBrokerBeanDefinitionParser.java | 11 + ...essageBrokerBeanDefinitionParserTests.java | 9 +- 18 files changed, 1719 insertions(+), 22 deletions(-) create mode 100644 spring-messaging/src/main/java/org/springframework/messaging/converter/ProtobufMessageConverter.java create mode 100644 spring-messaging/src/test/java/org/springframework/messaging/converter/ProtobufMessageConverterTest.java create mode 100644 spring-messaging/src/test/java/org/springframework/messaging/protobuf/Msg.java create mode 100644 spring-messaging/src/test/java/org/springframework/messaging/protobuf/MsgOrBuilder.java create mode 100644 spring-messaging/src/test/java/org/springframework/messaging/protobuf/OuterSample.java create mode 100644 spring-messaging/src/test/java/org/springframework/messaging/protobuf/SecondMsg.java create mode 100644 spring-messaging/src/test/java/org/springframework/messaging/protobuf/SecondMsgOrBuilder.java create mode 100644 spring-messaging/src/test/proto/sample.proto diff --git a/spring-messaging/spring-messaging.gradle b/spring-messaging/spring-messaging.gradle index 8d6999b9fb6e..3855f3007c46 100644 --- a/spring-messaging/spring-messaging.gradle +++ b/spring-messaging/spring-messaging.gradle @@ -15,6 +15,7 @@ dependencies { optional("javax.xml.bind:jaxb-api") optional("org.jetbrains.kotlinx:kotlinx-coroutines-core") optional("org.jetbrains.kotlinx:kotlinx-coroutines-reactor") + optional("com.google.protobuf:protobuf-java-util") testCompile("javax.inject:javax.inject-tck") testCompile("javax.servlet:javax.servlet-api") testCompile("javax.validation:validation-api") diff --git a/spring-messaging/src/main/java/org/springframework/messaging/converter/AbstractMessageConverter.java b/spring-messaging/src/main/java/org/springframework/messaging/converter/AbstractMessageConverter.java index 6ac679d39ef1..d741a6007594 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/converter/AbstractMessageConverter.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/converter/AbstractMessageConverter.java @@ -20,6 +20,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Arrays; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -47,7 +48,7 @@ public abstract class AbstractMessageConverter implements SmartMessageConverter protected final Log logger = LogFactory.getLog(getClass()); - private final List supportedMimeTypes; + private List supportedMimeTypes; @Nullable private ContentTypeResolver contentTypeResolver = new DefaultContentTypeResolver(); @@ -62,8 +63,7 @@ public abstract class AbstractMessageConverter implements SmartMessageConverter * @param supportedMimeType the supported MIME type */ protected AbstractMessageConverter(MimeType supportedMimeType) { - Assert.notNull(supportedMimeType, "supportedMimeType is required"); - this.supportedMimeTypes = Collections.singletonList(supportedMimeType); + setSupportedMimeTypes(Collections.singletonList(supportedMimeType)); } /** @@ -71,8 +71,16 @@ protected AbstractMessageConverter(MimeType supportedMimeType) { * @param supportedMimeTypes the supported MIME types */ protected AbstractMessageConverter(Collection supportedMimeTypes) { - Assert.notNull(supportedMimeTypes, "supportedMimeTypes must not be null"); - this.supportedMimeTypes = new ArrayList<>(supportedMimeTypes); + setSupportedMimeTypes(new ArrayList<>(supportedMimeTypes)); + } + + /** + * Construct an {@code AbstractMessageConverter} supporting multiple MIME types. + * @param supportedMimeTypes the supported MIME types + * @since 5.2.2 + */ + protected AbstractMessageConverter(MimeType... supportedMimeTypes) { + setSupportedMimeTypes(Arrays.asList(supportedMimeTypes)); } @@ -83,6 +91,15 @@ public List getSupportedMimeTypes() { return Collections.unmodifiableList(this.supportedMimeTypes); } + /** + * Set the list of {@link MimeType} objects supported by this converter. + * @since 5.2.2 + */ + protected void setSupportedMimeTypes(List supportedMimeTypes) { + Assert.notNull(supportedMimeTypes, "supportedMimeTypes must not be null"); + this.supportedMimeTypes = supportedMimeTypes; + } + /** * Configure the {@link ContentTypeResolver} to use to resolve the content * type of an input message. diff --git a/spring-messaging/src/main/java/org/springframework/messaging/converter/MappingJackson2MessageConverter.java b/spring-messaging/src/main/java/org/springframework/messaging/converter/MappingJackson2MessageConverter.java index abf98391db78..16dd3e19bc89 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/converter/MappingJackson2MessageConverter.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/converter/MappingJackson2MessageConverter.java @@ -84,7 +84,7 @@ public MappingJackson2MessageConverter() { * @since 4.1.5 */ public MappingJackson2MessageConverter(MimeType... supportedMimeTypes) { - super(Arrays.asList(supportedMimeTypes)); + super(supportedMimeTypes); this.objectMapper = initObjectMapper(); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/converter/MarshallingMessageConverter.java b/spring-messaging/src/main/java/org/springframework/messaging/converter/MarshallingMessageConverter.java index 3a477c79f463..d031157398c2 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/converter/MarshallingMessageConverter.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/converter/MarshallingMessageConverter.java @@ -70,7 +70,7 @@ public MarshallingMessageConverter() { * @param supportedMimeTypes the MIME types */ public MarshallingMessageConverter(MimeType... supportedMimeTypes) { - super(Arrays.asList(supportedMimeTypes)); + super(supportedMimeTypes); } /** diff --git a/spring-messaging/src/main/java/org/springframework/messaging/converter/ProtobufMessageConverter.java b/spring-messaging/src/main/java/org/springframework/messaging/converter/ProtobufMessageConverter.java new file mode 100644 index 000000000000..2ac163a3a68f --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/converter/ProtobufMessageConverter.java @@ -0,0 +1,324 @@ +/* + * Copyright 2002-2019 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 + * + * https://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.messaging.converter; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.lang.reflect.Method; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Map; + +import com.google.protobuf.ExtensionRegistry; +import com.google.protobuf.Message; +import com.google.protobuf.util.JsonFormat; +import org.springframework.lang.Nullable; +import org.springframework.messaging.MessageHeaders; +import org.springframework.util.ClassUtils; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.MimeType; + +import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON; +import static org.springframework.util.MimeTypeUtils.TEXT_PLAIN; + +/** + * An {@code MessageConverter} that reads and writes + * {@link com.google.protobuf.Message com.google.protobuf.Messages} using + * Google Protocol Buffers. + * + *

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

This converter supports by default {@code "application/x-protobuf"} with the official + * {@code "com.google.protobuf:protobuf-java"} library. + * + *

{@code "application/json"} can be supported with the official {@code "com.google.protobuf:protobuf-java-util"} 3.x + * with 3.3 or higher recommended + * + * @author Parviz Rozikov + * @since 5.2.2 + */ +public class ProtobufMessageConverter extends AbstractMessageConverter { + + + /** + * The default charset used by the converter. + */ + public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + + /** + * The mime-type for protobuf {@code application/x-protobuf}. + */ + public static final MimeType PROTOBUF = new MimeType("application", "x-protobuf", DEFAULT_CHARSET); + + + private static final Map, Method> methodCache = new ConcurrentReferenceHashMap<>(); + + final ExtensionRegistry extensionRegistry; + + @Nullable + private final ProtobufFormatSupport protobufFormatSupport; + + + /** + * Construct a new {@code ProtobufMessageConverter}. + */ + public ProtobufMessageConverter() { + this((ProtobufFormatSupport) null, (ExtensionRegistry) null); + } + + /** + * Construct a new {@code ProtobufMessageConverter} with a registry that specifies + * protocol message extensions. + * + * @param extensionRegistry the registry to populate + */ + public ProtobufMessageConverter(@Nullable ExtensionRegistry extensionRegistry) { + this(null, extensionRegistry); + } + + /** + * Construct a new {@code ProtobufMessageConverter} with the given + * {@code JsonFormat.Parser} and {@code JsonFormat.Printer} configuration. + * + * @param parser the JSON parser configuration + * @param printer the JSON printer configuration + */ + public ProtobufMessageConverter(@Nullable JsonFormat.Parser parser, @Nullable JsonFormat.Printer printer) { + this(new ProtobufJavaUtilSupport(parser, printer), (ExtensionRegistry) null); + } + + /** + * Construct a new {@code ProtobufMessageConverter} with the given + * {@code JsonFormat.Parser} and {@code JsonFormat.Printer} configuration, also + * accepting a registry that specifies protocol message extensions. + * + * @param parser the JSON parser configuration + * @param printer the JSON printer configuration + * @param extensionRegistry the registry to populate + */ + public ProtobufMessageConverter(@Nullable JsonFormat.Parser parser, + @Nullable JsonFormat.Printer printer, @Nullable ExtensionRegistry extensionRegistry) { + + this(new ProtobufJavaUtilSupport(parser, printer), extensionRegistry); + } + + /** + * Construct a new {@code ProtobufMessageConverter} with the given + * {@code ProtobufFormatSupport} configuration, also + * accepting a registry that specifies protocol message extensions. + * + * @param formatSupport support third party + * @param extensionRegistry the registry to populate + */ + public ProtobufMessageConverter(@Nullable ProtobufFormatSupport formatSupport, + @Nullable ExtensionRegistry extensionRegistry) { + super(PROTOBUF); + + if (formatSupport != null) { + this.protobufFormatSupport = formatSupport; + } + else if (ClassUtils.isPresent("com.google.protobuf.util.JsonFormat", getClass().getClassLoader())) { + this.protobufFormatSupport = new ProtobufJavaUtilSupport(null, null); + } + else { + this.protobufFormatSupport = null; + } + + if (this.protobufFormatSupport != null) { + setSupportedMimeTypes(Arrays.asList(protobufFormatSupport.supportedMediaTypes())); + } + + this.extensionRegistry = (extensionRegistry == null ? ExtensionRegistry.newInstance() : extensionRegistry); + } + + @Override + protected boolean supports(Class clazz) { + return Message.class.isAssignableFrom(clazz); + } + + @Override + protected boolean canConvertTo(Object payload, @Nullable MessageHeaders headers) { + MimeType mimeType = getMimeType(headers); + return (super.canConvertTo(payload, headers) || + this.protobufFormatSupport != null && this.protobufFormatSupport.supportsWriteOnly(mimeType)); + } + + @Override + protected Object convertFromInternal(org.springframework.messaging.Message message, Class targetClass, @Nullable Object conversionHint) { + MimeType contentType = getMimeType(message.getHeaders()); + final Object payload = message.getPayload(); + + if (contentType == null) { + contentType = PROTOBUF; + } + + Charset charset = contentType.getCharset(); + if (charset == null) { + charset = DEFAULT_CHARSET; + } + + Message.Builder builder = getMessageBuilder(targetClass); + + try { + if (PROTOBUF.isCompatibleWith(contentType)) { + builder.mergeFrom((byte[]) payload, this.extensionRegistry); + } + else if (protobufFormatSupport != null) { + this.protobufFormatSupport.merge( + message, charset, contentType, this.extensionRegistry, builder); + } + } + catch (IOException e) { + throw new MessageConversionException(message, "Could not read proto message" + e.getMessage(), e); + } + + return builder.build(); + } + + + @Override + protected Object convertToInternal(Object payload, @Nullable MessageHeaders headers, @Nullable Object conversionHint) { + final Message message = (Message) payload; + + MimeType contentType = getMimeType(headers); + if (contentType == null) { + contentType = PROTOBUF; + + } + + Charset charset = contentType.getCharset(); + if (charset == null) { + charset = DEFAULT_CHARSET; + } + + try { + if (PROTOBUF.isCompatibleWith(contentType)) { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + message.writeTo(byteArrayOutputStream); + payload = byteArrayOutputStream.toByteArray(); + } + else if (this.protobufFormatSupport != null) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + this.protobufFormatSupport.print(message, outputStream, contentType, charset); + payload = new String(outputStream.toByteArray(), charset); + } + } + catch (IOException e) { + throw new MessageConversionException("Could not write proto message" + e.getMessage(), e); + + } + return payload; + } + + + /** + * Create a new {@code Message.Builder} instance for the given class. + *

This method uses a ConcurrentReferenceHashMap for caching method lookups. + */ + private Message.Builder getMessageBuilder(Class clazz) { + try { + Method method = methodCache.get(clazz); + if (method == null) { + method = clazz.getMethod("newBuilder"); + methodCache.put(clazz, method); + } + return (Message.Builder) method.invoke(clazz); + } + catch (Exception ex) { + throw new MessageConversionException( + "Invalid Protobuf Message type: no invocable newBuilder() method on " + clazz, ex); + } + } + + + /** + * Protobuf format support. + */ + interface ProtobufFormatSupport { + + MimeType[] supportedMediaTypes(); + + boolean supportsWriteOnly(@Nullable MimeType mediaType); + + void merge(org.springframework.messaging.Message message, Charset charset, MimeType contentType, + ExtensionRegistry extensionRegistry, Message.Builder builder) + throws IOException, MessageConversionException; + + void print(Message message, OutputStream output, MimeType contentType, Charset charset) + throws IOException, MessageConversionException; + } + + + /** + * {@link ProtobufFormatSupport} implementation used when + * {@code com.google.protobuf.util.JsonFormat} is available. + */ + static class ProtobufJavaUtilSupport implements ProtobufFormatSupport { + + private final JsonFormat.Parser parser; + + private final JsonFormat.Printer printer; + + public ProtobufJavaUtilSupport(@Nullable JsonFormat.Parser parser, @Nullable JsonFormat.Printer printer) { + this.parser = (parser != null ? parser : JsonFormat.parser()); + this.printer = (printer != null ? printer : JsonFormat.printer()); + } + + @Override + public MimeType[] supportedMediaTypes() { + return new MimeType[]{PROTOBUF, TEXT_PLAIN, APPLICATION_JSON}; + } + + @Override + public boolean supportsWriteOnly(@Nullable MimeType mimeType) { + return false; + } + + @Override + public void merge(org.springframework.messaging.Message message, Charset charset, MimeType contentType, + ExtensionRegistry extensionRegistry, Message.Builder builder) + throws IOException, MessageConversionException { + + if (contentType.isCompatibleWith(APPLICATION_JSON)) { + this.parser.merge(message.getPayload().toString(), builder); + } + else { + throw new MessageConversionException( + "protobuf-java-util does not support parsing " + contentType); + } + } + + @Override + public void print(Message message, OutputStream output, MimeType contentType, Charset charset) + throws IOException, MessageConversionException { + + if (contentType.isCompatibleWith(APPLICATION_JSON)) { + OutputStreamWriter writer = new OutputStreamWriter(output, charset); + this.printer.appendTo(message, writer); + writer.flush(); + } else { + throw new MessageConversionException( + "protobuf-java-util does not support printing " + contentType); + } + } + } + + +} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractMessageBrokerConfiguration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractMessageBrokerConfiguration.java index a3cc4f10c15a..519fbac6d110 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractMessageBrokerConfiguration.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractMessageBrokerConfiguration.java @@ -37,6 +37,7 @@ import org.springframework.messaging.converter.MappingJackson2MessageConverter; import org.springframework.messaging.converter.MessageConverter; import org.springframework.messaging.converter.StringMessageConverter; +import org.springframework.messaging.converter.ProtobufMessageConverter; import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; import org.springframework.messaging.handler.invocation.HandlerMethodReturnValueHandler; import org.springframework.messaging.simp.SimpLogging; @@ -93,9 +94,15 @@ public abstract class AbstractMessageBrokerConfiguration implements ApplicationC private static final String MVC_VALIDATOR_NAME = "mvcValidator"; - private static final boolean jackson2Present = ClassUtils.isPresent( - "com.fasterxml.jackson.databind.ObjectMapper", AbstractMessageBrokerConfiguration.class.getClassLoader()); + private static final boolean jackson2Present; + private static final boolean protobufPresent; + + static { + ClassLoader classLoader = AbstractMessageBrokerConfiguration.class.getClassLoader(); + jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader); + protobufPresent = ClassUtils.isPresent("com.google.protobuf.Message", classLoader); + } @Nullable private ApplicationContext applicationContext; @@ -391,6 +398,9 @@ public CompositeMessageConverter brokerMessageConverter() { if (jackson2Present) { converters.add(createJacksonConverter()); } + if (protobufPresent) { + converters.add(createProtobufConverter()); + } } return new CompositeMessageConverter(converters); } @@ -403,6 +413,14 @@ protected MappingJackson2MessageConverter createJacksonConverter() { return converter; } + protected ProtobufMessageConverter createProtobufConverter() { + DefaultContentTypeResolver resolver = new DefaultContentTypeResolver(); + resolver.setDefaultMimeType(ProtobufMessageConverter.PROTOBUF); + final ProtobufMessageConverter converter = new ProtobufMessageConverter(); + converter.setContentTypeResolver(resolver); + return converter; + } + /** * Override this method to add custom message converters. * @param messageConverters the list to add converters to, initially empty diff --git a/spring-messaging/src/test/java/org/springframework/messaging/converter/MessageConverterTests.java b/spring-messaging/src/test/java/org/springframework/messaging/converter/MessageConverterTests.java index ecc62c210a3e..ff834b84e40a 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/converter/MessageConverterTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/converter/MessageConverterTests.java @@ -16,9 +16,6 @@ package org.springframework.messaging.converter; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -81,14 +78,15 @@ public void supportsMimeTypeNotSpecified() { public void supportsMimeTypeNoneConfigured() { Message message = MessageBuilder.withPayload( "ABC").setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.APPLICATION_JSON).build(); - this.converter = new TestMessageConverter(Collections.emptyList()); + final MimeType[] empty = {}; + this.converter = new TestMessageConverter(empty); assertThat(this.converter.fromMessage(message, String.class)).isEqualTo("success-from"); } @Test public void canConvertFromStrictContentTypeMatch() { - this.converter = new TestMessageConverter(Arrays.asList(MimeTypeUtils.TEXT_PLAIN)); + this.converter = new TestMessageConverter(MimeTypeUtils.TEXT_PLAIN); this.converter.setStrictContentTypeMatch(true); Message message = MessageBuilder.withPayload("ABC").build(); @@ -102,7 +100,8 @@ public void canConvertFromStrictContentTypeMatch() { @Test public void setStrictContentTypeMatchWithNoSupportedMimeTypes() { - this.converter = new TestMessageConverter(Collections.emptyList()); + final MimeType[] empty = {}; + this.converter = new TestMessageConverter(empty); assertThatIllegalArgumentException().isThrownBy(() -> this.converter.setStrictContentTypeMatch(true)); } @@ -149,7 +148,7 @@ public TestMessageConverter() { super(MimeTypeUtils.TEXT_PLAIN); } - public TestMessageConverter(Collection supportedMimeTypes) { + public TestMessageConverter(MimeType... supportedMimeTypes) { super(supportedMimeTypes); } @@ -160,14 +159,14 @@ protected boolean supports(Class clazz) { @Override protected Object convertFromInternal(Message message, Class targetClass, - @Nullable Object conversionHint) { + @Nullable Object conversionHint) { return "success-from"; } @Override protected Object convertToInternal(Object payload, @Nullable MessageHeaders headers, - @Nullable Object conversionHint) { + @Nullable Object conversionHint) { return "success-to"; } diff --git a/spring-messaging/src/test/java/org/springframework/messaging/converter/ProtobufMessageConverterTest.java b/spring-messaging/src/test/java/org/springframework/messaging/converter/ProtobufMessageConverterTest.java new file mode 100644 index 000000000000..fd3b622fc1e6 --- /dev/null +++ b/spring-messaging/src/test/java/org/springframework/messaging/converter/ProtobufMessageConverterTest.java @@ -0,0 +1,142 @@ +/* + * Copyright 2002-2019 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 + * + * https://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.messaging.converter; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import com.google.protobuf.ExtensionRegistry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.protobuf.Msg; +import org.springframework.messaging.protobuf.SecondMsg; +import org.springframework.messaging.support.MessageBuilder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.springframework.messaging.MessageHeaders.CONTENT_TYPE; +import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON; + +/** + * Test suite for {@link ProtobufMessageConverter}. + * + * @author Parviz Rozikov + */ +class ProtobufMessageConverterTest { + + private ProtobufMessageConverter converter; + + private ExtensionRegistry extensionRegistry; + + private Msg testMsg; + private Message message; + private Message messageWithoutContentType; + private Message messageJson; + + + @BeforeEach + public void setup() { + this.extensionRegistry = mock(ExtensionRegistry.class); + this.converter = new ProtobufMessageConverter(); + this.testMsg = Msg.newBuilder().setFoo("Foo").setBlah(SecondMsg.newBuilder().setBlah(123).build()).build(); + this.message = MessageBuilder.withPayload(this.testMsg.toByteArray()) + .setHeader(CONTENT_TYPE, ProtobufMessageConverter.PROTOBUF).build(); + this.messageWithoutContentType = MessageBuilder.withPayload(this.testMsg.toByteArray()) + .build(); + this.messageJson = MessageBuilder.withPayload("{\n" + + " \"foo\": \"Foo\",\n" + + " \"blah\": {\n" + + " \"blah\": 123\n" + + " }\n" + + "}") + .setHeader(CONTENT_TYPE, APPLICATION_JSON).build(); + } + + @Test + public void extensionRegistryNull() { + ProtobufMessageConverter converter = new ProtobufMessageConverter((ExtensionRegistry) null); + assertThat(converter.extensionRegistry).isNotNull(); + } + + + @Test + public void canConvertFrom() { + assertThat(converter.canConvertFrom(message, Msg.class)).isTrue(); + assertThat(converter.canConvertFrom(messageWithoutContentType, Msg.class)).isTrue(); + assertThat(converter.canConvertFrom(messageJson, Msg.class)).isTrue(); + } + + @Test + public void canConvertTo() { + assertThat(converter.canConvertTo(testMsg, message.getHeaders())).isTrue(); + assertThat(converter.canConvertTo(testMsg, messageWithoutContentType.getHeaders())).isTrue(); + assertThat(converter.canConvertTo(testMsg, messageJson.getHeaders())).isTrue(); + } + + + @Test + public void convertFrom() throws IOException { + final Msg msg = (Msg) converter.fromMessage(message, Msg.class); + assertThat(msg).isEqualTo(testMsg); + } + + @Test + public void convertTo() throws IOException { + final Message message = converter.toMessage(this.testMsg, this.message.getHeaders()); + assertThat(message).isNotNull(); + assertThat(message.getPayload()).isEqualTo(this.message.getPayload()); + } + + + @Test + public void convertFromNoContentType() throws IOException { + Msg result = (Msg) converter.fromMessage(messageWithoutContentType, Msg.class); + assertThat(result).isEqualTo(testMsg); + } + + + @Test + public void defaultContentType() throws Exception { + assertThat(converter.getDefaultContentType(testMsg)).isEqualTo(ProtobufMessageConverter.PROTOBUF); + } + + @Test + public void testJsonWithGoogleProtobuf() throws Exception { + this.converter = new ProtobufMessageConverter( + new ProtobufMessageConverter.ProtobufJavaUtilSupport(null, null), + extensionRegistry); + + final Map headers = new HashMap<>(); + headers.put(CONTENT_TYPE, APPLICATION_JSON); + + //convertTo + final Message message = this.converter.toMessage(this.testMsg, new MessageHeaders(headers)); + assertThat(message).isNotNull(); + assertThat(message.getHeaders().get(CONTENT_TYPE)).isEqualTo(APPLICATION_JSON); + assertThat(((String) message.getPayload()).length() > 0).isTrue(); + assertThat(((String) message.getPayload()).isEmpty()).as("Body is empty").isFalse(); + assertThat(((String) message.getPayload())).isEqualTo(this.messageJson.getPayload()); + + //convertFrom + final Msg msg = (Msg) converter.fromMessage(message, Msg.class); + assertThat(msg).isEqualTo(this.testMsg); + } + +} \ No newline at end of file diff --git a/spring-messaging/src/test/java/org/springframework/messaging/protobuf/Msg.java b/spring-messaging/src/test/java/org/springframework/messaging/protobuf/Msg.java new file mode 100644 index 000000000000..41784343ae3f --- /dev/null +++ b/spring-messaging/src/test/java/org/springframework/messaging/protobuf/Msg.java @@ -0,0 +1,654 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://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.messaging.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; + @java.lang.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); + } + }; + + @java.lang.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 java.lang.Object foo_; + /** + * optional string foo = 1; + */ + public boolean hasFoo() { + return ((bitField0_ & 0x00000001) == 0x00000001); + } + /** + * optional string foo = 1; + */ + public java.lang.String getFoo() { + java.lang.Object ref = foo_; + if (ref instanceof java.lang.String) { + return (java.lang.String) ref; + } else { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + if (bs.isValidUtf8()) { + foo_ = s; + } + return s; + } + } + /** + * optional string foo = 1; + */ + public com.google.protobuf.ByteString + getFooBytes() { + java.lang.Object ref = foo_; + if (ref instanceof java.lang.String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.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; + @java.lang.Override + protected java.lang.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); } + + @java.lang.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.messaging.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 java.lang.Object foo_ = ""; + /** + * optional string foo = 1; + */ + public boolean hasFoo() { + return ((bitField0_ & 0x00000001) == 0x00000001); + } + /** + * optional string foo = 1; + */ + public java.lang.String getFoo() { + java.lang.Object ref = foo_; + if (!(ref instanceof java.lang.String)) { + java.lang.String s = ((com.google.protobuf.ByteString) ref) + .toStringUtf8(); + foo_ = s; + return s; + } else { + return (java.lang.String) ref; + } + } + /** + * optional string foo = 1; + */ + public com.google.protobuf.ByteString + getFooBytes() { + java.lang.Object ref = foo_; + if (ref instanceof String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + foo_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + /** + * optional string foo = 1; + */ + public Builder setFoo( + java.lang.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-messaging/src/test/java/org/springframework/messaging/protobuf/MsgOrBuilder.java b/spring-messaging/src/test/java/org/springframework/messaging/protobuf/MsgOrBuilder.java new file mode 100644 index 000000000000..ca9f94be7450 --- /dev/null +++ b/spring-messaging/src/test/java/org/springframework/messaging/protobuf/MsgOrBuilder.java @@ -0,0 +1,37 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: sample.proto + +package org.springframework.messaging.protobuf; + +public interface MsgOrBuilder + extends com.google.protobuf.MessageOrBuilder { + + // optional string foo = 1; + /** + * optional string foo = 1; + */ + boolean hasFoo(); + /** + * optional string foo = 1; + */ + java.lang.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-messaging/src/test/java/org/springframework/messaging/protobuf/OuterSample.java b/spring-messaging/src/test/java/org/springframework/messaging/protobuf/OuterSample.java new file mode 100644 index 000000000000..d3ecd6a62caa --- /dev/null +++ b/spring-messaging/src/test/java/org/springframework/messaging/protobuf/OuterSample.java @@ -0,0 +1,63 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: sample.proto + +package org.springframework.messaging.protobuf; + +@SuppressWarnings("deprecation") +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 { + java.lang.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 java.lang.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 java.lang.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-messaging/src/test/java/org/springframework/messaging/protobuf/SecondMsg.java b/spring-messaging/src/test/java/org/springframework/messaging/protobuf/SecondMsg.java new file mode 100644 index 000000000000..5567d4e7843b --- /dev/null +++ b/spring-messaging/src/test/java/org/springframework/messaging/protobuf/SecondMsg.java @@ -0,0 +1,389 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: sample.proto + +package org.springframework.messaging.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; + @java.lang.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); + } + }; + + @java.lang.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; + @java.lang.Override + protected java.lang.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); } + + @java.lang.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.messaging.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-messaging/src/test/java/org/springframework/messaging/protobuf/SecondMsgOrBuilder.java b/spring-messaging/src/test/java/org/springframework/messaging/protobuf/SecondMsgOrBuilder.java new file mode 100644 index 000000000000..fc4ff1576cd6 --- /dev/null +++ b/spring-messaging/src/test/java/org/springframework/messaging/protobuf/SecondMsgOrBuilder.java @@ -0,0 +1,18 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: sample.proto + +package org.springframework.messaging.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-messaging/src/test/java/org/springframework/messaging/simp/config/MessageBrokerConfigurationTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/config/MessageBrokerConfigurationTests.java index 779c1de0526e..4b643212b6cf 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/config/MessageBrokerConfigurationTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/config/MessageBrokerConfigurationTests.java @@ -41,6 +41,7 @@ import org.springframework.messaging.converter.MappingJackson2MessageConverter; import org.springframework.messaging.converter.MessageConverter; import org.springframework.messaging.converter.StringMessageConverter; +import org.springframework.messaging.converter.ProtobufMessageConverter; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; @@ -281,13 +282,17 @@ public void configureMessageConvertersDefault() { CompositeMessageConverter compositeConverter = config.brokerMessageConverter(); List converters = compositeConverter.getConverters(); - assertThat(converters).hasSize(3); + assertThat(converters).hasSize(4); assertThat(converters.get(0)).isInstanceOf(StringMessageConverter.class); assertThat(converters.get(1)).isInstanceOf(ByteArrayMessageConverter.class); assertThat(converters.get(2)).isInstanceOf(MappingJackson2MessageConverter.class); + assertThat(converters.get(3)).isInstanceOf(ProtobufMessageConverter.class); ContentTypeResolver resolver = ((MappingJackson2MessageConverter) converters.get(2)).getContentTypeResolver(); assertThat(((DefaultContentTypeResolver) resolver).getDefaultMimeType()).isEqualTo(MimeTypeUtils.APPLICATION_JSON); + + resolver = ((ProtobufMessageConverter) converters.get(3)).getContentTypeResolver(); + assertThat(((DefaultContentTypeResolver) resolver).getDefaultMimeType()).isEqualTo(ProtobufMessageConverter.PROTOBUF); } @Test @@ -339,12 +344,13 @@ protected boolean configureMessageConverters(List messageConve }; CompositeMessageConverter compositeConverter = config.brokerMessageConverter(); - assertThat(compositeConverter.getConverters()).hasSize(4); + assertThat(compositeConverter.getConverters()).hasSize(5); Iterator iterator = compositeConverter.getConverters().iterator(); assertThat(iterator.next()).isEqualTo(testConverter); assertThat(iterator.next()).isInstanceOf(StringMessageConverter.class); assertThat(iterator.next()).isInstanceOf(ByteArrayMessageConverter.class); assertThat(iterator.next()).isInstanceOf(MappingJackson2MessageConverter.class); + assertThat(iterator.next()).isInstanceOf(ProtobufMessageConverter.class); } @Test diff --git a/spring-messaging/src/test/proto/sample.proto b/spring-messaging/src/test/proto/sample.proto new file mode 100644 index 000000000000..c303ef2894cf --- /dev/null +++ b/spring-messaging/src/test/proto/sample.proto @@ -0,0 +1,12 @@ +option java_package = "org.springframework.protobuf.messaging"; +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/spring-websocket/spring-websocket.gradle b/spring-websocket/spring-websocket.gradle index 96355eef5369..b464a375073a 100644 --- a/spring-websocket/spring-websocket.gradle +++ b/spring-websocket/spring-websocket.gradle @@ -21,6 +21,7 @@ dependencies { optional("io.undertow:undertow-servlet") optional("io.undertow:undertow-websockets-jsr") optional("com.fasterxml.jackson.core:jackson-databind") + optional("com.google.protobuf:protobuf-java-util") testCompile("org.apache.tomcat.embed:tomcat-embed-core") testCompile("org.apache.tomcat.embed:tomcat-embed-websocket") testCompile("io.projectreactor.netty:reactor-netty") diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParser.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParser.java index 31294cb57940..7323849b022c 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParser.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParser.java @@ -44,6 +44,7 @@ import org.springframework.messaging.converter.DefaultContentTypeResolver; import org.springframework.messaging.converter.MappingJackson2MessageConverter; import org.springframework.messaging.converter.StringMessageConverter; +import org.springframework.messaging.converter.ProtobufMessageConverter; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.messaging.simp.SimpSessionScope; import org.springframework.messaging.simp.broker.SimpleBrokerMessageHandler; @@ -115,10 +116,13 @@ class MessageBrokerBeanDefinitionParser implements BeanDefinitionParser { private static final boolean javaxValidationPresent; + private static final boolean protobufPresent; + static { ClassLoader classLoader = MessageBrokerBeanDefinitionParser.class.getClassLoader(); jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader); javaxValidationPresent = ClassUtils.isPresent("javax.validation.Validator", classLoader); + protobufPresent = ClassUtils.isPresent("com.google.protobuf.Message", classLoader); } @@ -502,6 +506,13 @@ private RuntimeBeanReference registerMessageConverter( jacksonConverterDef.getPropertyValues().add("objectMapper", jacksonFactoryDef); converters.add(jacksonConverterDef); } + if (protobufPresent) { + final RootBeanDefinition protoConverterDef = new RootBeanDefinition(ProtobufMessageConverter.class); + RootBeanDefinition resolverDef = new RootBeanDefinition(DefaultContentTypeResolver.class); + resolverDef.getPropertyValues().add("defaultMimeType", ProtobufMessageConverter.PROTOBUF); + protoConverterDef.getPropertyValues().add("contentTypeResolver", resolverDef); + converters.add(protoConverterDef); + } } ConstructorArgumentValues cargs = new ConstructorArgumentValues(); cargs.addIndexedArgumentValue(0, converters); diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParserTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParserTests.java index 43e1ad59bdf8..ec0ded55043f 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParserTests.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParserTests.java @@ -41,6 +41,7 @@ import org.springframework.messaging.converter.DefaultContentTypeResolver; import org.springframework.messaging.converter.MappingJackson2MessageConverter; import org.springframework.messaging.converter.MessageConverter; +import org.springframework.messaging.converter.ProtobufMessageConverter; import org.springframework.messaging.converter.StringMessageConverter; import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; import org.springframework.messaging.handler.invocation.HandlerMethodReturnValueHandler; @@ -339,14 +340,18 @@ public void annotationMethodMessageHandler() { assertThat(simpMessagingTemplate.getUserDestinationPrefix()).isEqualTo("/personal/"); List converters = compositeMessageConverter.getConverters(); - assertThat(converters).hasSize(3); + assertThat(converters).hasSize(4); assertThat(converters.get(0)).isInstanceOf(StringMessageConverter.class); assertThat(converters.get(1)).isInstanceOf(ByteArrayMessageConverter.class); assertThat(converters.get(2)).isInstanceOf(MappingJackson2MessageConverter.class); + assertThat(converters.get(3)).isInstanceOf(ProtobufMessageConverter.class); ContentTypeResolver resolver = ((MappingJackson2MessageConverter) converters.get(2)).getContentTypeResolver(); assertThat(((DefaultContentTypeResolver) resolver).getDefaultMimeType()).isEqualTo(MimeTypeUtils.APPLICATION_JSON); + resolver = ((ProtobufMessageConverter) converters.get(3)).getContentTypeResolver(); + assertThat(((DefaultContentTypeResolver) resolver).getDefaultMimeType()).isEqualTo(ProtobufMessageConverter.PROTOBUF); + DirectFieldAccessor handlerAccessor = new DirectFieldAccessor(annotationMethodMessageHandler); Object pathMatcher = handlerAccessor.getPropertyValue("pathMatcher"); String pathSeparator = (String) new DirectFieldAccessor(pathMatcher).getPropertyValue("pathSeparator"); @@ -415,7 +420,7 @@ public void messageConverters() { CompositeMessageConverter compositeConverter = this.appContext.getBean(CompositeMessageConverter.class); assertThat(compositeConverter).isNotNull(); - assertThat(compositeConverter.getConverters().size()).isEqualTo(4); + assertThat(compositeConverter.getConverters().size()).isEqualTo(5); assertThat(compositeConverter.getConverters().iterator().next().getClass()).isEqualTo(StringMessageConverter.class); }