diff --git a/spring-web/src/main/java/org/springframework/http/converter/protobuf/ProtobufHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/protobuf/ProtobufHttpMessageConverter.java index 93b8db7fa3..b6295f6b0e 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/protobuf/ProtobufHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/protobuf/ProtobufHttpMessageConverter.java @@ -24,12 +24,14 @@ 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.concurrent.ConcurrentHashMap; import com.google.protobuf.CodedOutputStream; import com.google.protobuf.ExtensionRegistry; import com.google.protobuf.Message; import com.google.protobuf.TextFormat; +import com.google.protobuf.util.JsonFormat; import com.googlecode.protobuf.format.FormatFactory; import com.googlecode.protobuf.format.ProtobufFormatter; @@ -41,30 +43,36 @@ import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.util.ClassUtils; +import static org.springframework.http.MediaType.*; + /** * An {@code HttpMessageConverter} that reads and writes {@link com.google.protobuf.Message}s * using Google Protocol Buffers. * - *

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

Other formats can be supported with additional libraries: - *

- * *

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

Requires Protobuf 2.6 or 3.x and Protobuf Java Format 1.4 or higher, as of Spring 5.0. + *

This converter supports by default {@code "application/x-protobuf"} and {@code "text/plain"} + * with the official {@code "com.google.protobuf:protobuf-java"} library. Other formats can be + * supported with one of the following additional libraries on the classpath: + *

+ * + *

Requires Protobuf 2.6 or higher (and Protobuf Java Format 1.4 or higher for formatting). + * This converter will auto-adapt to Protobuf 3 and its default {@code protobuf-java-util} JSON + * format if the Protobuf 2 based {@code protobuf-java-format} isn't present; however, for more + * explicit JSON setup on Protobuf 3, consider {@link ProtobufJsonFormatHttpMessageConverter}. * * @author Alex Antonov * @author Brian Clozel * @author Juergen Hoeller * @since 4.1 + * @see FormatFactory + * @see JsonFormat + * @see ProtobufJsonFormatHttpMessageConverter */ public class ProtobufHttpMessageConverter extends AbstractHttpMessageConverter { @@ -76,33 +84,12 @@ public class ProtobufHttpMessageConverter extends AbstractHttpMessageConverter, Method> methodCache = new ConcurrentHashMap<>(); - private final ProtobufFormatSupport protobufFormatSupport; - private final ExtensionRegistry extensionRegistry = ExtensionRegistry.newInstance(); - - private static final MediaType[] SUPPORTED_MEDIATYPES; - - static { - if (isProtobufJavaFormatPresent) { - SUPPORTED_MEDIATYPES = new MediaType[] {PROTOBUF, MediaType.TEXT_PLAIN, MediaType.APPLICATION_XML, - MediaType.APPLICATION_JSON}; - } - else if (isProtobufJavaUtilPresent) { - SUPPORTED_MEDIATYPES = new MediaType[] {PROTOBUF, MediaType.TEXT_PLAIN, MediaType.APPLICATION_JSON}; - } - else { - SUPPORTED_MEDIATYPES = new MediaType[] {PROTOBUF, MediaType.TEXT_PLAIN}; - } - } + private final ProtobufFormatSupport protobufFormatSupport; /** @@ -115,18 +102,29 @@ public class ProtobufHttpMessageConverter extends AbstractHttpMessageConverterMost importantly, this class allows for custom JSON parser and printer configurations + * through the {@link JsonFormat} utility. If no special parser or printer configuration is + * given, default variants will be used instead. + * + *

Requires Protobuf 3.x and {@code "com.google.protobuf:protobuf-java-util"} 3.x, + * with 3.3 or higher recommended. + * + * @author Juergen Hoeller + * @since 5.0 + * @see JsonFormat#parser() + * @see JsonFormat#printer() + * @see #ProtobufJsonFormatHttpMessageConverter(JsonFormat.Parser, JsonFormat.Printer) + */ +public class ProtobufJsonFormatHttpMessageConverter extends ProtobufHttpMessageConverter { + + /** + * Construct a new {@code ProtobufJsonFormatHttpMessageConverter} with default + * {@link JsonFormat.Parser} and {@link JsonFormat.Printer} configuration. + */ + public ProtobufJsonFormatHttpMessageConverter() { + this(null, null, null); + } + + /** + * Construct a new {@code ProtobufJsonFormatHttpMessageConverter} with the given + * {@link JsonFormat.Parser} and {@link JsonFormat.Printer} configuration. + * @param parser the JSON parser configuration + * @param printer the JSON printer configuration + */ + public ProtobufJsonFormatHttpMessageConverter(JsonFormat.Parser parser, JsonFormat.Printer printer) { + this(parser, printer, null); + } + + /** + * Construct a new {@code ProtobufJsonFormatHttpMessageConverter} with the given + * {@link JsonFormat.Parser} and {@link JsonFormat.Printer} configuration, also + * accepting an initializer that allows the registration of message extensions + * @param parser the JSON parser configuration + * @param printer the JSON printer configuration + * @param registryInitializer an initializer for message extensions + */ + public ProtobufJsonFormatHttpMessageConverter(JsonFormat.Parser parser, JsonFormat.Printer printer, + ExtensionRegistryInitializer registryInitializer) { + + super(new ProtobufJavaUtilSupport(parser, printer), registryInitializer); + } + +} diff --git a/spring-web/src/test/java/org/springframework/http/converter/protobuf/ProtobufHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/protobuf/ProtobufHttpMessageConverterTests.java index 1ef5b55752..72171e0767 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/protobuf/ProtobufHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/protobuf/ProtobufHttpMessageConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2017 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. @@ -35,6 +35,7 @@ import static org.mockito.Mockito.*; * Test suite for {@link ProtobufHttpMessageConverter}. * * @author Alex Antonov + * @author Juergen Hoeller */ public class ProtobufHttpMessageConverterTests { @@ -46,7 +47,7 @@ public class ProtobufHttpMessageConverterTests { @Before - public void setUp() { + public void setup() { this.registryInitializer = mock(ExtensionRegistryInitializer.class); this.converter = new ProtobufHttpMessageConverter(this.registryInitializer); this.testMsg = Msg.newBuilder().setFoo("Foo").setBlah(SecondMsg.newBuilder().setBlah(123).build()).build(); @@ -60,12 +61,7 @@ public class ProtobufHttpMessageConverterTests { @Test public void extensionRegistryNull() { - try { - new ProtobufHttpMessageConverter(null); - } - catch (Exception ex) { - fail("Unable to create ProtobufHttpMessageConverter with null extensionRegistry"); - } + new ProtobufHttpMessageConverter(null); } @Test diff --git a/spring-web/src/test/java/org/springframework/http/converter/protobuf/ProtobufJsonFormatHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/protobuf/ProtobufJsonFormatHttpMessageConverterTests.java new file mode 100644 index 0000000000..2d2d38703a --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/converter/protobuf/ProtobufJsonFormatHttpMessageConverterTests.java @@ -0,0 +1,132 @@ +/* + * Copyright 2002-2017 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.converter.protobuf; + +import java.io.IOException; + +import com.google.protobuf.Message; +import com.google.protobuf.util.JsonFormat; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.http.MediaType; +import org.springframework.http.MockHttpInputMessage; +import org.springframework.http.MockHttpOutputMessage; +import org.springframework.protobuf.Msg; +import org.springframework.protobuf.SecondMsg; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +/** + * Test suite for {@link ProtobufJsonFormatHttpMessageConverter}. + * + * @author Juergen Hoeller + */ +public class ProtobufJsonFormatHttpMessageConverterTests { + + private ProtobufHttpMessageConverter converter; + + private ExtensionRegistryInitializer registryInitializer; + + private Msg testMsg; + + + @Before + public void setup() { + this.registryInitializer = mock(ExtensionRegistryInitializer.class); + this.converter = new ProtobufJsonFormatHttpMessageConverter( + JsonFormat.parser(), JsonFormat.printer(), this.registryInitializer); + this.testMsg = Msg.newBuilder().setFoo("Foo").setBlah(SecondMsg.newBuilder().setBlah(123).build()).build(); + } + + + @Test + public void extensionRegistryInitialized() { + verify(this.registryInitializer, times(1)).initializeExtensionRegistry(any()); + } + + @Test + public void extensionRegistryNull() { + new ProtobufHttpMessageConverter(null); + } + + @Test + public void canRead() { + assertTrue(this.converter.canRead(Msg.class, null)); + assertTrue(this.converter.canRead(Msg.class, ProtobufHttpMessageConverter.PROTOBUF)); + assertTrue(this.converter.canRead(Msg.class, MediaType.APPLICATION_JSON)); + assertTrue(this.converter.canRead(Msg.class, MediaType.TEXT_PLAIN)); + } + + @Test + public void canWrite() { + assertTrue(this.converter.canWrite(Msg.class, null)); + assertTrue(this.converter.canWrite(Msg.class, ProtobufHttpMessageConverter.PROTOBUF)); + assertTrue(this.converter.canWrite(Msg.class, MediaType.APPLICATION_JSON)); + assertTrue(this.converter.canWrite(Msg.class, MediaType.TEXT_PLAIN)); + } + + @Test + public void read() throws IOException { + byte[] body = this.testMsg.toByteArray(); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body); + inputMessage.getHeaders().setContentType(ProtobufHttpMessageConverter.PROTOBUF); + Message result = this.converter.read(Msg.class, inputMessage); + assertEquals(this.testMsg, result); + } + + @Test + public void readNoContentType() throws IOException { + byte[] body = this.testMsg.toByteArray(); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body); + Message result = this.converter.read(Msg.class, inputMessage); + assertEquals(this.testMsg, result); + } + + @Test + public void write() throws IOException { + MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + MediaType contentType = ProtobufHttpMessageConverter.PROTOBUF; + this.converter.write(this.testMsg, contentType, outputMessage); + assertEquals(contentType, outputMessage.getHeaders().getContentType()); + assertTrue(outputMessage.getBodyAsBytes().length > 0); + Message result = Msg.parseFrom(outputMessage.getBodyAsBytes()); + assertEquals(this.testMsg, result); + + String messageHeader = + outputMessage.getHeaders().getFirst(ProtobufHttpMessageConverter.X_PROTOBUF_MESSAGE_HEADER); + assertEquals("Msg", messageHeader); + String schemaHeader = + outputMessage.getHeaders().getFirst(ProtobufHttpMessageConverter.X_PROTOBUF_SCHEMA_HEADER); + assertEquals("sample.proto", schemaHeader); + } + + @Test + public void defaultContentType() throws Exception { + assertEquals(ProtobufHttpMessageConverter.PROTOBUF, this.converter.getDefaultContentType(this.testMsg)); + } + + @Test + public void getContentLength() throws Exception { + MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + MediaType contentType = ProtobufHttpMessageConverter.PROTOBUF; + this.converter.write(this.testMsg, contentType, outputMessage); + assertEquals(-1, outputMessage.getHeaders().getContentLength()); + } + +}