001/* 002 * Copyright 2002-2019 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.http.converter.protobuf; 018 019import java.io.IOException; 020import java.io.InputStream; 021import java.io.InputStreamReader; 022import java.io.OutputStream; 023import java.io.OutputStreamWriter; 024import java.lang.reflect.Method; 025import java.nio.charset.Charset; 026import java.nio.charset.StandardCharsets; 027import java.util.Arrays; 028import java.util.Map; 029 030import com.google.protobuf.CodedOutputStream; 031import com.google.protobuf.ExtensionRegistry; 032import com.google.protobuf.Message; 033import com.google.protobuf.TextFormat; 034import com.google.protobuf.util.JsonFormat; 035import com.googlecode.protobuf.format.FormatFactory; 036import com.googlecode.protobuf.format.ProtobufFormatter; 037 038import org.springframework.http.HttpInputMessage; 039import org.springframework.http.HttpOutputMessage; 040import org.springframework.http.MediaType; 041import org.springframework.http.converter.AbstractHttpMessageConverter; 042import org.springframework.http.converter.HttpMessageConversionException; 043import org.springframework.http.converter.HttpMessageNotReadableException; 044import org.springframework.http.converter.HttpMessageNotWritableException; 045import org.springframework.lang.Nullable; 046import org.springframework.util.Assert; 047import org.springframework.util.ClassUtils; 048import org.springframework.util.ConcurrentReferenceHashMap; 049 050import static org.springframework.http.MediaType.APPLICATION_JSON; 051import static org.springframework.http.MediaType.APPLICATION_XML; 052import static org.springframework.http.MediaType.TEXT_HTML; 053import static org.springframework.http.MediaType.TEXT_PLAIN; 054 055/** 056 * An {@code HttpMessageConverter} that reads and writes 057 * {@link com.google.protobuf.Message com.google.protobuf.Messages} using 058 * <a href="https://developers.google.com/protocol-buffers/">Google Protocol Buffers</a>. 059 * 060 * <p>To generate {@code Message} Java classes, you need to install the {@code protoc} binary. 061 * 062 * <p>This converter supports by default {@code "application/x-protobuf"} and {@code "text/plain"} 063 * with the official {@code "com.google.protobuf:protobuf-java"} library. Other formats can be 064 * supported with one of the following additional libraries on the classpath: 065 * <ul> 066 * <li>{@code "application/json"}, {@code "application/xml"}, and {@code "text/html"} (write-only) 067 * with the {@code "com.googlecode.protobuf-java-format:protobuf-java-format"} third-party library 068 * <li>{@code "application/json"} with the official {@code "com.google.protobuf:protobuf-java-util"} 069 * for Protobuf 3 (see {@link ProtobufJsonFormatHttpMessageConverter} for a configurable variant) 070 * </ul> 071 * 072 * <p>Requires Protobuf 2.6 or higher (and Protobuf Java Format 1.4 or higher for formatting). 073 * This converter will auto-adapt to Protobuf 3 and its default {@code protobuf-java-util} JSON 074 * format if the Protobuf 2 based {@code protobuf-java-format} isn't present; however, for more 075 * explicit JSON setup on Protobuf 3, consider {@link ProtobufJsonFormatHttpMessageConverter}. 076 * 077 * @author Alex Antonov 078 * @author Brian Clozel 079 * @author Juergen Hoeller 080 * @author Sebastien Deleuze 081 * @since 4.1 082 * @see FormatFactory 083 * @see JsonFormat 084 * @see ProtobufJsonFormatHttpMessageConverter 085 */ 086public class ProtobufHttpMessageConverter extends AbstractHttpMessageConverter<Message> { 087 088 /** 089 * The default charset used by the converter. 090 */ 091 public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; 092 093 /** 094 * The media-type for protobuf {@code application/x-protobuf}. 095 */ 096 public static final MediaType PROTOBUF = new MediaType("application", "x-protobuf", DEFAULT_CHARSET); 097 098 /** 099 * The HTTP header containing the protobuf schema. 100 */ 101 public static final String X_PROTOBUF_SCHEMA_HEADER = "X-Protobuf-Schema"; 102 103 /** 104 * The HTTP header containing the protobuf message. 105 */ 106 public static final String X_PROTOBUF_MESSAGE_HEADER = "X-Protobuf-Message"; 107 108 109 private static final Map<Class<?>, Method> methodCache = new ConcurrentReferenceHashMap<>(); 110 111 final ExtensionRegistry extensionRegistry; 112 113 @Nullable 114 private final ProtobufFormatSupport protobufFormatSupport; 115 116 117 /** 118 * Construct a new {@code ProtobufHttpMessageConverter}. 119 */ 120 public ProtobufHttpMessageConverter() { 121 this(null, null); 122 } 123 124 /** 125 * Construct a new {@code ProtobufHttpMessageConverter} with an 126 * initializer that allows the registration of message extensions. 127 * @param registryInitializer an initializer for message extensions 128 * @deprecated as of Spring Framework 5.1, use {@link #ProtobufHttpMessageConverter(ExtensionRegistry)} instead 129 */ 130 @Deprecated 131 public ProtobufHttpMessageConverter(@Nullable ExtensionRegistryInitializer registryInitializer) { 132 this(null, null); 133 if (registryInitializer != null) { 134 registryInitializer.initializeExtensionRegistry(this.extensionRegistry); 135 } 136 } 137 138 /** 139 * Construct a new {@code ProtobufHttpMessageConverter} with a registry that specifies 140 * protocol message extensions. 141 * @param extensionRegistry the registry to populate 142 */ 143 public ProtobufHttpMessageConverter(ExtensionRegistry extensionRegistry) { 144 this(null, extensionRegistry); 145 } 146 147 ProtobufHttpMessageConverter(@Nullable ProtobufFormatSupport formatSupport, 148 @Nullable ExtensionRegistry extensionRegistry) { 149 150 if (formatSupport != null) { 151 this.protobufFormatSupport = formatSupport; 152 } 153 else if (ClassUtils.isPresent("com.googlecode.protobuf.format.FormatFactory", getClass().getClassLoader())) { 154 this.protobufFormatSupport = new ProtobufJavaFormatSupport(); 155 } 156 else if (ClassUtils.isPresent("com.google.protobuf.util.JsonFormat", getClass().getClassLoader())) { 157 this.protobufFormatSupport = new ProtobufJavaUtilSupport(null, null); 158 } 159 else { 160 this.protobufFormatSupport = null; 161 } 162 163 setSupportedMediaTypes(Arrays.asList(this.protobufFormatSupport != null ? 164 this.protobufFormatSupport.supportedMediaTypes() : new MediaType[] {PROTOBUF, TEXT_PLAIN})); 165 166 this.extensionRegistry = (extensionRegistry == null ? ExtensionRegistry.newInstance() : extensionRegistry); 167 } 168 169 170 @Override 171 protected boolean supports(Class<?> clazz) { 172 return Message.class.isAssignableFrom(clazz); 173 } 174 175 @Override 176 protected MediaType getDefaultContentType(Message message) { 177 return PROTOBUF; 178 } 179 180 @Override 181 protected Message readInternal(Class<? extends Message> clazz, HttpInputMessage inputMessage) 182 throws IOException, HttpMessageNotReadableException { 183 184 MediaType contentType = inputMessage.getHeaders().getContentType(); 185 if (contentType == null) { 186 contentType = PROTOBUF; 187 } 188 Charset charset = contentType.getCharset(); 189 if (charset == null) { 190 charset = DEFAULT_CHARSET; 191 } 192 193 Message.Builder builder = getMessageBuilder(clazz); 194 if (PROTOBUF.isCompatibleWith(contentType)) { 195 builder.mergeFrom(inputMessage.getBody(), this.extensionRegistry); 196 } 197 else if (TEXT_PLAIN.isCompatibleWith(contentType)) { 198 InputStreamReader reader = new InputStreamReader(inputMessage.getBody(), charset); 199 TextFormat.merge(reader, this.extensionRegistry, builder); 200 } 201 else if (this.protobufFormatSupport != null) { 202 this.protobufFormatSupport.merge( 203 inputMessage.getBody(), charset, contentType, this.extensionRegistry, builder); 204 } 205 return builder.build(); 206 } 207 208 /** 209 * Create a new {@code Message.Builder} instance for the given class. 210 * <p>This method uses a ConcurrentReferenceHashMap for caching method lookups. 211 */ 212 private Message.Builder getMessageBuilder(Class<? extends Message> clazz) { 213 try { 214 Method method = methodCache.get(clazz); 215 if (method == null) { 216 method = clazz.getMethod("newBuilder"); 217 methodCache.put(clazz, method); 218 } 219 return (Message.Builder) method.invoke(clazz); 220 } 221 catch (Exception ex) { 222 throw new HttpMessageConversionException( 223 "Invalid Protobuf Message type: no invocable newBuilder() method on " + clazz, ex); 224 } 225 } 226 227 228 @Override 229 protected boolean canWrite(@Nullable MediaType mediaType) { 230 return (super.canWrite(mediaType) || 231 (this.protobufFormatSupport != null && this.protobufFormatSupport.supportsWriteOnly(mediaType))); 232 } 233 234 @SuppressWarnings("deprecation") 235 @Override 236 protected void writeInternal(Message message, HttpOutputMessage outputMessage) 237 throws IOException, HttpMessageNotWritableException { 238 239 MediaType contentType = outputMessage.getHeaders().getContentType(); 240 if (contentType == null) { 241 contentType = getDefaultContentType(message); 242 Assert.state(contentType != null, "No content type"); 243 } 244 Charset charset = contentType.getCharset(); 245 if (charset == null) { 246 charset = DEFAULT_CHARSET; 247 } 248 249 if (PROTOBUF.isCompatibleWith(contentType)) { 250 setProtoHeader(outputMessage, message); 251 CodedOutputStream codedOutputStream = CodedOutputStream.newInstance(outputMessage.getBody()); 252 message.writeTo(codedOutputStream); 253 codedOutputStream.flush(); 254 } 255 else if (TEXT_PLAIN.isCompatibleWith(contentType)) { 256 OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputMessage.getBody(), charset); 257 TextFormat.print(message, outputStreamWriter); // deprecated on Protobuf 3.9 258 outputStreamWriter.flush(); 259 outputMessage.getBody().flush(); 260 } 261 else if (this.protobufFormatSupport != null) { 262 this.protobufFormatSupport.print(message, outputMessage.getBody(), contentType, charset); 263 outputMessage.getBody().flush(); 264 } 265 } 266 267 /** 268 * Set the "X-Protobuf-*" HTTP headers when responding with a message of 269 * content type "application/x-protobuf" 270 * <p><b>Note:</b> <code>outputMessage.getBody()</code> should not have been called 271 * before because it writes HTTP headers (making them read only).</p> 272 */ 273 private void setProtoHeader(HttpOutputMessage response, Message message) { 274 response.getHeaders().set(X_PROTOBUF_SCHEMA_HEADER, message.getDescriptorForType().getFile().getName()); 275 response.getHeaders().set(X_PROTOBUF_MESSAGE_HEADER, message.getDescriptorForType().getFullName()); 276 } 277 278 279 /** 280 * Protobuf format support. 281 */ 282 interface ProtobufFormatSupport { 283 284 MediaType[] supportedMediaTypes(); 285 286 boolean supportsWriteOnly(@Nullable MediaType mediaType); 287 288 void merge(InputStream input, Charset charset, MediaType contentType, 289 ExtensionRegistry extensionRegistry, Message.Builder builder) 290 throws IOException, HttpMessageConversionException; 291 292 void print(Message message, OutputStream output, MediaType contentType, Charset charset) 293 throws IOException, HttpMessageConversionException; 294 } 295 296 297 /** 298 * {@link ProtobufFormatSupport} implementation used when 299 * {@code com.googlecode.protobuf.format.FormatFactory} is available. 300 */ 301 static class ProtobufJavaFormatSupport implements ProtobufFormatSupport { 302 303 private final ProtobufFormatter jsonFormatter; 304 305 private final ProtobufFormatter xmlFormatter; 306 307 private final ProtobufFormatter htmlFormatter; 308 309 public ProtobufJavaFormatSupport() { 310 FormatFactory formatFactory = new FormatFactory(); 311 this.jsonFormatter = formatFactory.createFormatter(FormatFactory.Formatter.JSON); 312 this.xmlFormatter = formatFactory.createFormatter(FormatFactory.Formatter.XML); 313 this.htmlFormatter = formatFactory.createFormatter(FormatFactory.Formatter.HTML); 314 } 315 316 @Override 317 public MediaType[] supportedMediaTypes() { 318 return new MediaType[] {PROTOBUF, TEXT_PLAIN, APPLICATION_XML, APPLICATION_JSON}; 319 } 320 321 @Override 322 public boolean supportsWriteOnly(@Nullable MediaType mediaType) { 323 return TEXT_HTML.isCompatibleWith(mediaType); 324 } 325 326 @Override 327 public void merge(InputStream input, Charset charset, MediaType contentType, 328 ExtensionRegistry extensionRegistry, Message.Builder builder) 329 throws IOException, HttpMessageConversionException { 330 331 if (contentType.isCompatibleWith(APPLICATION_JSON)) { 332 this.jsonFormatter.merge(input, charset, extensionRegistry, builder); 333 } 334 else if (contentType.isCompatibleWith(APPLICATION_XML)) { 335 this.xmlFormatter.merge(input, charset, extensionRegistry, builder); 336 } 337 else { 338 throw new HttpMessageConversionException( 339 "protobuf-java-format does not support parsing " + contentType); 340 } 341 } 342 343 @Override 344 public void print(Message message, OutputStream output, MediaType contentType, Charset charset) 345 throws IOException, HttpMessageConversionException { 346 347 if (contentType.isCompatibleWith(APPLICATION_JSON)) { 348 this.jsonFormatter.print(message, output, charset); 349 } 350 else if (contentType.isCompatibleWith(APPLICATION_XML)) { 351 this.xmlFormatter.print(message, output, charset); 352 } 353 else if (contentType.isCompatibleWith(TEXT_HTML)) { 354 this.htmlFormatter.print(message, output, charset); 355 } 356 else { 357 throw new HttpMessageConversionException( 358 "protobuf-java-format does not support printing " + contentType); 359 } 360 } 361 } 362 363 364 /** 365 * {@link ProtobufFormatSupport} implementation used when 366 * {@code com.google.protobuf.util.JsonFormat} is available. 367 */ 368 static class ProtobufJavaUtilSupport implements ProtobufFormatSupport { 369 370 private final JsonFormat.Parser parser; 371 372 private final JsonFormat.Printer printer; 373 374 public ProtobufJavaUtilSupport(@Nullable JsonFormat.Parser parser, @Nullable JsonFormat.Printer printer) { 375 this.parser = (parser != null ? parser : JsonFormat.parser()); 376 this.printer = (printer != null ? printer : JsonFormat.printer()); 377 } 378 379 @Override 380 public MediaType[] supportedMediaTypes() { 381 return new MediaType[] {PROTOBUF, TEXT_PLAIN, APPLICATION_JSON}; 382 } 383 384 @Override 385 public boolean supportsWriteOnly(@Nullable MediaType mediaType) { 386 return false; 387 } 388 389 @Override 390 public void merge(InputStream input, Charset charset, MediaType contentType, 391 ExtensionRegistry extensionRegistry, Message.Builder builder) 392 throws IOException, HttpMessageConversionException { 393 394 if (contentType.isCompatibleWith(APPLICATION_JSON)) { 395 InputStreamReader reader = new InputStreamReader(input, charset); 396 this.parser.merge(reader, builder); 397 } 398 else { 399 throw new HttpMessageConversionException( 400 "protobuf-java-util does not support parsing " + contentType); 401 } 402 } 403 404 @Override 405 public void print(Message message, OutputStream output, MediaType contentType, Charset charset) 406 throws IOException, HttpMessageConversionException { 407 408 if (contentType.isCompatibleWith(APPLICATION_JSON)) { 409 OutputStreamWriter writer = new OutputStreamWriter(output, charset); 410 this.printer.appendTo(message, writer); 411 writer.flush(); 412 } 413 else { 414 throw new HttpMessageConversionException( 415 "protobuf-java-util does not support printing " + contentType); 416 } 417 } 418 } 419 420}