001/* 002 * Copyright 2002-2020 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.messaging.converter; 018 019import java.io.ByteArrayOutputStream; 020import java.io.IOException; 021import java.io.OutputStream; 022import java.io.OutputStreamWriter; 023import java.lang.reflect.Method; 024import java.nio.charset.Charset; 025import java.nio.charset.StandardCharsets; 026import java.util.Map; 027 028import com.google.protobuf.ExtensionRegistry; 029import com.google.protobuf.Message; 030import com.google.protobuf.util.JsonFormat; 031 032import org.springframework.lang.Nullable; 033import org.springframework.messaging.MessageHeaders; 034import org.springframework.util.ClassUtils; 035import org.springframework.util.ConcurrentReferenceHashMap; 036import org.springframework.util.MimeType; 037 038import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON; 039import static org.springframework.util.MimeTypeUtils.TEXT_PLAIN; 040 041/** 042 * An {@code MessageConverter} that reads and writes 043 * {@link com.google.protobuf.Message com.google.protobuf.Messages} using 044 * <a href="https://developers.google.com/protocol-buffers/">Google Protocol Buffers</a>. 045 * 046 * <p>To generate {@code Message} Java classes, you need to install the {@code protoc} binary. 047 * 048 * <p>This converter supports by default {@code "application/x-protobuf"} with the official 049 * {@code "com.google.protobuf:protobuf-java"} library. 050 * 051 * <p>{@code "application/json"} can be supported with the official 052 * {@code "com.google.protobuf:protobuf-java-util"} 3.x, with 3.3 or higher recommended. 053 * 054 * @author Parviz Rozikov 055 * @author Rossen Stoyanchev 056 * @since 5.2.2 057 */ 058public class ProtobufMessageConverter extends AbstractMessageConverter { 059 060 /** 061 * The default charset used by the converter. 062 */ 063 public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; 064 065 /** 066 * The mime-type for protobuf {@code application/x-protobuf}. 067 */ 068 public static final MimeType PROTOBUF = new MimeType("application", "x-protobuf", DEFAULT_CHARSET); 069 070 071 private static final Map<Class<?>, Method> methodCache = new ConcurrentReferenceHashMap<>(); 072 073 final ExtensionRegistry extensionRegistry; 074 075 @Nullable 076 private final ProtobufFormatSupport protobufFormatSupport; 077 078 079 /** 080 * Constructor with a default instance of {@link ExtensionRegistry}. 081 */ 082 public ProtobufMessageConverter() { 083 this(null, null); 084 } 085 086 /** 087 * Constructor with a given {@code ExtensionRegistry}. 088 */ 089 public ProtobufMessageConverter(ExtensionRegistry extensionRegistry) { 090 this(null, extensionRegistry); 091 } 092 093 ProtobufMessageConverter(@Nullable ProtobufFormatSupport formatSupport, 094 @Nullable ExtensionRegistry extensionRegistry) { 095 096 super(PROTOBUF, TEXT_PLAIN); 097 098 if (formatSupport != null) { 099 this.protobufFormatSupport = formatSupport; 100 } 101 else if (ClassUtils.isPresent("com.google.protobuf.util.JsonFormat", getClass().getClassLoader())) { 102 this.protobufFormatSupport = new ProtobufJavaUtilSupport(null, null); 103 } 104 else { 105 this.protobufFormatSupport = null; 106 } 107 108 if (this.protobufFormatSupport != null) { 109 addSupportedMimeTypes(this.protobufFormatSupport.supportedMediaTypes()); 110 } 111 112 this.extensionRegistry = (extensionRegistry == null ? ExtensionRegistry.newInstance() : extensionRegistry); 113 } 114 115 116 @Override 117 protected boolean supports(Class<?> clazz) { 118 return Message.class.isAssignableFrom(clazz); 119 } 120 121 @Override 122 protected boolean canConvertTo(Object payload, @Nullable MessageHeaders headers) { 123 MimeType contentType = getMimeType(headers); 124 return (super.canConvertTo(payload, headers) || 125 this.protobufFormatSupport != null && this.protobufFormatSupport.supportsWriteOnly(contentType)); 126 } 127 128 @Override 129 protected Object convertFromInternal(org.springframework.messaging.Message<?> message, 130 Class<?> targetClass, @Nullable Object conversionHint) { 131 132 MimeType contentType = getMimeType(message.getHeaders()); 133 final Object payload = message.getPayload(); 134 135 if (contentType == null) { 136 contentType = PROTOBUF; 137 } 138 139 Charset charset = contentType.getCharset(); 140 if (charset == null) { 141 charset = DEFAULT_CHARSET; 142 } 143 144 Message.Builder builder = getMessageBuilder(targetClass); 145 try { 146 if (PROTOBUF.isCompatibleWith(contentType)) { 147 builder.mergeFrom((byte[]) payload, this.extensionRegistry); 148 } 149 else if (this.protobufFormatSupport != null) { 150 this.protobufFormatSupport.merge(message, charset, contentType, this.extensionRegistry, builder); 151 } 152 } 153 catch (IOException ex) { 154 throw new MessageConversionException(message, "Could not read proto message" + ex.getMessage(), ex); 155 } 156 157 return builder.build(); 158 } 159 160 161 @Override 162 protected Object convertToInternal( 163 Object payload, @Nullable MessageHeaders headers, @Nullable Object conversionHint) { 164 165 final Message message = (Message) payload; 166 167 MimeType contentType = getMimeType(headers); 168 if (contentType == null) { 169 contentType = PROTOBUF; 170 } 171 172 Charset charset = contentType.getCharset(); 173 if (charset == null) { 174 charset = DEFAULT_CHARSET; 175 } 176 177 try { 178 if (PROTOBUF.isCompatibleWith(contentType)) { 179 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); 180 message.writeTo(byteArrayOutputStream); 181 payload = byteArrayOutputStream.toByteArray(); 182 } 183 else if (this.protobufFormatSupport != null) { 184 ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 185 this.protobufFormatSupport.print(message, outputStream, contentType, charset); 186 payload = outputStream.toString(charset.name()); 187 } 188 } 189 catch (IOException ex) { 190 throw new MessageConversionException("Failed to print Protobuf message: " + ex.getMessage(), ex); 191 192 } 193 return payload; 194 } 195 196 /** 197 * Create a new {@code Message.Builder} instance for the given class. 198 * <p>This method uses a ConcurrentReferenceHashMap for caching method lookups. 199 */ 200 private Message.Builder getMessageBuilder(Class<?> clazz) { 201 try { 202 Method method = methodCache.get(clazz); 203 if (method == null) { 204 method = clazz.getMethod("newBuilder"); 205 methodCache.put(clazz, method); 206 } 207 return (Message.Builder) method.invoke(clazz); 208 } 209 catch (Exception ex) { 210 throw new MessageConversionException( 211 "Invalid Protobuf Message type: no invocable newBuilder() method on " + clazz, ex); 212 } 213 } 214 215 216 /** 217 * Protobuf format support. 218 */ 219 interface ProtobufFormatSupport { 220 221 MimeType[] supportedMediaTypes(); 222 223 boolean supportsWriteOnly(@Nullable MimeType mediaType); 224 225 void merge(org.springframework.messaging.Message<?> message, 226 Charset charset, MimeType contentType, ExtensionRegistry extensionRegistry, 227 Message.Builder builder) throws IOException, MessageConversionException; 228 229 void print(Message message, OutputStream output, MimeType contentType, Charset charset) 230 throws IOException, MessageConversionException; 231 } 232 233 234 /** 235 * {@link ProtobufFormatSupport} implementation used when 236 * {@code com.google.protobuf.util.JsonFormat} is available. 237 */ 238 static class ProtobufJavaUtilSupport implements ProtobufFormatSupport { 239 240 private final JsonFormat.Parser parser; 241 242 private final JsonFormat.Printer printer; 243 244 public ProtobufJavaUtilSupport(@Nullable JsonFormat.Parser parser, @Nullable JsonFormat.Printer printer) { 245 this.parser = (parser != null ? parser : JsonFormat.parser()); 246 this.printer = (printer != null ? printer : JsonFormat.printer()); 247 } 248 249 @Override 250 public MimeType[] supportedMediaTypes() { 251 return new MimeType[]{APPLICATION_JSON}; 252 } 253 254 @Override 255 public boolean supportsWriteOnly(@Nullable MimeType mimeType) { 256 return false; 257 } 258 259 @Override 260 public void merge(org.springframework.messaging.Message<?> message, Charset charset, 261 MimeType contentType, ExtensionRegistry extensionRegistry, Message.Builder builder) 262 throws IOException, MessageConversionException { 263 264 if (contentType.isCompatibleWith(APPLICATION_JSON)) { 265 this.parser.merge(message.getPayload().toString(), builder); 266 } 267 else { 268 throw new MessageConversionException( 269 "protobuf-java-util does not support parsing " + contentType); 270 } 271 } 272 273 @Override 274 public void print(Message message, OutputStream output, MimeType contentType, Charset charset) 275 throws IOException, MessageConversionException { 276 277 if (contentType.isCompatibleWith(APPLICATION_JSON)) { 278 OutputStreamWriter writer = new OutputStreamWriter(output, charset); 279 this.printer.appendTo(message, writer); 280 writer.flush(); 281 } 282 else { 283 throw new MessageConversionException( 284 "protobuf-java-util does not support printing " + contentType); 285 } 286 } 287 } 288 289}