Skip to content

Commit ce5e2b9

Browse files
committed
ProtobufJsonFormatHttpMessageConverter for configurable JSON processing
Issue: SPR-15550
1 parent 113f0fb commit ce5e2b9

File tree

4 files changed

+317
-99
lines changed

4 files changed

+317
-99
lines changed

spring-web/src/main/java/org/springframework/http/converter/protobuf/ProtobufHttpMessageConverter.java

Lines changed: 109 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,14 @@
2424
import java.lang.reflect.Method;
2525
import java.nio.charset.Charset;
2626
import java.nio.charset.StandardCharsets;
27+
import java.util.Arrays;
2728
import java.util.concurrent.ConcurrentHashMap;
2829

2930
import com.google.protobuf.CodedOutputStream;
3031
import com.google.protobuf.ExtensionRegistry;
3132
import com.google.protobuf.Message;
3233
import com.google.protobuf.TextFormat;
34+
import com.google.protobuf.util.JsonFormat;
3335
import com.googlecode.protobuf.format.FormatFactory;
3436
import com.googlecode.protobuf.format.ProtobufFormatter;
3537

@@ -41,30 +43,36 @@
4143
import org.springframework.http.converter.HttpMessageNotWritableException;
4244
import org.springframework.util.ClassUtils;
4345

46+
import static org.springframework.http.MediaType.*;
47+
4448
/**
4549
* An {@code HttpMessageConverter} that reads and writes {@link com.google.protobuf.Message}s
4650
* using <a href="https://developers.google.com/protocol-buffers/">Google Protocol Buffers</a>.
4751
*
48-
* <p>This converter supports by default {@code "application/x-protobuf"} and {@code "text/plain"}
49-
* with the official {@code "com.google.protobuf:protobuf-java"} library.
52+
* <p>To generate {@code Message} Java classes, you need to install the {@code protoc} binary.
5053
*
51-
* <p>Other formats can be supported with additional libraries:
54+
* <p>This converter supports by default {@code "application/x-protobuf"} and {@code "text/plain"}
55+
* with the official {@code "com.google.protobuf:protobuf-java"} library. Other formats can be
56+
* supported with one of the following additional libraries on the classpath:
5257
* <ul>
53-
* <li>{@code "application/json"} with the official library
54-
* {@code "com.google.protobuf:protobuf-java-util"}
55-
* <li>{@code "application/json"}, {@code "application/xml"} and {@code "text/html"} (write only)
56-
* can be supported with the 3rd party library
57-
* {@code "com.googlecode.protobuf-java-format:protobuf-java-format"}
58+
* <li>{@code "application/json"}, {@code "application/xml"}, and {@code "text/html"} (write-only)
59+
* with the {@code "com.googlecode.protobuf-java-format:protobuf-java-format"} third-party library
60+
* <li>{@code "application/json"} with the official {@code "com.google.protobuf:protobuf-java-util"}
61+
* for Protobuf 3 (see {@link ProtobufJsonFormatHttpMessageConverter} for a configurable variant)
5862
* </ul>
5963
*
60-
* <p>To generate {@code Message} Java classes, you need to install the {@code protoc} binary.
61-
*
62-
* <p>Requires Protobuf 2.6 or 3.x and Protobuf Java Format 1.4 or higher, as of Spring 5.0.
64+
* <p>Requires Protobuf 2.6 or higher (and Protobuf Java Format 1.4 or higher for formatting).
65+
* This converter will auto-adapt to Protobuf 3 and its default {@code protobuf-java-util} JSON
66+
* format if the Protobuf 2 based {@code protobuf-java-format} isn't present; however, for more
67+
* explicit JSON setup on Protobuf 3, consider {@link ProtobufJsonFormatHttpMessageConverter}.
6368
*
6469
* @author Alex Antonov
6570
* @author Brian Clozel
6671
* @author Juergen Hoeller
6772
* @since 4.1
73+
* @see FormatFactory
74+
* @see JsonFormat
75+
* @see ProtobufJsonFormatHttpMessageConverter
6876
*/
6977
public class ProtobufHttpMessageConverter extends AbstractHttpMessageConverter<Message> {
7078

@@ -76,33 +84,12 @@ public class ProtobufHttpMessageConverter extends AbstractHttpMessageConverter<M
7684

7785
public static final String X_PROTOBUF_MESSAGE_HEADER = "X-Protobuf-Message";
7886

79-
private static final boolean isProtobufJavaUtilPresent =
80-
ClassUtils.isPresent("com.google.protobuf.util.JsonFormat", ProtobufHttpMessageConverter.class.getClassLoader());
81-
82-
private static final boolean isProtobufJavaFormatPresent =
83-
ClassUtils.isPresent("com.googlecode.protobuf.format.JsonFormat", ProtobufHttpMessageConverter.class.getClassLoader());
8487

8588
private static final ConcurrentHashMap<Class<?>, Method> methodCache = new ConcurrentHashMap<>();
8689

87-
private final ProtobufFormatSupport protobufFormatSupport;
88-
8990
private final ExtensionRegistry extensionRegistry = ExtensionRegistry.newInstance();
9091

91-
92-
private static final MediaType[] SUPPORTED_MEDIATYPES;
93-
94-
static {
95-
if (isProtobufJavaFormatPresent) {
96-
SUPPORTED_MEDIATYPES = new MediaType[] {PROTOBUF, MediaType.TEXT_PLAIN, MediaType.APPLICATION_XML,
97-
MediaType.APPLICATION_JSON};
98-
}
99-
else if (isProtobufJavaUtilPresent) {
100-
SUPPORTED_MEDIATYPES = new MediaType[] {PROTOBUF, MediaType.TEXT_PLAIN, MediaType.APPLICATION_JSON};
101-
}
102-
else {
103-
SUPPORTED_MEDIATYPES = new MediaType[] {PROTOBUF, MediaType.TEXT_PLAIN};
104-
}
105-
}
92+
private final ProtobufFormatSupport protobufFormatSupport;
10693

10794

10895
/**
@@ -115,18 +102,29 @@ public ProtobufHttpMessageConverter() {
115102
/**
116103
* Construct a new {@code ProtobufHttpMessageConverter} with an
117104
* initializer that allows the registration of message extensions.
105+
* @param registryInitializer an initializer for message extensions
118106
*/
119107
public ProtobufHttpMessageConverter(ExtensionRegistryInitializer registryInitializer) {
120-
super(SUPPORTED_MEDIATYPES);
121-
if (isProtobufJavaFormatPresent) {
108+
this(null, registryInitializer);
109+
}
110+
111+
ProtobufHttpMessageConverter(ProtobufFormatSupport formatSupport, ExtensionRegistryInitializer registryInitializer) {
112+
if (formatSupport != null) {
113+
this.protobufFormatSupport = formatSupport;
114+
}
115+
else if (ClassUtils.isPresent("com.googlecode.protobuf.format.FormatFactory", getClass().getClassLoader())) {
122116
this.protobufFormatSupport = new ProtobufJavaFormatSupport();
123117
}
124-
else if (isProtobufJavaUtilPresent) {
125-
this.protobufFormatSupport = new ProtobufJavaUtilSupport();
118+
else if (ClassUtils.isPresent("com.google.protobuf.util.JsonFormat", getClass().getClassLoader())) {
119+
this.protobufFormatSupport = new ProtobufJavaUtilSupport(null, null);
126120
}
127121
else {
128122
this.protobufFormatSupport = null;
129123
}
124+
125+
setSupportedMediaTypes(Arrays.asList((this.protobufFormatSupport != null ?
126+
this.protobufFormatSupport.supportedMediaTypes() : new MediaType[] {PROTOBUF, TEXT_PLAIN})));
127+
130128
if (registryInitializer != null) {
131129
registryInitializer.initializeExtensionRegistry(this.extensionRegistry);
132130
}
@@ -161,11 +159,11 @@ protected Message readInternal(Class<? extends Message> clazz, HttpInputMessage
161159
if (PROTOBUF.isCompatibleWith(contentType)) {
162160
builder.mergeFrom(inputMessage.getBody(), this.extensionRegistry);
163161
}
164-
else if (MediaType.TEXT_PLAIN.isCompatibleWith(contentType)) {
162+
else if (TEXT_PLAIN.isCompatibleWith(contentType)) {
165163
InputStreamReader reader = new InputStreamReader(inputMessage.getBody(), charset);
166164
TextFormat.merge(reader, this.extensionRegistry, builder);
167165
}
168-
else if (isProtobufJavaUtilPresent || isProtobufJavaFormatPresent) {
166+
else if (this.protobufFormatSupport != null) {
169167
this.protobufFormatSupport.merge(inputMessage.getBody(), charset, contentType,
170168
this.extensionRegistry, builder);
171169
}
@@ -176,14 +174,10 @@ else if (isProtobufJavaUtilPresent || isProtobufJavaFormatPresent) {
176174
}
177175
}
178176

179-
/**
180-
* This method overrides the parent implementation, since this HttpMessageConverter
181-
* can also produce {@code MediaType.HTML "text/html"} ContentType.
182-
*/
183177
@Override
184178
protected boolean canWrite(MediaType mediaType) {
185179
return (super.canWrite(mediaType) ||
186-
(isProtobufJavaFormatPresent && MediaType.TEXT_HTML.isCompatibleWith(mediaType)));
180+
(this.protobufFormatSupport != null && this.protobufFormatSupport.supportsWriteOnly(mediaType)));
187181
}
188182

189183
@Override
@@ -205,13 +199,13 @@ protected void writeInternal(Message message, HttpOutputMessage outputMessage)
205199
message.writeTo(codedOutputStream);
206200
codedOutputStream.flush();
207201
}
208-
else if (MediaType.TEXT_PLAIN.isCompatibleWith(contentType)) {
209-
final OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputMessage.getBody(), charset);
202+
else if (TEXT_PLAIN.isCompatibleWith(contentType)) {
203+
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputMessage.getBody(), charset);
210204
TextFormat.print(message, outputStreamWriter);
211205
outputStreamWriter.flush();
212206
outputMessage.getBody().flush();
213207
}
214-
else if (isProtobufJavaUtilPresent || isProtobufJavaFormatPresent) {
208+
else if (this.protobufFormatSupport != null) {
215209
this.protobufFormatSupport.print(message, outputMessage.getBody(), contentType, charset);
216210
outputMessage.getBody().flush();
217211
}
@@ -243,98 +237,122 @@ private static Message.Builder getMessageBuilder(Class<? extends Message> clazz)
243237
}
244238

245239

246-
private interface ProtobufFormatSupport {
240+
interface ProtobufFormatSupport {
241+
242+
MediaType[] supportedMediaTypes();
243+
244+
boolean supportsWriteOnly(MediaType mediaType);
247245

248246
void merge(InputStream input, Charset charset, MediaType contentType, ExtensionRegistry extensionRegistry,
249247
Message.Builder builder) throws IOException;
250248

251-
void print(Message message, OutputStream output, MediaType contentType, Charset cs) throws IOException;
249+
void print(Message message, OutputStream output, MediaType contentType, Charset charset) throws IOException;
252250
}
253251

254252

255-
private class ProtobufJavaUtilSupport implements ProtobufFormatSupport {
253+
static class ProtobufJavaFormatSupport implements ProtobufFormatSupport {
254+
255+
private final ProtobufFormatter jsonFormatter;
256256

257-
private final com.google.protobuf.util.JsonFormat.Parser parser;
257+
private final ProtobufFormatter xmlFormatter;
258258

259-
private final com.google.protobuf.util.JsonFormat.Printer printer;
259+
private final ProtobufFormatter htmlFormatter;
260260

261-
public ProtobufJavaUtilSupport() {
262-
this.parser = com.google.protobuf.util.JsonFormat.parser();
263-
this.printer = com.google.protobuf.util.JsonFormat.printer();
261+
public ProtobufJavaFormatSupport() {
262+
FormatFactory formatFactory = new FormatFactory();
263+
this.jsonFormatter = formatFactory.createFormatter(FormatFactory.Formatter.JSON);
264+
this.xmlFormatter = formatFactory.createFormatter(FormatFactory.Formatter.XML);
265+
this.htmlFormatter = formatFactory.createFormatter(FormatFactory.Formatter.HTML);
266+
}
267+
268+
@Override
269+
public MediaType[] supportedMediaTypes() {
270+
return new MediaType[] {PROTOBUF, TEXT_PLAIN, APPLICATION_XML, APPLICATION_JSON};
271+
}
272+
273+
@Override
274+
public boolean supportsWriteOnly(MediaType mediaType) {
275+
return TEXT_HTML.isCompatibleWith(mediaType);
264276
}
265277

266278
@Override
267279
public void merge(InputStream input, Charset charset, MediaType contentType,
268280
ExtensionRegistry extensionRegistry, Message.Builder builder) throws IOException {
269281

270-
if (contentType.isCompatibleWith(MediaType.APPLICATION_JSON)) {
271-
InputStreamReader reader = new InputStreamReader(input, charset);
272-
this.parser.merge(reader, builder);
282+
if (contentType.isCompatibleWith(APPLICATION_JSON)) {
283+
this.jsonFormatter.merge(input, charset, extensionRegistry, builder);
284+
}
285+
else if (contentType.isCompatibleWith(APPLICATION_XML)) {
286+
this.xmlFormatter.merge(input, charset, extensionRegistry, builder);
273287
}
274288
else {
275-
throw new IOException(
276-
"com.googlecode.protobuf:protobuf-java-util does not support " + contentType + " format");
289+
throw new IOException("com.google.protobuf.util does not support " + contentType + " format");
277290
}
278291
}
279292

280293
@Override
281-
public void print(Message message, OutputStream output, MediaType contentType, Charset cs) throws IOException {
282-
if (contentType.isCompatibleWith(MediaType.APPLICATION_JSON)) {
283-
this.printer.appendTo(message, new OutputStreamWriter(output, cs));
294+
public void print(Message message, OutputStream output, MediaType contentType, Charset charset)
295+
throws IOException {
296+
297+
if (contentType.isCompatibleWith(APPLICATION_JSON)) {
298+
this.jsonFormatter.print(message, output, charset);
299+
}
300+
else if (contentType.isCompatibleWith(APPLICATION_XML)) {
301+
this.xmlFormatter.print(message, output, charset);
302+
}
303+
else if (contentType.isCompatibleWith(TEXT_HTML)) {
304+
this.htmlFormatter.print(message, output, charset);
284305
}
285306
else {
286-
throw new IOException(
287-
"com.googlecode.protobuf:protobuf-java-util does not support " + contentType + " format");
307+
throw new IOException("protobuf-java-format does not support " + contentType + " format");
288308
}
289309
}
290310
}
291311

292312

293-
private class ProtobufJavaFormatSupport implements ProtobufFormatSupport {
313+
static class ProtobufJavaUtilSupport implements ProtobufFormatSupport {
294314

295-
private final FormatFactory FORMAT_FACTORY;
315+
private final JsonFormat.Parser parser;
296316

297-
private final ProtobufFormatter JSON_FORMATTER;
317+
private final JsonFormat.Printer printer;
298318

299-
private final ProtobufFormatter XML_FORMATTER;
319+
public ProtobufJavaUtilSupport(JsonFormat.Parser parser, JsonFormat.Printer printer) {
320+
this.parser = (parser != null ? parser : JsonFormat.parser());
321+
this.printer = (printer != null ? printer : JsonFormat.printer());
322+
}
300323

301-
private final ProtobufFormatter HTML_FORMATTER;
324+
@Override
325+
public MediaType[] supportedMediaTypes() {
326+
return new MediaType[] {PROTOBUF, TEXT_PLAIN, APPLICATION_JSON};
327+
}
302328

303-
public ProtobufJavaFormatSupport() {
304-
FORMAT_FACTORY = new FormatFactory();
305-
JSON_FORMATTER = FORMAT_FACTORY.createFormatter(FormatFactory.Formatter.JSON);
306-
XML_FORMATTER = FORMAT_FACTORY.createFormatter(FormatFactory.Formatter.XML);
307-
HTML_FORMATTER = FORMAT_FACTORY.createFormatter(FormatFactory.Formatter.HTML);
329+
@Override
330+
public boolean supportsWriteOnly(MediaType mediaType) {
331+
return false;
308332
}
309333

310334
@Override
311335
public void merge(InputStream input, Charset charset, MediaType contentType,
312336
ExtensionRegistry extensionRegistry, Message.Builder builder) throws IOException {
313337

314-
if (contentType.isCompatibleWith(MediaType.APPLICATION_JSON)) {
315-
JSON_FORMATTER.merge(input, charset, extensionRegistry, builder);
316-
}
317-
else if (contentType.isCompatibleWith(MediaType.APPLICATION_XML)) {
318-
XML_FORMATTER.merge(input, charset, extensionRegistry, builder);
338+
if (contentType.isCompatibleWith(APPLICATION_JSON)) {
339+
InputStreamReader reader = new InputStreamReader(input, charset);
340+
this.parser.merge(reader, builder);
319341
}
320342
else {
321-
throw new IOException("com.google.protobuf.util does not support " + contentType + " format");
343+
throw new IOException("protobuf-java-util does not support " + contentType + " format");
322344
}
323345
}
324346

325347
@Override
326-
public void print(Message message, OutputStream output, MediaType contentType, Charset cs) throws IOException {
327-
if (contentType.isCompatibleWith(MediaType.APPLICATION_JSON)) {
328-
JSON_FORMATTER.print(message, output, cs);
329-
}
330-
else if (contentType.isCompatibleWith(MediaType.APPLICATION_XML)) {
331-
XML_FORMATTER.print(message, output, cs);
332-
}
333-
else if (contentType.isCompatibleWith(MediaType.TEXT_HTML)) {
334-
HTML_FORMATTER.print(message, output, cs);
348+
public void print(Message message, OutputStream output, MediaType contentType, Charset charset)
349+
throws IOException {
350+
351+
if (contentType.isCompatibleWith(APPLICATION_JSON)) {
352+
this.printer.appendTo(message, new OutputStreamWriter(output, charset));
335353
}
336354
else {
337-
throw new IOException("com.google.protobuf.util does not support " + contentType + " format");
355+
throw new IOException("protobuf-java-util does not support " + contentType + " format");
338356
}
339357
}
340358
}

0 commit comments

Comments
 (0)