Java tutorial
/* * 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 static 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<Extension<FieldOptions, String>>(); this.serializedEnumValueExtensions = new HashSet<Extension<EnumValueOptions, String>>(); 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> * 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, Map<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<EnumValueDescriptor>( 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 (Message) 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: %s", jsonElement.getAsInt())); } return fieldValue; } } private static Method getCachedMethod(Class<?> clazz, String methodName, Class<?>... methodParamTypes) throws NoSuchMethodException { Map<Class<?>, Method> mapOfMethods = mapOfMapOfMethods.get(methodName); if (mapOfMethods == null) { mapOfMethods = new MapMaker().makeMap(); Map<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; } }