xref: /aosp_15_r20/external/gson/proto/src/main/java/com/google/gson/protobuf/ProtoTypeAdapter.java (revision a8de600362638ea28fd6cb3225451dc706d269bb)
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