001/* 002 * Copyright 2002-2016 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.InputStreamReader; 021import java.io.OutputStreamWriter; 022import java.lang.reflect.Method; 023import java.nio.charset.Charset; 024import java.util.concurrent.ConcurrentHashMap; 025 026import com.google.protobuf.ExtensionRegistry; 027import com.google.protobuf.Message; 028import com.google.protobuf.TextFormat; 029import com.googlecode.protobuf.format.HtmlFormat; 030import com.googlecode.protobuf.format.JsonFormat; 031import com.googlecode.protobuf.format.ProtobufFormatter; 032import com.googlecode.protobuf.format.XmlFormat; 033 034import org.springframework.http.HttpInputMessage; 035import org.springframework.http.HttpOutputMessage; 036import org.springframework.http.MediaType; 037import org.springframework.http.converter.AbstractHttpMessageConverter; 038import org.springframework.http.converter.HttpMessageNotReadableException; 039import org.springframework.http.converter.HttpMessageNotWritableException; 040import org.springframework.util.FileCopyUtils; 041 042/** 043 * An {@code HttpMessageConverter} that reads and writes {@link com.google.protobuf.Message}s 044 * using <a href="https://developers.google.com/protocol-buffers/">Google Protocol Buffers</a>. 045 * 046 * <p>By default, it supports {@code "application/x-protobuf"}, {@code "text/plain"}, 047 * {@code "application/json"}, {@code "application/xml"}, while also writing {@code "text/html"}. 048 * 049 * <p>To generate {@code Message} Java classes, you need to install the {@code protoc} binary. 050 * 051 * <p>Requires Protobuf 2.6 and Protobuf Java Format 1.4, as of Spring 4.3. 052 * 053 * @author Alex Antonov 054 * @author Brian Clozel 055 * @author Juergen Hoeller 056 * @since 4.1 057 */ 058public class ProtobufHttpMessageConverter extends AbstractHttpMessageConverter<Message> { 059 060 public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); 061 062 public static final MediaType PROTOBUF = new MediaType("application", "x-protobuf", DEFAULT_CHARSET); 063 064 public static final String X_PROTOBUF_SCHEMA_HEADER = "X-Protobuf-Schema"; 065 066 public static final String X_PROTOBUF_MESSAGE_HEADER = "X-Protobuf-Message"; 067 068 069 private static final ProtobufFormatter JSON_FORMAT = new JsonFormat(); 070 071 private static final ProtobufFormatter XML_FORMAT = new XmlFormat(); 072 073 private static final ProtobufFormatter HTML_FORMAT = new HtmlFormat(); 074 075 076 private static final ConcurrentHashMap<Class<?>, Method> methodCache = new ConcurrentHashMap<Class<?>, Method>(); 077 078 private final ExtensionRegistry extensionRegistry = ExtensionRegistry.newInstance(); 079 080 081 /** 082 * Construct a new instance. 083 */ 084 public ProtobufHttpMessageConverter() { 085 this(null); 086 } 087 088 /** 089 * Construct a new instance with an {@link ExtensionRegistryInitializer} 090 * that allows the registration of message extensions. 091 */ 092 public ProtobufHttpMessageConverter(ExtensionRegistryInitializer registryInitializer) { 093 super(PROTOBUF, MediaType.TEXT_PLAIN, MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML); 094 if (registryInitializer != null) { 095 registryInitializer.initializeExtensionRegistry(this.extensionRegistry); 096 } 097 } 098 099 100 @Override 101 protected boolean supports(Class<?> clazz) { 102 return Message.class.isAssignableFrom(clazz); 103 } 104 105 @Override 106 protected MediaType getDefaultContentType(Message message) { 107 return PROTOBUF; 108 } 109 110 @Override 111 protected Message readInternal(Class<? extends Message> clazz, HttpInputMessage inputMessage) 112 throws IOException, HttpMessageNotReadableException { 113 114 MediaType contentType = inputMessage.getHeaders().getContentType(); 115 if (contentType == null) { 116 contentType = PROTOBUF; 117 } 118 Charset charset = contentType.getCharset(); 119 if (charset == null) { 120 charset = DEFAULT_CHARSET; 121 } 122 123 try { 124 Message.Builder builder = getMessageBuilder(clazz); 125 if (MediaType.TEXT_PLAIN.isCompatibleWith(contentType)) { 126 InputStreamReader reader = new InputStreamReader(inputMessage.getBody(), charset); 127 TextFormat.merge(reader, this.extensionRegistry, builder); 128 } 129 else if (MediaType.APPLICATION_JSON.isCompatibleWith(contentType)) { 130 JSON_FORMAT.merge(inputMessage.getBody(), charset, this.extensionRegistry, builder); 131 } 132 else if (MediaType.APPLICATION_XML.isCompatibleWith(contentType)) { 133 XML_FORMAT.merge(inputMessage.getBody(), charset, this.extensionRegistry, builder); 134 } 135 else { 136 builder.mergeFrom(inputMessage.getBody(), this.extensionRegistry); 137 } 138 return builder.build(); 139 } 140 catch (Exception ex) { 141 throw new HttpMessageNotReadableException("Could not read Protobuf message: " + ex.getMessage(), ex); 142 } 143 } 144 145 /** 146 * This method overrides the parent implementation, since this HttpMessageConverter 147 * can also produce {@code MediaType.HTML "text/html"} ContentType. 148 */ 149 @Override 150 protected boolean canWrite(MediaType mediaType) { 151 return (super.canWrite(mediaType) || MediaType.TEXT_HTML.isCompatibleWith(mediaType)); 152 } 153 154 @Override 155 protected void writeInternal(Message message, HttpOutputMessage outputMessage) 156 throws IOException, HttpMessageNotWritableException { 157 158 MediaType contentType = outputMessage.getHeaders().getContentType(); 159 if (contentType == null) { 160 contentType = getDefaultContentType(message); 161 } 162 Charset charset = contentType.getCharset(); 163 if (charset == null) { 164 charset = DEFAULT_CHARSET; 165 } 166 167 if (MediaType.TEXT_PLAIN.isCompatibleWith(contentType)) { 168 OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputMessage.getBody(), charset); 169 TextFormat.print(message, outputStreamWriter); 170 outputStreamWriter.flush(); 171 } 172 else if (MediaType.APPLICATION_JSON.isCompatibleWith(contentType)) { 173 JSON_FORMAT.print(message, outputMessage.getBody(), charset); 174 } 175 else if (MediaType.APPLICATION_XML.isCompatibleWith(contentType)) { 176 XML_FORMAT.print(message, outputMessage.getBody(), charset); 177 } 178 else if (MediaType.TEXT_HTML.isCompatibleWith(contentType)) { 179 HTML_FORMAT.print(message, outputMessage.getBody(), charset); 180 } 181 else if (PROTOBUF.isCompatibleWith(contentType)) { 182 setProtoHeader(outputMessage, message); 183 FileCopyUtils.copy(message.toByteArray(), outputMessage.getBody()); 184 } 185 } 186 187 /** 188 * Set the "X-Protobuf-*" HTTP headers when responding with a message of 189 * content type "application/x-protobuf" 190 * <p><b>Note:</b> <code>outputMessage.getBody()</code> should not have been called 191 * before because it writes HTTP headers (making them read only).</p> 192 */ 193 private void setProtoHeader(HttpOutputMessage response, Message message) { 194 response.getHeaders().set(X_PROTOBUF_SCHEMA_HEADER, message.getDescriptorForType().getFile().getName()); 195 response.getHeaders().set(X_PROTOBUF_MESSAGE_HEADER, message.getDescriptorForType().getFullName()); 196 } 197 198 199 /** 200 * Create a new {@code Message.Builder} instance for the given class. 201 * <p>This method uses a ConcurrentHashMap for caching method lookups. 202 */ 203 private static Message.Builder getMessageBuilder(Class<? extends Message> clazz) throws Exception { 204 Method method = methodCache.get(clazz); 205 if (method == null) { 206 method = clazz.getMethod("newBuilder"); 207 methodCache.put(clazz, method); 208 } 209 return (Message.Builder) method.invoke(clazz); 210 } 211 212}