Skip to content

Commit b56703e

Browse files
bclozelrstoyanchev
authored andcommitted
Add Google Protobuf support with a MessageConverter
This change adds a new HttpMessageConverter supporting Google protocol buffers (aka Protobuf). This message converter supports the following media types: * application/json * application/xml * text/plain * text/html (output only) * and by default application/x-protobuf Note, in order to generate Proto Message classes, the protoc binary must be available on your system. Issue: SPR-5807/SPR-6259
1 parent 105ea19 commit b56703e

File tree

10 files changed

+1540
-0
lines changed

10 files changed

+1540
-0
lines changed

build.gradle

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ buildscript {
66
classpath("org.springframework.build.gradle:propdeps-plugin:0.0.7")
77
classpath("org.asciidoctor:asciidoctor-gradle-plugin:0.7.0")
88
classpath("org.springframework.build.gradle:docbook-reference-plugin:0.2.8")
9+
classpath("ws.antonov.gradle.plugins:gradle-plugin-protobuf:0.9.1")
910
}
1011
}
1112

@@ -37,6 +38,7 @@ configure(allprojects) { project ->
3738
ext.tiles3Version = "3.0.4"
3839
ext.tomcatVersion = "8.0.9"
3940
ext.xstreamVersion = "1.4.7"
41+
ext.protobufVersion = "2.5.0"
4042

4143
ext.gradleScriptDir = "${rootProject.projectDir}/gradle"
4244

@@ -611,6 +613,21 @@ project("spring-web") {
611613
description = "Spring Web"
612614
apply plugin: "groovy"
613615

616+
// Re-generate Protobuf classes from *.proto files and move them in test sources
617+
if (project.hasProperty('genProtobuf')) {
618+
apply plugin: 'protobuf'
619+
620+
task updateGenProtobuf(type:Copy, dependsOn: ":spring-web:generateTestProto") {
621+
from "${project.buildDir}/generated-sources/test/"
622+
into "${projectDir}/src/test/java"
623+
doLast {
624+
project.delete "${project.buildDir}/generated-sources/test"
625+
}
626+
}
627+
628+
tasks.getByPath("compileTestJava").dependsOn "updateGenProtobuf"
629+
}
630+
614631
dependencies {
615632
compile(project(":spring-aop")) // for JaxWsPortProxyFactoryBean
616633
compile(project(":spring-beans")) // for MultiPartFilter
@@ -638,6 +655,8 @@ project("spring-web") {
638655
exclude group: "javax.servlet", module: "javax.servlet-api"
639656
}
640657
optional("log4j:log4j:1.2.17")
658+
optional("com.googlecode.protobuf-java-format:protobuf-java-format:1.2")
659+
optional("com.google.protobuf:protobuf-java:${protobufVersion}")
641660
testCompile(project(":spring-context-support")) // for JafMediaTypeFactory
642661
testCompile("xmlunit:xmlunit:1.5")
643662
testCompile("org.slf4j:slf4j-jcl:${slf4jVersion}")
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2002-2014 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.http.converter.protobuf;
18+
19+
import com.google.protobuf.ExtensionRegistry;
20+
21+
/**
22+
* Google Protocol Messages can contain message extensions that can be parsed if
23+
* the appropriate configuration has been registered in the {@code ExtensionRegistry}.
24+
*
25+
* This interface provides a facility to populate the {@code ExtensionRegistry}.
26+
*
27+
* @author Alex Antonov
28+
* @since 4.1
29+
* @see <a href="https://developers.google.com/protocol-buffers/docs/reference/java/com/google/protobuf/ExtensionRegistry">
30+
* com.google.protobuf.ExtensionRegistry</a>
31+
*/
32+
public interface ExtensionRegistryInitializer {
33+
34+
/**
35+
* Initializes the {@code ExtensionRegistry} with Protocol Message extensions
36+
* @param registry the registry to populate
37+
*/
38+
void initializeExtensionRegistry(ExtensionRegistry registry);
39+
40+
}
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
/*
2+
* Copyright 2002-2014 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.http.converter.protobuf;
18+
19+
import java.io.IOException;
20+
import java.io.InputStreamReader;
21+
import java.io.OutputStreamWriter;
22+
import java.io.StringWriter;
23+
import java.io.UnsupportedEncodingException;
24+
import java.lang.reflect.Method;
25+
import java.nio.charset.Charset;
26+
import java.util.concurrent.ConcurrentHashMap;
27+
28+
import com.google.protobuf.ExtensionRegistry;
29+
import com.google.protobuf.Message;
30+
import com.google.protobuf.TextFormat;
31+
import com.googlecode.protobuf.format.HtmlFormat;
32+
import com.googlecode.protobuf.format.JsonFormat;
33+
import com.googlecode.protobuf.format.XmlFormat;
34+
35+
import org.springframework.http.HttpHeaders;
36+
import org.springframework.http.HttpInputMessage;
37+
import org.springframework.http.HttpOutputMessage;
38+
import org.springframework.http.MediaType;
39+
import org.springframework.http.converter.AbstractHttpMessageConverter;
40+
import org.springframework.http.converter.HttpMessageNotReadableException;
41+
import org.springframework.http.converter.HttpMessageNotWritableException;
42+
import org.springframework.util.Assert;
43+
import org.springframework.util.FileCopyUtils;
44+
45+
46+
/**
47+
* Implementation of {@link org.springframework.http.converter.HttpMessageConverter}
48+
* that can read and write Protobuf {@code Message}s using the
49+
* <a href="https://developers.google.com/protocol-buffers/">Google Protocol buffers</a> library.
50+
*
51+
* <p>By default, it supports {@code application/json}, {@code application/xml}, {@code text/plain}
52+
* and {@code application/x-protobuf}. {@code text/html} is only supported when writing messages.
53+
*
54+
* <p>In order to generate Message Java classes, you should install the protoc binary on your system.
55+
* <p>Tested against Protobuf version 2.5.0.
56+
*
57+
* @author Alex Antonov
58+
* @author Brian Clozel
59+
* @since 4.1
60+
*/
61+
public class ProtobufHttpMessageConverter extends AbstractHttpMessageConverter<Message> {
62+
63+
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
64+
65+
public static final MediaType PROTOBUF = new MediaType("application", "x-protobuf", DEFAULT_CHARSET);
66+
67+
public static final String X_PROTOBUF_SCHEMA_HEADER = "X-Protobuf-Schema";
68+
public static final String X_PROTOBUF_MESSAGE_HEADER = "X-Protobuf-Message";
69+
70+
private static final ConcurrentHashMap<Class<?>, Method> newBuilderMethodCache =
71+
new ConcurrentHashMap<Class<?>, Method>();
72+
73+
private ExtensionRegistry extensionRegistry = ExtensionRegistry.newInstance();
74+
75+
/**
76+
* Construct a new {@code ProtobufHttpMessageConverter}.
77+
*/
78+
public ProtobufHttpMessageConverter() {
79+
this(null);
80+
}
81+
82+
/**
83+
* Construct a new {@code ProtobufHttpMessageConverter} with a {@link ExtensionRegistryInitializer},
84+
* allowing to register message extensions.
85+
*/
86+
public ProtobufHttpMessageConverter(ExtensionRegistryInitializer registryInitializer) {
87+
88+
//TODO: should we handle "*+json", "*+xml" as well?
89+
super(PROTOBUF, MediaType.TEXT_PLAIN,
90+
MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON);
91+
Assert.notNull(registryInitializer, "ExtensionRegistryInitializer must not be null");
92+
registryInitializer.initializeExtensionRegistry(extensionRegistry);
93+
}
94+
95+
@Override
96+
protected boolean supports(Class<?> clazz) {
97+
return Message.class.isAssignableFrom(clazz);
98+
}
99+
100+
101+
@Override
102+
protected Message readInternal(Class<? extends Message> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
103+
MediaType contentType = inputMessage.getHeaders().getContentType();
104+
contentType = contentType != null ? contentType : PROTOBUF;
105+
106+
InputStreamReader reader = new InputStreamReader(inputMessage.getBody(), getCharset(inputMessage.getHeaders()));
107+
108+
try {
109+
Message.Builder builder = createMessageBuilder(clazz);
110+
111+
if (MediaType.APPLICATION_JSON.isCompatibleWith(contentType)) {
112+
JsonFormat.merge(reader, extensionRegistry, builder);
113+
}
114+
else if (MediaType.TEXT_PLAIN.isCompatibleWith(contentType)) {
115+
TextFormat.merge(reader, extensionRegistry, builder);
116+
}
117+
else if (MediaType.APPLICATION_XML.isCompatibleWith(contentType)) {
118+
XmlFormat.merge(reader, extensionRegistry, builder);
119+
}
120+
else { // ProtobufHttpMessageConverter.PROTOBUF
121+
builder.mergeFrom(inputMessage.getBody(), extensionRegistry);
122+
}
123+
return builder.build();
124+
}
125+
catch (Exception e) {
126+
throw new HttpMessageNotReadableException("Could not read Protobuf message: " + e.getMessage(), e);
127+
}
128+
}
129+
130+
private Charset getCharset(HttpHeaders headers) {
131+
if (headers == null || headers.getContentType() == null || headers.getContentType().getCharSet() == null) {
132+
return DEFAULT_CHARSET;
133+
}
134+
return headers.getContentType().getCharSet();
135+
}
136+
137+
/**
138+
* Create a new {@code Message.Builder} instance for the given class.
139+
* <p>This method uses a ConcurrentHashMap for caching method lookups.
140+
*/
141+
private Message.Builder createMessageBuilder(Class<? extends Message> clazz) throws Exception {
142+
Method m = newBuilderMethodCache.get(clazz);
143+
if (m == null) {
144+
m = clazz.getMethod("newBuilder");
145+
newBuilderMethodCache.put(clazz, m);
146+
}
147+
return (Message.Builder) m.invoke(clazz);
148+
}
149+
150+
/**
151+
* This method overrides the parent implementation, since this HttpMessageConverter
152+
* can also produce {@code MediaType.HTML "text/html"} ContentType.
153+
*/
154+
@Override
155+
protected boolean canWrite(MediaType mediaType) {
156+
return super.canWrite(mediaType) || MediaType.TEXT_HTML.isCompatibleWith(mediaType);
157+
}
158+
159+
@Override
160+
protected void writeInternal(Message message, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
161+
MediaType contentType = outputMessage.getHeaders().getContentType();
162+
Charset charset = getCharset(contentType);
163+
OutputStreamWriter writer = new OutputStreamWriter(outputMessage.getBody(), charset);
164+
165+
if (MediaType.TEXT_HTML.isCompatibleWith(contentType)) {
166+
HtmlFormat.print(message, writer);
167+
}
168+
else if (MediaType.APPLICATION_JSON.isCompatibleWith(contentType)) {
169+
JsonFormat.print(message, writer);
170+
}
171+
else if (MediaType.TEXT_PLAIN.isCompatibleWith(contentType)) {
172+
TextFormat.print(message, writer);
173+
}
174+
else if (MediaType.APPLICATION_XML.isCompatibleWith(contentType)) {
175+
XmlFormat.print(message, writer);
176+
}
177+
else if (PROTOBUF.isCompatibleWith(contentType)) {
178+
setProtoHeader(outputMessage, message);
179+
FileCopyUtils.copy(message.toByteArray(), outputMessage.getBody());
180+
}
181+
}
182+
183+
private Charset getCharset(MediaType contentType) {
184+
return contentType.getCharSet() != null ? contentType.getCharSet() : DEFAULT_CHARSET;
185+
}
186+
187+
/**
188+
* Set the "X-Protobuf-*" HTTP headers when responding with
189+
* a message of ContentType "application/x-protobuf"
190+
*/
191+
private void setProtoHeader(final HttpOutputMessage response, final Message message) {
192+
response.getHeaders().set(X_PROTOBUF_SCHEMA_HEADER,
193+
message.getDescriptorForType().getFile().getName());
194+
195+
response.getHeaders().set(X_PROTOBUF_MESSAGE_HEADER,
196+
message.getDescriptorForType().getFullName());
197+
}
198+
199+
@Override
200+
protected MediaType getDefaultContentType(Message message) {
201+
return PROTOBUF;
202+
}
203+
204+
}

0 commit comments

Comments
 (0)