2424import java .lang .reflect .Method ;
2525import java .nio .charset .Charset ;
2626import java .nio .charset .StandardCharsets ;
27+ import java .util .Arrays ;
2728import java .util .concurrent .ConcurrentHashMap ;
2829
2930import com .google .protobuf .CodedOutputStream ;
3031import com .google .protobuf .ExtensionRegistry ;
3132import com .google .protobuf .Message ;
3233import com .google .protobuf .TextFormat ;
34+ import com .google .protobuf .util .JsonFormat ;
3335import com .googlecode .protobuf .format .FormatFactory ;
3436import com .googlecode .protobuf .format .ProtobufFormatter ;
3537
4143import org .springframework .http .converter .HttpMessageNotWritableException ;
4244import 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 */
6977public 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