1 /* 2 * Copyright (C) 2010 Google Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.google.gson.protobuf; 18 19 import static java.util.Objects.requireNonNull; 20 21 import com.google.common.base.CaseFormat; 22 import com.google.common.collect.MapMaker; 23 import com.google.gson.JsonArray; 24 import com.google.gson.JsonDeserializationContext; 25 import com.google.gson.JsonDeserializer; 26 import com.google.gson.JsonElement; 27 import com.google.gson.JsonObject; 28 import com.google.gson.JsonParseException; 29 import com.google.gson.JsonSerializationContext; 30 import com.google.gson.JsonSerializer; 31 import com.google.protobuf.DescriptorProtos.EnumValueOptions; 32 import com.google.protobuf.DescriptorProtos.FieldOptions; 33 import com.google.protobuf.Descriptors.Descriptor; 34 import com.google.protobuf.Descriptors.EnumDescriptor; 35 import com.google.protobuf.Descriptors.EnumValueDescriptor; 36 import com.google.protobuf.Descriptors.FieldDescriptor; 37 import com.google.protobuf.DynamicMessage; 38 import com.google.protobuf.Extension; 39 import com.google.protobuf.Message; 40 import java.lang.reflect.Field; 41 import java.lang.reflect.InvocationTargetException; 42 import java.lang.reflect.Method; 43 import java.lang.reflect.Type; 44 import java.util.ArrayList; 45 import java.util.Collection; 46 import java.util.HashSet; 47 import java.util.Map; 48 import java.util.Set; 49 import java.util.concurrent.ConcurrentMap; 50 51 /** 52 * GSON type adapter for protocol buffers that knows how to serialize enums either by using their 53 * values or their names, and also supports custom proto field names. 54 * <p> 55 * You can specify which case representation is used for the proto fields when writing/reading the 56 * JSON payload by calling {@link Builder#setFieldNameSerializationFormat(CaseFormat, CaseFormat)}. 57 * <p> 58 * An example of default serialization/deserialization using custom proto field names is shown 59 * below: 60 * 61 * <pre> 62 * message MyMessage { 63 * // Will be serialized as 'osBuildID' instead of the default 'osBuildId'. 64 * string os_build_id = 1 [(serialized_name) = "osBuildID"]; 65 * } 66 * </pre> 67 * 68 * @author Inderjeet Singh 69 * @author Emmanuel Cron 70 * @author Stanley Wang 71 */ 72 public class ProtoTypeAdapter 73 implements JsonSerializer<Message>, JsonDeserializer<Message> { 74 /** 75 * Determines how enum <u>values</u> should be serialized. 76 */ 77 public enum EnumSerialization { 78 /** 79 * Serializes and deserializes enum values using their <b>number</b>. When this is used, custom 80 * value names set on enums are ignored. 81 */ 82 NUMBER, 83 /** Serializes and deserializes enum values using their <b>name</b>. */ 84 NAME; 85 } 86 87 /** 88 * Builder for {@link ProtoTypeAdapter}s. 89 */ 90 public static class Builder { 91 private final Set<Extension<FieldOptions, String>> serializedNameExtensions; 92 private final Set<Extension<EnumValueOptions, String>> serializedEnumValueExtensions; 93 private EnumSerialization enumSerialization; 94 private CaseFormat protoFormat; 95 private CaseFormat jsonFormat; 96 Builder(EnumSerialization enumSerialization, CaseFormat fromFieldNameFormat, CaseFormat toFieldNameFormat)97 private Builder(EnumSerialization enumSerialization, CaseFormat fromFieldNameFormat, 98 CaseFormat toFieldNameFormat) { 99 this.serializedNameExtensions = new HashSet<>(); 100 this.serializedEnumValueExtensions = new HashSet<>(); 101 setEnumSerialization(enumSerialization); 102 setFieldNameSerializationFormat(fromFieldNameFormat, toFieldNameFormat); 103 } 104 setEnumSerialization(EnumSerialization enumSerialization)105 public Builder setEnumSerialization(EnumSerialization enumSerialization) { 106 this.enumSerialization = requireNonNull(enumSerialization); 107 return this; 108 } 109 110 /** 111 * Sets the field names serialization format. The first parameter defines how to read the format 112 * of the proto field names you are converting to JSON. The second parameter defines which 113 * format to use when serializing them. 114 * <p> 115 * For example, if you use the following parameters: {@link CaseFormat#LOWER_UNDERSCORE}, 116 * {@link CaseFormat#LOWER_CAMEL}, the following conversion will occur: 117 * 118 * <pre>{@code 119 * PROTO <-> JSON 120 * my_field myField 121 * foo foo 122 * n__id_ct nIdCt 123 * }</pre> 124 */ setFieldNameSerializationFormat(CaseFormat fromFieldNameFormat, CaseFormat toFieldNameFormat)125 public Builder setFieldNameSerializationFormat(CaseFormat fromFieldNameFormat, 126 CaseFormat toFieldNameFormat) { 127 this.protoFormat = fromFieldNameFormat; 128 this.jsonFormat = toFieldNameFormat; 129 return this; 130 } 131 132 /** 133 * Adds a field proto annotation that, when set, overrides the default field name 134 * serialization/deserialization. For example, if you add the '{@code serialized_name}' 135 * annotation and you define a field in your proto like the one below: 136 * 137 * <pre> 138 * string client_app_id = 1 [(serialized_name) = "appId"]; 139 * </pre> 140 * 141 * ...the adapter will serialize the field using '{@code appId}' instead of the default ' 142 * {@code clientAppId}'. This lets you customize the name serialization of any proto field. 143 */ addSerializedNameExtension( Extension<FieldOptions, String> serializedNameExtension)144 public Builder addSerializedNameExtension( 145 Extension<FieldOptions, String> serializedNameExtension) { 146 serializedNameExtensions.add(requireNonNull(serializedNameExtension)); 147 return this; 148 } 149 150 /** 151 * Adds an enum value proto annotation that, when set, overrides the default <b>enum</b> value 152 * serialization/deserialization of this adapter. For example, if you add the ' 153 * {@code serialized_value}' annotation and you define an enum in your proto like the one below: 154 * 155 * <pre> 156 * enum MyEnum { 157 * UNKNOWN = 0; 158 * CLIENT_APP_ID = 1 [(serialized_value) = "APP_ID"]; 159 * TWO = 2 [(serialized_value) = "2"]; 160 * } 161 * </pre> 162 * 163 * ...the adapter will serialize the value {@code CLIENT_APP_ID} as "{@code APP_ID}" and the 164 * value {@code TWO} as "{@code 2}". This works for both serialization and deserialization. 165 * <p> 166 * Note that you need to set the enum serialization of this adapter to 167 * {@link EnumSerialization#NAME}, otherwise these annotations will be ignored. 168 */ addSerializedEnumValueExtension( Extension<EnumValueOptions, String> serializedEnumValueExtension)169 public Builder addSerializedEnumValueExtension( 170 Extension<EnumValueOptions, String> serializedEnumValueExtension) { 171 serializedEnumValueExtensions.add(requireNonNull(serializedEnumValueExtension)); 172 return this; 173 } 174 build()175 public ProtoTypeAdapter build() { 176 return new ProtoTypeAdapter(enumSerialization, protoFormat, jsonFormat, 177 serializedNameExtensions, serializedEnumValueExtensions); 178 } 179 } 180 181 /** 182 * Creates a new {@link ProtoTypeAdapter} builder, defaulting enum serialization to 183 * {@link EnumSerialization#NAME} and converting field serialization from 184 * {@link CaseFormat#LOWER_UNDERSCORE} to {@link CaseFormat#LOWER_CAMEL}. 185 */ newBuilder()186 public static Builder newBuilder() { 187 return new Builder(EnumSerialization.NAME, CaseFormat.LOWER_UNDERSCORE, CaseFormat.LOWER_CAMEL); 188 } 189 190 private static final com.google.protobuf.Descriptors.FieldDescriptor.Type ENUM_TYPE = 191 com.google.protobuf.Descriptors.FieldDescriptor.Type.ENUM; 192 193 private static final ConcurrentMap<String, ConcurrentMap<Class<?>, Method>> mapOfMapOfMethods = 194 new MapMaker().makeMap(); 195 196 private final EnumSerialization enumSerialization; 197 private final CaseFormat protoFormat; 198 private final CaseFormat jsonFormat; 199 private final Set<Extension<FieldOptions, String>> serializedNameExtensions; 200 private final Set<Extension<EnumValueOptions, String>> serializedEnumValueExtensions; 201 ProtoTypeAdapter(EnumSerialization enumSerialization, CaseFormat protoFormat, CaseFormat jsonFormat, Set<Extension<FieldOptions, String>> serializedNameExtensions, Set<Extension<EnumValueOptions, String>> serializedEnumValueExtensions)202 private ProtoTypeAdapter(EnumSerialization enumSerialization, 203 CaseFormat protoFormat, 204 CaseFormat jsonFormat, 205 Set<Extension<FieldOptions, String>> serializedNameExtensions, 206 Set<Extension<EnumValueOptions, String>> serializedEnumValueExtensions) { 207 this.enumSerialization = enumSerialization; 208 this.protoFormat = protoFormat; 209 this.jsonFormat = jsonFormat; 210 this.serializedNameExtensions = serializedNameExtensions; 211 this.serializedEnumValueExtensions = serializedEnumValueExtensions; 212 } 213 214 @Override serialize(Message src, Type typeOfSrc, JsonSerializationContext context)215 public JsonElement serialize(Message src, Type typeOfSrc, 216 JsonSerializationContext context) { 217 JsonObject ret = new JsonObject(); 218 final Map<FieldDescriptor, Object> fields = src.getAllFields(); 219 220 for (Map.Entry<FieldDescriptor, Object> fieldPair : fields.entrySet()) { 221 final FieldDescriptor desc = fieldPair.getKey(); 222 String name = getCustSerializedName(desc.getOptions(), desc.getName()); 223 224 if (desc.getType() == ENUM_TYPE) { 225 // Enum collections are also returned as ENUM_TYPE 226 if (fieldPair.getValue() instanceof Collection) { 227 // Build the array to avoid infinite loop 228 JsonArray array = new JsonArray(); 229 @SuppressWarnings("unchecked") 230 Collection<EnumValueDescriptor> enumDescs = 231 (Collection<EnumValueDescriptor>) fieldPair.getValue(); 232 for (EnumValueDescriptor enumDesc : enumDescs) { 233 array.add(context.serialize(getEnumValue(enumDesc))); 234 ret.add(name, array); 235 } 236 } else { 237 EnumValueDescriptor enumDesc = ((EnumValueDescriptor) fieldPair.getValue()); 238 ret.add(name, context.serialize(getEnumValue(enumDesc))); 239 } 240 } else { 241 ret.add(name, context.serialize(fieldPair.getValue())); 242 } 243 } 244 return ret; 245 } 246 247 @Override deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)248 public Message deserialize(JsonElement json, Type typeOfT, 249 JsonDeserializationContext context) throws JsonParseException { 250 try { 251 JsonObject jsonObject = json.getAsJsonObject(); 252 @SuppressWarnings("unchecked") 253 Class<? extends Message> protoClass = (Class<? extends Message>) typeOfT; 254 255 if (DynamicMessage.class.isAssignableFrom(protoClass)) { 256 throw new IllegalStateException("only generated messages are supported"); 257 } 258 259 try { 260 // Invoke the ProtoClass.newBuilder() method 261 Message.Builder protoBuilder = 262 (Message.Builder) getCachedMethod(protoClass, "newBuilder").invoke(null); 263 264 Message defaultInstance = 265 (Message) getCachedMethod(protoClass, "getDefaultInstance").invoke(null); 266 267 Descriptor protoDescriptor = 268 (Descriptor) getCachedMethod(protoClass, "getDescriptor").invoke(null); 269 // Call setters on all of the available fields 270 for (FieldDescriptor fieldDescriptor : protoDescriptor.getFields()) { 271 String jsonFieldName = 272 getCustSerializedName(fieldDescriptor.getOptions(), fieldDescriptor.getName()); 273 274 JsonElement jsonElement = jsonObject.get(jsonFieldName); 275 if (jsonElement != null && !jsonElement.isJsonNull()) { 276 // Do not reuse jsonFieldName here, it might have a custom value 277 Object fieldValue; 278 if (fieldDescriptor.getType() == ENUM_TYPE) { 279 if (jsonElement.isJsonArray()) { 280 // Handling array 281 Collection<EnumValueDescriptor> enumCollection = 282 new ArrayList<>(jsonElement.getAsJsonArray().size()); 283 for (JsonElement element : jsonElement.getAsJsonArray()) { 284 enumCollection.add( 285 findValueByNameAndExtension(fieldDescriptor.getEnumType(), element)); 286 } 287 fieldValue = enumCollection; 288 } else { 289 // No array, just a plain value 290 fieldValue = 291 findValueByNameAndExtension(fieldDescriptor.getEnumType(), jsonElement); 292 } 293 protoBuilder.setField(fieldDescriptor, fieldValue); 294 } else if (fieldDescriptor.isRepeated()) { 295 // If the type is an array, then we have to grab the type from the class. 296 // protobuf java field names are always lower camel case 297 String protoArrayFieldName = 298 protoFormat.to(CaseFormat.LOWER_CAMEL, fieldDescriptor.getName()) + "_"; 299 Field protoArrayField = protoClass.getDeclaredField(protoArrayFieldName); 300 Type protoArrayFieldType = protoArrayField.getGenericType(); 301 fieldValue = context.deserialize(jsonElement, protoArrayFieldType); 302 protoBuilder.setField(fieldDescriptor, fieldValue); 303 } else { 304 Object field = defaultInstance.getField(fieldDescriptor); 305 fieldValue = context.deserialize(jsonElement, field.getClass()); 306 protoBuilder.setField(fieldDescriptor, fieldValue); 307 } 308 } 309 } 310 return protoBuilder.build(); 311 } catch (SecurityException e) { 312 throw new JsonParseException(e); 313 } catch (NoSuchMethodException e) { 314 throw new JsonParseException(e); 315 } catch (IllegalArgumentException e) { 316 throw new JsonParseException(e); 317 } catch (IllegalAccessException e) { 318 throw new JsonParseException(e); 319 } catch (InvocationTargetException e) { 320 throw new JsonParseException(e); 321 } 322 } catch (Exception e) { 323 throw new JsonParseException("Error while parsing proto", e); 324 } 325 } 326 327 /** 328 * Retrieves the custom field name from the given options, and if not found, returns the specified 329 * default name. 330 */ getCustSerializedName(FieldOptions options, String defaultName)331 private String getCustSerializedName(FieldOptions options, String defaultName) { 332 for (Extension<FieldOptions, String> extension : serializedNameExtensions) { 333 if (options.hasExtension(extension)) { 334 return options.getExtension(extension); 335 } 336 } 337 return protoFormat.to(jsonFormat, defaultName); 338 } 339 340 /** 341 * Retrieves the custom enum value name from the given options, and if not found, returns the 342 * specified default value. 343 */ getCustSerializedEnumValue(EnumValueOptions options, String defaultValue)344 private String getCustSerializedEnumValue(EnumValueOptions options, String defaultValue) { 345 for (Extension<EnumValueOptions, String> extension : serializedEnumValueExtensions) { 346 if (options.hasExtension(extension)) { 347 return options.getExtension(extension); 348 } 349 } 350 return defaultValue; 351 } 352 353 /** 354 * Returns the enum value to use for serialization, depending on the value of 355 * {@link EnumSerialization} that was given to this adapter. 356 */ getEnumValue(EnumValueDescriptor enumDesc)357 private Object getEnumValue(EnumValueDescriptor enumDesc) { 358 if (enumSerialization == EnumSerialization.NAME) { 359 return getCustSerializedEnumValue(enumDesc.getOptions(), enumDesc.getName()); 360 } else { 361 return enumDesc.getNumber(); 362 } 363 } 364 365 /** 366 * Finds an enum value in the given {@link EnumDescriptor} that matches the given JSON element, 367 * either by name if the current adapter is using {@link EnumSerialization#NAME}, otherwise by 368 * number. If matching by name, it uses the extension value if it is defined, otherwise it uses 369 * its default value. 370 * 371 * @throws IllegalArgumentException if a matching name/number was not found 372 */ findValueByNameAndExtension(EnumDescriptor desc, JsonElement jsonElement)373 private EnumValueDescriptor findValueByNameAndExtension(EnumDescriptor desc, 374 JsonElement jsonElement) { 375 if (enumSerialization == EnumSerialization.NAME) { 376 // With enum name 377 for (EnumValueDescriptor enumDesc : desc.getValues()) { 378 String enumValue = getCustSerializedEnumValue(enumDesc.getOptions(), enumDesc.getName()); 379 if (enumValue.equals(jsonElement.getAsString())) { 380 return enumDesc; 381 } 382 } 383 throw new IllegalArgumentException( 384 String.format("Unrecognized enum name: %s", jsonElement.getAsString())); 385 } else { 386 // With enum value 387 EnumValueDescriptor fieldValue = desc.findValueByNumber(jsonElement.getAsInt()); 388 if (fieldValue == null) { 389 throw new IllegalArgumentException( 390 String.format("Unrecognized enum value: %d", jsonElement.getAsInt())); 391 } 392 return fieldValue; 393 } 394 } 395 getCachedMethod(Class<?> clazz, String methodName, Class<?>... methodParamTypes)396 private static Method getCachedMethod(Class<?> clazz, String methodName, 397 Class<?>... methodParamTypes) throws NoSuchMethodException { 398 ConcurrentMap<Class<?>, Method> mapOfMethods = mapOfMapOfMethods.get(methodName); 399 if (mapOfMethods == null) { 400 mapOfMethods = new MapMaker().makeMap(); 401 ConcurrentMap<Class<?>, Method> previous = 402 mapOfMapOfMethods.putIfAbsent(methodName, mapOfMethods); 403 mapOfMethods = previous == null ? mapOfMethods : previous; 404 } 405 406 Method method = mapOfMethods.get(clazz); 407 if (method == null) { 408 method = clazz.getMethod(methodName, methodParamTypes); 409 mapOfMethods.putIfAbsent(clazz, method); 410 // NB: it doesn't matter which method we return in the event of a race. 411 } 412 return method; 413 } 414 415 } 416