417 lines
17 KiB
Java
417 lines
17 KiB
Java
/*
|
|
* Copyright (C) 2010 Google Inc.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package com.google.gson.protobuf;
|
|
|
|
import static com.google.common.base.Preconditions.checkNotNull;
|
|
|
|
import com.google.common.base.CaseFormat;
|
|
import com.google.common.collect.MapMaker;
|
|
import com.google.gson.JsonArray;
|
|
import com.google.gson.JsonDeserializationContext;
|
|
import com.google.gson.JsonDeserializer;
|
|
import com.google.gson.JsonElement;
|
|
import com.google.gson.JsonObject;
|
|
import com.google.gson.JsonParseException;
|
|
import com.google.gson.JsonSerializationContext;
|
|
import com.google.gson.JsonSerializer;
|
|
import com.google.protobuf.DescriptorProtos.EnumValueOptions;
|
|
import com.google.protobuf.DescriptorProtos.FieldOptions;
|
|
import com.google.protobuf.Descriptors.Descriptor;
|
|
import com.google.protobuf.Descriptors.EnumDescriptor;
|
|
import com.google.protobuf.Descriptors.EnumValueDescriptor;
|
|
import com.google.protobuf.Descriptors.FieldDescriptor;
|
|
import com.google.protobuf.DynamicMessage;
|
|
import com.google.protobuf.Extension;
|
|
import com.google.protobuf.Message;
|
|
import java.lang.reflect.Field;
|
|
import java.lang.reflect.InvocationTargetException;
|
|
import java.lang.reflect.Method;
|
|
import java.lang.reflect.Type;
|
|
import java.util.ArrayList;
|
|
import java.util.Collection;
|
|
import java.util.HashSet;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
import java.util.concurrent.ConcurrentMap;
|
|
|
|
/**
|
|
* GSON type adapter for protocol buffers that knows how to serialize enums either by using their
|
|
* values or their names, and also supports custom proto field names.
|
|
* <p>
|
|
* You can specify which case representation is used for the proto fields when writing/reading the
|
|
* JSON payload by calling {@link Builder#setFieldNameSerializationFormat(CaseFormat, CaseFormat)}.
|
|
* <p>
|
|
* An example of default serialization/deserialization using custom proto field names is shown
|
|
* below:
|
|
*
|
|
* <pre>
|
|
* message MyMessage {
|
|
* // Will be serialized as 'osBuildID' instead of the default 'osBuildId'.
|
|
* string os_build_id = 1 [(serialized_name) = "osBuildID"];
|
|
* }
|
|
* </pre>
|
|
* <p>
|
|
*
|
|
* @author Inderjeet Singh
|
|
* @author Emmanuel Cron
|
|
* @author Stanley Wang
|
|
*/
|
|
public class ProtoTypeAdapter
|
|
implements JsonSerializer<Message>, JsonDeserializer<Message> {
|
|
/**
|
|
* Determines how enum <u>values</u> should be serialized.
|
|
*/
|
|
public enum EnumSerialization {
|
|
/**
|
|
* Serializes and deserializes enum values using their <b>number</b>. When this is used, custom
|
|
* value names set on enums are ignored.
|
|
*/
|
|
NUMBER,
|
|
/** Serializes and deserializes enum values using their <b>name</b>. */
|
|
NAME;
|
|
}
|
|
|
|
/**
|
|
* Builder for {@link ProtoTypeAdapter}s.
|
|
*/
|
|
public static class Builder {
|
|
private final Set<Extension<FieldOptions, String>> serializedNameExtensions;
|
|
private final Set<Extension<EnumValueOptions, String>> serializedEnumValueExtensions;
|
|
private EnumSerialization enumSerialization;
|
|
private CaseFormat protoFormat;
|
|
private CaseFormat jsonFormat;
|
|
|
|
private Builder(EnumSerialization enumSerialization, CaseFormat fromFieldNameFormat,
|
|
CaseFormat toFieldNameFormat) {
|
|
this.serializedNameExtensions = new HashSet<>();
|
|
this.serializedEnumValueExtensions = new HashSet<>();
|
|
setEnumSerialization(enumSerialization);
|
|
setFieldNameSerializationFormat(fromFieldNameFormat, toFieldNameFormat);
|
|
}
|
|
|
|
public Builder setEnumSerialization(EnumSerialization enumSerialization) {
|
|
this.enumSerialization = checkNotNull(enumSerialization);
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Sets the field names serialization format. The first parameter defines how to read the format
|
|
* of the proto field names you are converting to JSON. The second parameter defines which
|
|
* format to use when serializing them.
|
|
* <p>
|
|
* For example, if you use the following parameters: {@link CaseFormat#LOWER_UNDERSCORE},
|
|
* {@link CaseFormat#LOWER_CAMEL}, the following conversion will occur:
|
|
*
|
|
* <pre>{@code
|
|
* PROTO <-> JSON
|
|
* my_field myField
|
|
* foo foo
|
|
* n__id_ct nIdCt
|
|
* }</pre>
|
|
*/
|
|
public Builder setFieldNameSerializationFormat(CaseFormat fromFieldNameFormat,
|
|
CaseFormat toFieldNameFormat) {
|
|
this.protoFormat = fromFieldNameFormat;
|
|
this.jsonFormat = toFieldNameFormat;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Adds a field proto annotation that, when set, overrides the default field name
|
|
* serialization/deserialization. For example, if you add the '{@code serialized_name}'
|
|
* annotation and you define a field in your proto like the one below:
|
|
*
|
|
* <pre>
|
|
* string client_app_id = 1 [(serialized_name) = "appId"];
|
|
* </pre>
|
|
*
|
|
* ...the adapter will serialize the field using '{@code appId}' instead of the default '
|
|
* {@code clientAppId}'. This lets you customize the name serialization of any proto field.
|
|
*/
|
|
public Builder addSerializedNameExtension(
|
|
Extension<FieldOptions, String> serializedNameExtension) {
|
|
serializedNameExtensions.add(checkNotNull(serializedNameExtension));
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Adds an enum value proto annotation that, when set, overrides the default <b>enum</b> value
|
|
* serialization/deserialization of this adapter. For example, if you add the '
|
|
* {@code serialized_value}' annotation and you define an enum in your proto like the one below:
|
|
*
|
|
* <pre>
|
|
* enum MyEnum {
|
|
* UNKNOWN = 0;
|
|
* CLIENT_APP_ID = 1 [(serialized_value) = "APP_ID"];
|
|
* TWO = 2 [(serialized_value) = "2"];
|
|
* }
|
|
* </pre>
|
|
*
|
|
* ...the adapter will serialize the value {@code CLIENT_APP_ID} as "{@code APP_ID}" and the
|
|
* value {@code TWO} as "{@code 2}". This works for both serialization and deserialization.
|
|
* <p>
|
|
* Note that you need to set the enum serialization of this adapter to
|
|
* {@link EnumSerialization#NAME}, otherwise these annotations will be ignored.
|
|
*/
|
|
public Builder addSerializedEnumValueExtension(
|
|
Extension<EnumValueOptions, String> serializedEnumValueExtension) {
|
|
serializedEnumValueExtensions.add(checkNotNull(serializedEnumValueExtension));
|
|
return this;
|
|
}
|
|
|
|
public ProtoTypeAdapter build() {
|
|
return new ProtoTypeAdapter(enumSerialization, protoFormat, jsonFormat,
|
|
serializedNameExtensions, serializedEnumValueExtensions);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a new {@link ProtoTypeAdapter} builder, defaulting enum serialization to
|
|
* {@link EnumSerialization#NAME} and converting field serialization from
|
|
* {@link CaseFormat#LOWER_UNDERSCORE} to {@link CaseFormat#LOWER_CAMEL}.
|
|
*/
|
|
public static Builder newBuilder() {
|
|
return new Builder(EnumSerialization.NAME, CaseFormat.LOWER_UNDERSCORE, CaseFormat.LOWER_CAMEL);
|
|
}
|
|
|
|
private static final com.google.protobuf.Descriptors.FieldDescriptor.Type ENUM_TYPE =
|
|
com.google.protobuf.Descriptors.FieldDescriptor.Type.ENUM;
|
|
|
|
private static final ConcurrentMap<String, ConcurrentMap<Class<?>, Method>> mapOfMapOfMethods =
|
|
new MapMaker().makeMap();
|
|
|
|
private final EnumSerialization enumSerialization;
|
|
private final CaseFormat protoFormat;
|
|
private final CaseFormat jsonFormat;
|
|
private final Set<Extension<FieldOptions, String>> serializedNameExtensions;
|
|
private final Set<Extension<EnumValueOptions, String>> serializedEnumValueExtensions;
|
|
|
|
private ProtoTypeAdapter(EnumSerialization enumSerialization,
|
|
CaseFormat protoFormat,
|
|
CaseFormat jsonFormat,
|
|
Set<Extension<FieldOptions, String>> serializedNameExtensions,
|
|
Set<Extension<EnumValueOptions, String>> serializedEnumValueExtensions) {
|
|
this.enumSerialization = enumSerialization;
|
|
this.protoFormat = protoFormat;
|
|
this.jsonFormat = jsonFormat;
|
|
this.serializedNameExtensions = serializedNameExtensions;
|
|
this.serializedEnumValueExtensions = serializedEnumValueExtensions;
|
|
}
|
|
|
|
@Override
|
|
public JsonElement serialize(Message src, Type typeOfSrc,
|
|
JsonSerializationContext context) {
|
|
JsonObject ret = new JsonObject();
|
|
final Map<FieldDescriptor, Object> fields = src.getAllFields();
|
|
|
|
for (Map.Entry<FieldDescriptor, Object> fieldPair : fields.entrySet()) {
|
|
final FieldDescriptor desc = fieldPair.getKey();
|
|
String name = getCustSerializedName(desc.getOptions(), desc.getName());
|
|
|
|
if (desc.getType() == ENUM_TYPE) {
|
|
// Enum collections are also returned as ENUM_TYPE
|
|
if (fieldPair.getValue() instanceof Collection) {
|
|
// Build the array to avoid infinite loop
|
|
JsonArray array = new JsonArray();
|
|
@SuppressWarnings("unchecked")
|
|
Collection<EnumValueDescriptor> enumDescs =
|
|
(Collection<EnumValueDescriptor>) fieldPair.getValue();
|
|
for (EnumValueDescriptor enumDesc : enumDescs) {
|
|
array.add(context.serialize(getEnumValue(enumDesc)));
|
|
ret.add(name, array);
|
|
}
|
|
} else {
|
|
EnumValueDescriptor enumDesc = ((EnumValueDescriptor) fieldPair.getValue());
|
|
ret.add(name, context.serialize(getEnumValue(enumDesc)));
|
|
}
|
|
} else {
|
|
ret.add(name, context.serialize(fieldPair.getValue()));
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
@Override
|
|
public Message deserialize(JsonElement json, Type typeOfT,
|
|
JsonDeserializationContext context) throws JsonParseException {
|
|
try {
|
|
JsonObject jsonObject = json.getAsJsonObject();
|
|
@SuppressWarnings("unchecked")
|
|
Class<? extends Message> protoClass = (Class<? extends Message>) typeOfT;
|
|
|
|
if (DynamicMessage.class.isAssignableFrom(protoClass)) {
|
|
throw new IllegalStateException("only generated messages are supported");
|
|
}
|
|
|
|
try {
|
|
// Invoke the ProtoClass.newBuilder() method
|
|
Message.Builder protoBuilder =
|
|
(Message.Builder) getCachedMethod(protoClass, "newBuilder").invoke(null);
|
|
|
|
Message defaultInstance =
|
|
(Message) getCachedMethod(protoClass, "getDefaultInstance").invoke(null);
|
|
|
|
Descriptor protoDescriptor =
|
|
(Descriptor) getCachedMethod(protoClass, "getDescriptor").invoke(null);
|
|
// Call setters on all of the available fields
|
|
for (FieldDescriptor fieldDescriptor : protoDescriptor.getFields()) {
|
|
String jsonFieldName =
|
|
getCustSerializedName(fieldDescriptor.getOptions(), fieldDescriptor.getName());
|
|
|
|
JsonElement jsonElement = jsonObject.get(jsonFieldName);
|
|
if (jsonElement != null && !jsonElement.isJsonNull()) {
|
|
// Do not reuse jsonFieldName here, it might have a custom value
|
|
Object fieldValue;
|
|
if (fieldDescriptor.getType() == ENUM_TYPE) {
|
|
if (jsonElement.isJsonArray()) {
|
|
// Handling array
|
|
Collection<EnumValueDescriptor> enumCollection =
|
|
new ArrayList<>(jsonElement.getAsJsonArray().size());
|
|
for (JsonElement element : jsonElement.getAsJsonArray()) {
|
|
enumCollection.add(
|
|
findValueByNameAndExtension(fieldDescriptor.getEnumType(), element));
|
|
}
|
|
fieldValue = enumCollection;
|
|
} else {
|
|
// No array, just a plain value
|
|
fieldValue =
|
|
findValueByNameAndExtension(fieldDescriptor.getEnumType(), jsonElement);
|
|
}
|
|
protoBuilder.setField(fieldDescriptor, fieldValue);
|
|
} else if (fieldDescriptor.isRepeated()) {
|
|
// If the type is an array, then we have to grab the type from the class.
|
|
// protobuf java field names are always lower camel case
|
|
String protoArrayFieldName =
|
|
protoFormat.to(CaseFormat.LOWER_CAMEL, fieldDescriptor.getName()) + "_";
|
|
Field protoArrayField = protoClass.getDeclaredField(protoArrayFieldName);
|
|
Type protoArrayFieldType = protoArrayField.getGenericType();
|
|
fieldValue = context.deserialize(jsonElement, protoArrayFieldType);
|
|
protoBuilder.setField(fieldDescriptor, fieldValue);
|
|
} else {
|
|
Object field = defaultInstance.getField(fieldDescriptor);
|
|
fieldValue = context.deserialize(jsonElement, field.getClass());
|
|
protoBuilder.setField(fieldDescriptor, fieldValue);
|
|
}
|
|
}
|
|
}
|
|
return protoBuilder.build();
|
|
} catch (SecurityException e) {
|
|
throw new JsonParseException(e);
|
|
} catch (NoSuchMethodException e) {
|
|
throw new JsonParseException(e);
|
|
} catch (IllegalArgumentException e) {
|
|
throw new JsonParseException(e);
|
|
} catch (IllegalAccessException e) {
|
|
throw new JsonParseException(e);
|
|
} catch (InvocationTargetException e) {
|
|
throw new JsonParseException(e);
|
|
}
|
|
} catch (Exception e) {
|
|
throw new JsonParseException("Error while parsing proto", e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retrieves the custom field name from the given options, and if not found, returns the specified
|
|
* default name.
|
|
*/
|
|
private String getCustSerializedName(FieldOptions options, String defaultName) {
|
|
for (Extension<FieldOptions, String> extension : serializedNameExtensions) {
|
|
if (options.hasExtension(extension)) {
|
|
return options.getExtension(extension);
|
|
}
|
|
}
|
|
return protoFormat.to(jsonFormat, defaultName);
|
|
}
|
|
|
|
/**
|
|
* Retrieves the custom enum value name from the given options, and if not found, returns the
|
|
* specified default value.
|
|
*/
|
|
private String getCustSerializedEnumValue(EnumValueOptions options, String defaultValue) {
|
|
for (Extension<EnumValueOptions, String> extension : serializedEnumValueExtensions) {
|
|
if (options.hasExtension(extension)) {
|
|
return options.getExtension(extension);
|
|
}
|
|
}
|
|
return defaultValue;
|
|
}
|
|
|
|
/**
|
|
* Returns the enum value to use for serialization, depending on the value of
|
|
* {@link EnumSerialization} that was given to this adapter.
|
|
*/
|
|
private Object getEnumValue(EnumValueDescriptor enumDesc) {
|
|
if (enumSerialization == EnumSerialization.NAME) {
|
|
return getCustSerializedEnumValue(enumDesc.getOptions(), enumDesc.getName());
|
|
} else {
|
|
return enumDesc.getNumber();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Finds an enum value in the given {@link EnumDescriptor} that matches the given JSON element,
|
|
* either by name if the current adapter is using {@link EnumSerialization#NAME}, otherwise by
|
|
* number. If matching by name, it uses the extension value if it is defined, otherwise it uses
|
|
* its default value.
|
|
*
|
|
* @throws IllegalArgumentException if a matching name/number was not found
|
|
*/
|
|
private EnumValueDescriptor findValueByNameAndExtension(EnumDescriptor desc,
|
|
JsonElement jsonElement) {
|
|
if (enumSerialization == EnumSerialization.NAME) {
|
|
// With enum name
|
|
for (EnumValueDescriptor enumDesc : desc.getValues()) {
|
|
String enumValue = getCustSerializedEnumValue(enumDesc.getOptions(), enumDesc.getName());
|
|
if (enumValue.equals(jsonElement.getAsString())) {
|
|
return enumDesc;
|
|
}
|
|
}
|
|
throw new IllegalArgumentException(
|
|
String.format("Unrecognized enum name: %s", jsonElement.getAsString()));
|
|
} else {
|
|
// With enum value
|
|
EnumValueDescriptor fieldValue = desc.findValueByNumber(jsonElement.getAsInt());
|
|
if (fieldValue == null) {
|
|
throw new IllegalArgumentException(
|
|
String.format("Unrecognized enum value: %d", jsonElement.getAsInt()));
|
|
}
|
|
return fieldValue;
|
|
}
|
|
}
|
|
|
|
private static Method getCachedMethod(Class<?> clazz, String methodName,
|
|
Class<?>... methodParamTypes) throws NoSuchMethodException {
|
|
ConcurrentMap<Class<?>, Method> mapOfMethods = mapOfMapOfMethods.get(methodName);
|
|
if (mapOfMethods == null) {
|
|
mapOfMethods = new MapMaker().makeMap();
|
|
ConcurrentMap<Class<?>, Method> previous =
|
|
mapOfMapOfMethods.putIfAbsent(methodName, mapOfMethods);
|
|
mapOfMethods = previous == null ? mapOfMethods : previous;
|
|
}
|
|
|
|
Method method = mapOfMethods.get(clazz);
|
|
if (method == null) {
|
|
method = clazz.getMethod(methodName, methodParamTypes);
|
|
mapOfMethods.putIfAbsent(clazz, method);
|
|
// NB: it doesn't matter which method we return in the event of a race.
|
|
}
|
|
return method;
|
|
}
|
|
|
|
}
|