Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
#24022 - added protobuf MessageConverter
- Loading branch information
Showing
10 changed files
with
1,722 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
355 changes: 355 additions & 0 deletions
355
...aging/src/main/java/org/springframework/messaging/converter/ProtobufMessageConverter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,355 @@ | ||
package org.springframework.messaging.converter; | ||
|
||
import com.google.protobuf.ExtensionRegistry; | ||
import com.google.protobuf.Message; | ||
import com.google.protobuf.util.JsonFormat; | ||
import com.googlecode.protobuf.format.HtmlFormat; | ||
import com.googlecode.protobuf.format.XmlFormat; | ||
import org.springframework.lang.Nullable; | ||
import org.springframework.messaging.MessageHeaders; | ||
import org.springframework.util.ClassUtils; | ||
import org.springframework.util.ConcurrentReferenceHashMap; | ||
import org.springframework.util.MimeType; | ||
|
||
import java.io.*; | ||
import java.lang.reflect.Method; | ||
import java.nio.charset.Charset; | ||
import java.nio.charset.StandardCharsets; | ||
import java.util.Arrays; | ||
import java.util.Map; | ||
|
||
import static org.springframework.util.MimeTypeUtils.*; | ||
|
||
/** | ||
* An {@code MessageConverter} that reads and writes | ||
* {@link com.google.protobuf.Message com.google.protobuf.Messages} using | ||
* <a href="https://developers.google.com/protocol-buffers/">Google Protocol Buffers</a>. | ||
* | ||
* <p>To generate {@code Message} Java classes, you need to install the {@code protoc} binary. | ||
* | ||
* <p>This converter supports by default {@code "application/x-protobuf"} 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: | ||
* <ul> | ||
* <li>{@code "application/json"}, {@code "application/xml"}, and {@code "text/html"} (write-only) | ||
* with the {@code "com.googlecode.protobuf-java-format:protobuf-java-format"} third-party library | ||
* <li>{@code "application/json"} with the official {@code "com.google.protobuf:protobuf-java-util"} | ||
* | ||
* @author Parviz Rozikov | ||
*/ | ||
public class ProtobufMessageConverter extends AbstractMessageConverter { | ||
|
||
|
||
/** | ||
* The default charset used by the converter. | ||
*/ | ||
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; | ||
|
||
/** | ||
* The mime-type for protobuf {@code application/x-protobuf}. | ||
*/ | ||
public static final MimeType PROTOBUF = new MimeType("application", "x-protobuf", DEFAULT_CHARSET); | ||
|
||
|
||
private static final Map<Class<?>, Method> methodCache = new ConcurrentReferenceHashMap<>(); | ||
|
||
final ExtensionRegistry extensionRegistry; | ||
|
||
@Nullable | ||
private final ProtobufFormatSupport protobufFormatSupport; | ||
|
||
|
||
/** | ||
* Construct a new {@code ProtobufMessageConverter}. | ||
*/ | ||
public ProtobufMessageConverter() { | ||
this((ProtobufFormatSupport) null, (ExtensionRegistry) null); | ||
} | ||
|
||
|
||
/** | ||
* Construct a new {@code ProtobufMessageConverter} with a registry that specifies | ||
* protocol message extensions. | ||
* @param extensionRegistry the registry to populate | ||
*/ | ||
public ProtobufMessageConverter(@Nullable ExtensionRegistry extensionRegistry) { | ||
this(null, extensionRegistry); | ||
} | ||
|
||
/** | ||
* Construct a new {@code ProtobufMessageConverter} with the given | ||
* {@code JsonFormat.Parser} and {@code JsonFormat.Printer} configuration. | ||
* @param parser the JSON parser configuration | ||
* @param printer the JSON printer configuration | ||
*/ | ||
public ProtobufMessageConverter(@Nullable JsonFormat.Parser parser, | ||
@Nullable JsonFormat.Printer printer) { | ||
this(new ProtobufJavaUtilSupport(parser, printer), (ExtensionRegistry) null); | ||
} | ||
|
||
/** | ||
* Construct a new {@code ProtobufMessageConverter} with the given | ||
* {@code JsonFormat.Parser} and {@code JsonFormat.Printer} configuration, also | ||
* accepting a registry that specifies protocol message extensions. | ||
* @param parser the JSON parser configuration | ||
* @param printer the JSON printer configuration | ||
* @param extensionRegistry the registry to populate | ||
*/ | ||
public ProtobufMessageConverter(@Nullable JsonFormat.Parser parser, | ||
@Nullable JsonFormat.Printer printer, @Nullable ExtensionRegistry extensionRegistry) { | ||
this(new ProtobufJavaUtilSupport(parser, printer), extensionRegistry); | ||
} | ||
|
||
|
||
/** | ||
* Construct a new {@code ProtobufMessageConverter} with the given | ||
* {@code ProtobufFormatSupport} configuration, also | ||
* accepting a registry that specifies protocol message extensions. | ||
* @param formatSupport support third party | ||
* @param extensionRegistry the registry to populate | ||
*/ | ||
public ProtobufMessageConverter(@Nullable ProtobufFormatSupport formatSupport, | ||
@Nullable ExtensionRegistry extensionRegistry) { | ||
super(PROTOBUF); | ||
|
||
if (formatSupport != null) { | ||
this.protobufFormatSupport = formatSupport; | ||
} else if (ClassUtils.isPresent("com.googlecode.protobuf.format.FormatFactory", getClass().getClassLoader())) { | ||
this.protobufFormatSupport = new ProtobufJavaFormatSupport(); | ||
} else if (ClassUtils.isPresent("com.google.protobuf.util.JsonFormat", getClass().getClassLoader())) { | ||
this.protobufFormatSupport = new ProtobufJavaUtilSupport(null, null); | ||
} else { | ||
this.protobufFormatSupport = null; | ||
} | ||
|
||
setSupportedMimeTypes(Arrays.asList(this.protobufFormatSupport != null ? | ||
this.protobufFormatSupport.supportedMediaTypes() : new MimeType[]{PROTOBUF})); | ||
|
||
this.extensionRegistry = (extensionRegistry == null ? ExtensionRegistry.newInstance() : extensionRegistry); | ||
} | ||
|
||
|
||
/** | ||
* Create a new {@code Message.Builder} instance for the given class. | ||
* <p>This method uses a ConcurrentReferenceHashMap for caching method lookups. | ||
*/ | ||
private Message.Builder getMessageBuilder(Class<?> clazz) { | ||
try { | ||
assert supports(clazz); | ||
|
||
Method method = methodCache.get(clazz); | ||
if (method == null) { | ||
method = clazz.getMethod("newBuilder"); | ||
methodCache.put(clazz, method); | ||
} | ||
return (Message.Builder) method.invoke(clazz); | ||
} catch (Exception ex) { | ||
throw new MessageConversionException( | ||
"Invalid Protobuf Message type: no invocable newBuilder() method on " + clazz, ex); | ||
} | ||
} | ||
|
||
|
||
@Override | ||
protected Object convertFromInternal(org.springframework.messaging.Message<?> message, Class<?> targetClass, @Nullable Object conversionHint) { | ||
MimeType contentType = getMimeType(message.getHeaders()); | ||
final Object payload = message.getPayload(); | ||
|
||
if (contentType == null) { | ||
contentType = PROTOBUF; | ||
} | ||
|
||
Charset charset = contentType.getCharset(); | ||
if (charset == null) { | ||
charset = DEFAULT_CHARSET; | ||
} | ||
|
||
Message.Builder builder = getMessageBuilder(targetClass); | ||
|
||
try { | ||
if (PROTOBUF.isCompatibleWith(contentType)) { | ||
builder.mergeFrom((byte[]) payload, this.extensionRegistry); | ||
} else if (protobufFormatSupport != null) { | ||
this.protobufFormatSupport.merge( | ||
message, charset, contentType, this.extensionRegistry, builder); | ||
} | ||
|
||
} catch (IOException e) { | ||
throw new MessageConversionException(message, "Could not read proto message" + e.getMessage(), e); | ||
} | ||
|
||
return builder.build(); | ||
} | ||
|
||
|
||
@Override | ||
protected Object convertToInternal(Object payload, @Nullable MessageHeaders headers, @Nullable Object conversionHint) { | ||
|
||
final Message message = (Message) payload; | ||
|
||
MimeType contentType = getMimeType(headers); | ||
if (contentType == null) { | ||
contentType = PROTOBUF; | ||
|
||
} | ||
|
||
Charset charset = contentType.getCharset(); | ||
if (charset == null) { | ||
charset = DEFAULT_CHARSET; | ||
} | ||
|
||
try { | ||
if (PROTOBUF.isCompatibleWith(contentType)) { | ||
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); | ||
message.writeTo(byteArrayOutputStream); | ||
payload = byteArrayOutputStream.toByteArray(); | ||
} else if (this.protobufFormatSupport != null) { | ||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); | ||
this.protobufFormatSupport.print(message, outputStream, contentType, charset); | ||
payload = new String(outputStream.toByteArray(), charset); | ||
} | ||
} catch (IOException e) { | ||
e.printStackTrace(); | ||
} | ||
return payload; | ||
} | ||
|
||
@Override | ||
protected boolean supports(Class<?> clazz) { | ||
return Message.class.isAssignableFrom(clazz); | ||
} | ||
|
||
|
||
/** | ||
* Protobuf format support. | ||
*/ | ||
interface ProtobufFormatSupport { | ||
|
||
MimeType[] supportedMediaTypes(); | ||
|
||
boolean supportsWriteOnly(@Nullable MimeType mediaType); | ||
|
||
void merge(org.springframework.messaging.Message<?> message, Charset charset, MimeType contentType, | ||
ExtensionRegistry extensionRegistry, Message.Builder builder) | ||
throws IOException, MessageConversionException; | ||
|
||
void print(Message message, OutputStream output, MimeType contentType, Charset charset) | ||
throws IOException, MessageConversionException; | ||
} | ||
|
||
/** | ||
* {@link ProtobufFormatSupport} implementation used when | ||
* {@code com.googlecode.protobuf.format.FormatFactory} is available. | ||
*/ | ||
static class ProtobufJavaFormatSupport implements ProtobufFormatSupport { | ||
|
||
private final com.googlecode.protobuf.format.JsonFormat jsonFormatter; | ||
private final XmlFormat xmlFormatter; | ||
private final HtmlFormat htmlFormatter; | ||
|
||
public ProtobufJavaFormatSupport() { | ||
this.jsonFormatter = new com.googlecode.protobuf.format.JsonFormat(); | ||
this.xmlFormatter = new XmlFormat(); | ||
this.htmlFormatter = new HtmlFormat(); | ||
} | ||
|
||
@Override | ||
public MimeType[] supportedMediaTypes() { | ||
return new MimeType[]{PROTOBUF, TEXT_PLAIN, APPLICATION_XML, APPLICATION_JSON}; | ||
} | ||
|
||
@Override | ||
public boolean supportsWriteOnly(@Nullable MimeType mediaType) { | ||
return TEXT_HTML.isCompatibleWith(mediaType); | ||
} | ||
|
||
@Override | ||
public void merge(org.springframework.messaging.Message<?> message, Charset charset, MimeType contentType, | ||
ExtensionRegistry extensionRegistry, Message.Builder builder) | ||
throws IOException, MessageConversionException { | ||
|
||
final CharArrayReader charArrayReader = new CharArrayReader(message.getPayload().toString().toCharArray()); | ||
|
||
if (contentType.isCompatibleWith(APPLICATION_JSON)) { | ||
this.jsonFormatter.merge(charArrayReader, extensionRegistry, builder); | ||
} else if (contentType.isCompatibleWith(APPLICATION_XML)) { | ||
this.xmlFormatter.merge(charArrayReader, extensionRegistry, builder); | ||
} else { | ||
throw new MessageConversionException( | ||
"protobuf-java-format does not support parsing " + contentType); | ||
} | ||
} | ||
|
||
@Override | ||
public void print(Message message, OutputStream output, MimeType contentType, Charset charset) | ||
throws IOException, MessageConversionException { | ||
|
||
if (contentType.isCompatibleWith(APPLICATION_JSON)) { | ||
this.jsonFormatter.print(message, output, charset); | ||
} else if (contentType.isCompatibleWith(APPLICATION_XML)) { | ||
this.xmlFormatter.print(message, output, charset); | ||
} else if (contentType.isCompatibleWith(TEXT_HTML)) { | ||
this.htmlFormatter.print(message, output, charset); | ||
} else { | ||
throw new MessageConversionException( | ||
"protobuf-java-format does not support printing " + contentType); | ||
} | ||
} | ||
} | ||
|
||
|
||
/** | ||
* {@link ProtobufFormatSupport} implementation used when | ||
* {@code com.google.protobuf.util.JsonFormat} is available. | ||
*/ | ||
static class ProtobufJavaUtilSupport implements ProtobufFormatSupport { | ||
|
||
private final JsonFormat.Parser parser; | ||
|
||
private final JsonFormat.Printer printer; | ||
|
||
public ProtobufJavaUtilSupport(@Nullable JsonFormat.Parser parser, @Nullable JsonFormat.Printer printer) { | ||
this.parser = (parser != null ? parser : JsonFormat.parser()); | ||
this.printer = (printer != null ? printer : JsonFormat.printer()); | ||
} | ||
|
||
@Override | ||
public MimeType[] supportedMediaTypes() { | ||
return new MimeType[]{PROTOBUF, TEXT_PLAIN, APPLICATION_JSON}; | ||
} | ||
|
||
@Override | ||
public boolean supportsWriteOnly(@Nullable MimeType mimeType) { | ||
return false; | ||
} | ||
|
||
@Override | ||
public void merge(org.springframework.messaging.Message<?> message, Charset charset, MimeType contentType, | ||
ExtensionRegistry extensionRegistry, Message.Builder builder) | ||
throws IOException, MessageConversionException { | ||
|
||
if (contentType.isCompatibleWith(APPLICATION_JSON)) { | ||
this.parser.merge(message.getPayload().toString(), builder); | ||
} else { | ||
throw new MessageConversionException( | ||
"protobuf-java-util does not support parsing " + contentType); | ||
} | ||
} | ||
|
||
@Override | ||
public void print(Message message, OutputStream output, MimeType contentType, Charset charset) | ||
throws IOException, MessageConversionException { | ||
|
||
if (contentType.isCompatibleWith(APPLICATION_JSON)) { | ||
OutputStreamWriter writer = new OutputStreamWriter(output, charset); | ||
this.printer.appendTo(message, writer); | ||
writer.flush(); | ||
} else { | ||
throw new MessageConversionException( | ||
"protobuf-java-util does not support printing " + contentType); | ||
} | ||
} | ||
} | ||
|
||
|
||
} |
Oops, something went wrong.