Merge pull request #710 from Bhasmithal/master
Updating the ProtoTypeAdapter to support custom field names/values annotations
This commit is contained in:
commit
2b15334a49
@ -7,9 +7,12 @@
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>proto</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<version>0.5</version>
|
||||
<version>0.6-SNAPSHOT</version>
|
||||
<name>Gson Protobuf Support</name>
|
||||
<description>Gson support for Protobufs</description>
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
<distributionManagement>
|
||||
<repository>
|
||||
<id>local.repo</id>
|
||||
@ -52,21 +55,35 @@
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>1.7.1</version>
|
||||
<version>2.4</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.protobuf</groupId>
|
||||
<artifactId>protobuf-java</artifactId>
|
||||
<version>2.4.0a</version>
|
||||
<version>2.6.1</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<version>18.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>3.8.2</version>
|
||||
<version>4.12</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.truth</groupId>
|
||||
<artifactId>truth</artifactId>
|
||||
<version>0.27</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
@ -76,6 +93,7 @@
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-antrun-plugin</artifactId>
|
||||
<version>1.8</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>compile-protoc</id>
|
||||
@ -91,6 +109,7 @@
|
||||
<pathconvert pathsep=" " property="proto.files" refid="proto.path" />
|
||||
<exec executable="protoc" failonerror="true">
|
||||
<arg value="--java_out=src/main/java" />
|
||||
<arg value="--proto_path=/usr/include" />
|
||||
<arg value="-I${project.basedir}/src/main/protobuf" />
|
||||
<arg line="${proto.files}" />
|
||||
</exec>
|
||||
@ -125,6 +144,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-install-plugin</artifactId>
|
||||
<version>2.5.2</version>
|
||||
<!-- configuration>
|
||||
<updateReleaseInfo>true</updateReleaseInfo>
|
||||
<createChecksum>true</createChecksum>
|
||||
@ -182,7 +202,7 @@
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<configuration>
|
||||
<descriptor>src/main/resources/assembly-descriptor.xml</descriptor>
|
||||
<finalName>proto-${version}</finalName>
|
||||
<finalName>proto-${project.version}</finalName>
|
||||
<outputDirectory>target/dist</outputDirectory>
|
||||
<workDirectory>target/assembly/work</workDirectory>
|
||||
</configuration>
|
||||
|
@ -16,6 +16,10 @@
|
||||
|
||||
package com.google.gson.protobuf;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
import com.google.common.base.CaseFormat;
|
||||
import com.google.common.base.Converter;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonDeserializationContext;
|
||||
import com.google.gson.JsonDeserializer;
|
||||
@ -24,25 +28,182 @@ 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.Extension;
|
||||
import com.google.protobuf.GeneratedMessage;
|
||||
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.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Gson type adapter for protocol buffers
|
||||
* 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<GeneratedMessage>,
|
||||
JsonDeserializer<GeneratedMessage> {
|
||||
public class ProtoTypeAdapter
|
||||
implements JsonSerializer<GeneratedMessage>, JsonDeserializer<GeneratedMessage> {
|
||||
/**
|
||||
* 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 Converter<String, String> fieldNameSerializationFormat;
|
||||
|
||||
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) {
|
||||
fieldNameSerializationFormat = fromFieldNameFormat.converterTo(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, fieldNameSerializationFormat,
|
||||
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 final EnumSerialization enumSerialization;
|
||||
private final Converter<String, String> fieldNameSerializationFormat;
|
||||
private final Set<Extension<FieldOptions, String>> serializedNameExtensions;
|
||||
private final Set<Extension<EnumValueOptions, String>> serializedEnumValueExtensions;
|
||||
|
||||
private ProtoTypeAdapter(EnumSerialization enumSerialization,
|
||||
Converter<String, String> fieldNameSerializationFormat,
|
||||
Set<Extension<FieldOptions, String>> serializedNameExtensions,
|
||||
Set<Extension<EnumValueOptions, String>> serializedEnumValueExtensions) {
|
||||
this.enumSerialization = enumSerialization;
|
||||
this.fieldNameSerializationFormat = fieldNameSerializationFormat;
|
||||
this.serializedNameExtensions = serializedNameExtensions;
|
||||
this.serializedEnumValueExtensions = serializedEnumValueExtensions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(GeneratedMessage src, Type typeOfSrc,
|
||||
@ -52,56 +213,88 @@ public class ProtoTypeAdapter implements JsonSerializer<GeneratedMessage>,
|
||||
|
||||
for (Map.Entry<FieldDescriptor, Object> fieldPair : fields.entrySet()) {
|
||||
final FieldDescriptor desc = fieldPair.getKey();
|
||||
if (desc.isRepeated()) {
|
||||
List<?> fieldList = (List<?>) fieldPair.getValue();
|
||||
if (fieldList.size() != 0) {
|
||||
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();
|
||||
for (Object o : fieldList) {
|
||||
array.add(context.serialize(o));
|
||||
@SuppressWarnings("unchecked")
|
||||
Collection<EnumValueDescriptor> enumDescs =
|
||||
(Collection<EnumValueDescriptor>) fieldPair.getValue();
|
||||
for (EnumValueDescriptor enumDesc : enumDescs) {
|
||||
array.add(context.serialize(getEnumValue(enumDesc)));
|
||||
ret.add(name, array);
|
||||
}
|
||||
ret.add(desc.getName(), array);
|
||||
} else {
|
||||
EnumValueDescriptor enumDesc = ((EnumValueDescriptor) fieldPair.getValue());
|
||||
ret.add(name, context.serialize(getEnumValue(enumDesc)));
|
||||
}
|
||||
} else {
|
||||
ret.add(desc.getName(), context.serialize(fieldPair.getValue()));
|
||||
ret.add(name, context.serialize(fieldPair.getValue()));
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public GeneratedMessage deserialize(JsonElement json, Type typeOfT,
|
||||
JsonDeserializationContext context) throws JsonParseException {
|
||||
try {
|
||||
JsonObject jsonObject = json.getAsJsonObject();
|
||||
Class<? extends GeneratedMessage> protoClass =
|
||||
(Class<? extends GeneratedMessage>) typeOfT;
|
||||
@SuppressWarnings("unchecked")
|
||||
Class<? extends GeneratedMessage> protoClass = (Class<? extends GeneratedMessage>) typeOfT;
|
||||
|
||||
try {
|
||||
// Invoke the ProtoClass.newBuilder() method
|
||||
Object protoBuilder = getCachedMethod(protoClass, "newBuilder")
|
||||
.invoke(null);
|
||||
Class<?> builderClass = protoBuilder.getClass();
|
||||
GeneratedMessage.Builder<?> protoBuilder =
|
||||
(GeneratedMessage.Builder<?>) getCachedMethod(protoClass, "newBuilder").invoke(null);
|
||||
|
||||
Descriptor protoDescriptor = (Descriptor) getCachedMethod(
|
||||
protoClass, "getDescriptor").invoke(null);
|
||||
Descriptor protoDescriptor =
|
||||
(Descriptor) getCachedMethod(protoClass, "getDescriptor").invoke(null);
|
||||
// Call setters on all of the available fields
|
||||
for (FieldDescriptor fieldDescriptor : protoDescriptor.getFields()) {
|
||||
String name = fieldDescriptor.getName();
|
||||
if (jsonObject.has(name)) {
|
||||
JsonElement jsonElement = jsonObject.get(name);
|
||||
String fieldName = name + "_";
|
||||
Field field = protoClass.getDeclaredField(fieldName);
|
||||
Type fieldType = field.getGenericType();
|
||||
Object fieldValue = context.deserialize(jsonElement, fieldType);
|
||||
Method method = getCachedMethod(
|
||||
builderClass, "setField", FieldDescriptor.class, Object.class);
|
||||
method.invoke(protoBuilder, fieldDescriptor, fieldValue);
|
||||
String jsonFieldName =
|
||||
getCustSerializedName(fieldDescriptor.getOptions(), fieldDescriptor.getName());
|
||||
|
||||
if (jsonObject.has(jsonFieldName)) {
|
||||
JsonElement jsonElement = jsonObject.get(jsonFieldName);
|
||||
// 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>();
|
||||
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.
|
||||
String protoArrayFieldName =
|
||||
fieldNameSerializationFormat.convert(fieldDescriptor.getName()) + "_";
|
||||
Field protoArrayField = protoClass.getDeclaredField(protoArrayFieldName);
|
||||
Type protoArrayFieldType = protoArrayField.getGenericType();
|
||||
fieldValue = context.deserialize(jsonElement, protoArrayFieldType);
|
||||
protoBuilder.setField(fieldDescriptor, fieldValue);
|
||||
} else {
|
||||
Message prototype = protoBuilder.build();
|
||||
Object field = prototype.getField(fieldDescriptor);
|
||||
fieldValue = context.deserialize(jsonElement, field.getClass());
|
||||
protoBuilder.setField(fieldDescriptor, fieldValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Invoke the build method to return the final proto
|
||||
return (GeneratedMessage) getCachedMethod(builderClass, "build")
|
||||
.invoke(protoBuilder);
|
||||
return (GeneratedMessage) protoBuilder.build();
|
||||
} catch (SecurityException e) {
|
||||
throw new JsonParseException(e);
|
||||
} catch (NoSuchMethodException e) {
|
||||
@ -114,25 +307,91 @@ public class ProtoTypeAdapter implements JsonSerializer<GeneratedMessage>,
|
||||
throw new JsonParseException(e);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new JsonParseException("Error while parsing proto: ", e);
|
||||
throw new JsonParseException("Error while parsing proto", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Method getCachedMethod(Class<?> clazz, String methodName,
|
||||
Class<?>... methodParamTypes) throws NoSuchMethodException {
|
||||
/**
|
||||
* 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 fieldNameSerializationFormat.convert(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)
|
||||
throws NoSuchMethodException {
|
||||
if (!mapOfMapOfMethods.containsKey(methodName)) {
|
||||
mapOfMapOfMethods.put(methodName, new HashMap<Class<?>, Method>());
|
||||
}
|
||||
Map<Class<?>, Method> mapOfMethods = mapOfMapOfMethods.get(methodName);
|
||||
if (mapOfMethods == null) {
|
||||
mapOfMethods = new HashMap<Class<?>, Method>();
|
||||
mapOfMapOfMethods.put(methodName, mapOfMethods);
|
||||
if (!mapOfMethods.containsKey(clazz)) {
|
||||
mapOfMethods.put(clazz, clazz.getMethod(methodName));
|
||||
}
|
||||
Method method = mapOfMethods.get(clazz);
|
||||
if (method == null) {
|
||||
method = clazz.getMethod(methodName, methodParamTypes);
|
||||
mapOfMethods.put(clazz, method);
|
||||
}
|
||||
return method;
|
||||
return mapOfMethods.get(clazz);
|
||||
}
|
||||
|
||||
private static Map<String, Map<Class<?>, Method>> mapOfMapOfMethods =
|
||||
new HashMap<String, Map<Class<?>, Method>>();
|
||||
}
|
||||
new HashMap<String, Map<Class<?>, Method>>();
|
||||
}
|
||||
|
1
proto/src/main/java/com/google/gson/protobuf/generated/.gitignore
vendored
Normal file
1
proto/src/main/java/com/google/gson/protobuf/generated/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
**
|
32
proto/src/main/protobuf/annotations.proto
Normal file
32
proto/src/main/protobuf/annotations.proto
Normal file
@ -0,0 +1,32 @@
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
syntax = "proto2";
|
||||
|
||||
package google.gson.protobuf.generated;
|
||||
option java_package = "com.google.gson.protobuf.generated";
|
||||
|
||||
import "google/protobuf/descriptor.proto";
|
||||
|
||||
extend google.protobuf.FieldOptions {
|
||||
// Indicates a field name that overrides the default for serialization
|
||||
optional string serialized_name = 92066888;
|
||||
}
|
||||
|
||||
extend google.protobuf.EnumValueOptions {
|
||||
// Indicates a field value that overrides the default for serialization
|
||||
optional string serialized_value = 92066888;
|
||||
}
|
@ -14,9 +14,11 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
package com.google.gson.protobuf.generated;
|
||||
package google.gson.protobuf.generated;
|
||||
option java_package = "com.google.gson.protobuf.generated";
|
||||
|
||||
import "annotations.proto";
|
||||
|
||||
message SimpleProto {
|
||||
optional string msg = 1;
|
||||
optional int32 count = 2;
|
||||
@ -26,4 +28,38 @@ message ProtoWithRepeatedFields {
|
||||
repeated int64 numbers = 1;
|
||||
repeated SimpleProto simples = 2;
|
||||
optional string name = 3;
|
||||
}
|
||||
|
||||
// -- A more complex message with annotations and nested protos
|
||||
|
||||
message OuterMessage {
|
||||
optional int32 month = 1;
|
||||
optional int32 year = 2;
|
||||
optional int64 long_timestamp = 3 [(serialized_name) = "timeStamp"];
|
||||
optional string country_code_5f55 = 4;
|
||||
}
|
||||
|
||||
message ProtoWithAnnotations {
|
||||
optional string id = 1;
|
||||
optional OuterMessage outer_message = 2 [(serialized_name) = "expiration_date"];
|
||||
|
||||
message InnerMessage {
|
||||
optional int32 n__id_ct = 1;
|
||||
|
||||
enum Type {
|
||||
UNKNOWN = 0;
|
||||
TEXT = 1 [(serialized_value) = "text/plain"];
|
||||
IMAGE = 2 [(serialized_value) = "image/png"];
|
||||
}
|
||||
optional Type content = 2;
|
||||
|
||||
message Data {
|
||||
optional string data = 1;
|
||||
optional int32 width = 2;
|
||||
optional int32 height = 3;
|
||||
}
|
||||
repeated Data data = 3 [(serialized_name) = "$binary_data$"];
|
||||
}
|
||||
optional InnerMessage inner_message_1 = 3;
|
||||
optional InnerMessage inner_message_2 = 4;
|
||||
}
|
@ -0,0 +1,227 @@
|
||||
/*
|
||||
* 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.functional;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static com.google.common.truth.Truth.assert_;
|
||||
|
||||
import com.google.common.base.CaseFormat;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.protobuf.ProtoTypeAdapter;
|
||||
import com.google.gson.protobuf.ProtoTypeAdapter.EnumSerialization;
|
||||
import com.google.gson.protobuf.generated.Annotations;
|
||||
import com.google.gson.protobuf.generated.Bag.OuterMessage;
|
||||
import com.google.gson.protobuf.generated.Bag.ProtoWithAnnotations;
|
||||
import com.google.gson.protobuf.generated.Bag.ProtoWithAnnotations.InnerMessage;
|
||||
import com.google.gson.protobuf.generated.Bag.ProtoWithAnnotations.InnerMessage.Data;
|
||||
import com.google.gson.protobuf.generated.Bag.ProtoWithAnnotations.InnerMessage.Type;
|
||||
import com.google.protobuf.GeneratedMessage;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
/**
|
||||
* Functional tests for protocol buffers using annotations for field names and enum values.
|
||||
*
|
||||
* @author Emmanuel Cron
|
||||
*/
|
||||
public class ProtosWithAnnotationsTest extends TestCase {
|
||||
private Gson gson;
|
||||
private Gson gsonWithEnumNumbers;
|
||||
private Gson gsonWithLowerHyphen;
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
ProtoTypeAdapter.Builder protoTypeAdapter = ProtoTypeAdapter.newBuilder()
|
||||
.setEnumSerialization(EnumSerialization.NAME)
|
||||
.addSerializedNameExtension(Annotations.serializedName)
|
||||
.addSerializedEnumValueExtension(Annotations.serializedValue);
|
||||
gson = new GsonBuilder()
|
||||
.registerTypeHierarchyAdapter(GeneratedMessage.class, protoTypeAdapter.build())
|
||||
.create();
|
||||
gsonWithEnumNumbers = new GsonBuilder()
|
||||
.registerTypeHierarchyAdapter(GeneratedMessage.class, protoTypeAdapter
|
||||
.setEnumSerialization(EnumSerialization.NUMBER)
|
||||
.build())
|
||||
.create();
|
||||
gsonWithLowerHyphen = new GsonBuilder()
|
||||
.registerTypeHierarchyAdapter(GeneratedMessage.class, protoTypeAdapter
|
||||
.setFieldNameSerializationFormat(CaseFormat.LOWER_UNDERSCORE, CaseFormat.LOWER_HYPHEN)
|
||||
.build())
|
||||
.create();
|
||||
}
|
||||
|
||||
public void testProtoWithAnnotations_deserialize() {
|
||||
String json = String.format("{ %n"
|
||||
+ " \"id\":\"41e5e7fd6065d101b97018a465ffff01\",%n"
|
||||
+ " \"expiration_date\":{ %n"
|
||||
+ " \"month\":\"12\",%n"
|
||||
+ " \"year\":\"2017\",%n"
|
||||
+ " \"timeStamp\":\"9864653135687\",%n"
|
||||
+ " \"countryCode5f55\":\"en_US\"%n"
|
||||
+ " },%n"
|
||||
// Don't define innerMessage1
|
||||
+ " \"innerMessage2\":{ %n"
|
||||
// Set a number as a string; it should work
|
||||
+ " \"nIdCt\":\"98798465\",%n"
|
||||
+ " \"content\":\"text/plain\",%n"
|
||||
+ " \"$binary_data$\":[ %n"
|
||||
+ " { %n"
|
||||
+ " \"data\":\"OFIN8e9fhwoeh8((⁹8efywoih\",%n"
|
||||
// Don't define width
|
||||
+ " \"height\":665%n"
|
||||
+ " },%n"
|
||||
+ " { %n"
|
||||
// Define as an int; it should work
|
||||
+ " \"data\":65,%n"
|
||||
+ " \"width\":-56684%n"
|
||||
// Don't define height
|
||||
+ " }%n"
|
||||
+ " ]%n"
|
||||
+ " },%n"
|
||||
// Define a bunch of non recognizable data
|
||||
+ " \"non_existing\":\"foobar\",%n"
|
||||
+ " \"stillNot\":{ %n"
|
||||
+ " \"bunch\":\"of_useless data\"%n"
|
||||
+ " }%n"
|
||||
+ "}");
|
||||
ProtoWithAnnotations proto = gson.fromJson(json, ProtoWithAnnotations.class);
|
||||
assertThat(proto.getId()).isEqualTo("41e5e7fd6065d101b97018a465ffff01");
|
||||
assertThat(proto.getOuterMessage()).isEqualTo(OuterMessage.newBuilder()
|
||||
.setMonth(12)
|
||||
.setYear(2017)
|
||||
.setLongTimestamp(9864653135687L)
|
||||
.setCountryCode5F55("en_US")
|
||||
.build());
|
||||
assertThat(proto.hasInnerMessage1()).isFalse();
|
||||
assertThat(proto.getInnerMessage2()).isEqualTo(InnerMessage.newBuilder()
|
||||
.setNIdCt(98798465)
|
||||
.setContent(Type.TEXT)
|
||||
.addData(Data.newBuilder()
|
||||
.setData("OFIN8e9fhwoeh8((⁹8efywoih")
|
||||
.setHeight(665))
|
||||
.addData(Data.newBuilder()
|
||||
.setData("65")
|
||||
.setWidth(-56684))
|
||||
.build());
|
||||
|
||||
String rebuilt = gson.toJson(proto);
|
||||
assertThat(rebuilt).isEqualTo("{"
|
||||
+ "\"id\":\"41e5e7fd6065d101b97018a465ffff01\","
|
||||
+ "\"expiration_date\":{"
|
||||
+ "\"month\":12,"
|
||||
+ "\"year\":2017,"
|
||||
+ "\"timeStamp\":9864653135687,"
|
||||
+ "\"countryCode5f55\":\"en_US\""
|
||||
+ "},"
|
||||
+ "\"innerMessage2\":{"
|
||||
+ "\"nIdCt\":98798465,"
|
||||
+ "\"content\":\"text/plain\","
|
||||
+ "\"$binary_data$\":["
|
||||
+ "{"
|
||||
+ "\"data\":\"OFIN8e9fhwoeh8((⁹8efywoih\","
|
||||
+ "\"height\":665"
|
||||
+ "},"
|
||||
+ "{"
|
||||
+ "\"data\":\"65\","
|
||||
+ "\"width\":-56684"
|
||||
+ "}]}}");
|
||||
}
|
||||
|
||||
public void testProtoWithAnnotations_deserializeUnknownEnumValue() {
|
||||
String json = String.format("{ %n"
|
||||
+ " \"content\":\"UNKNOWN\"%n"
|
||||
+ "}");
|
||||
InnerMessage proto = gson.fromJson(json, InnerMessage.class);
|
||||
assertThat(proto.getContent()).isEqualTo(Type.UNKNOWN);
|
||||
}
|
||||
|
||||
public void testProtoWithAnnotations_deserializeUnrecognizedEnumValue() {
|
||||
String json = String.format("{ %n"
|
||||
+ " \"content\":\"UNRECOGNIZED\"%n"
|
||||
+ "}");
|
||||
try {
|
||||
gson.fromJson(json, InnerMessage.class);
|
||||
assert_().fail("Should have thrown");
|
||||
} catch (JsonParseException e) {
|
||||
// expected
|
||||
}
|
||||
}
|
||||
|
||||
public void testProtoWithAnnotations_deserializeWithEnumNumbers() {
|
||||
String json = String.format("{ %n"
|
||||
+ " \"content\":\"0\"%n"
|
||||
+ "}");
|
||||
InnerMessage proto = gsonWithEnumNumbers.fromJson(json, InnerMessage.class);
|
||||
assertThat(proto.getContent()).isEqualTo(Type.UNKNOWN);
|
||||
String rebuilt = gsonWithEnumNumbers.toJson(proto);
|
||||
assertThat(rebuilt).isEqualTo("{\"content\":0}");
|
||||
|
||||
json = String.format("{ %n"
|
||||
+ " \"content\":\"2\"%n"
|
||||
+ "}");
|
||||
proto = gsonWithEnumNumbers.fromJson(json, InnerMessage.class);
|
||||
assertThat(proto.getContent()).isEqualTo(Type.IMAGE);
|
||||
rebuilt = gsonWithEnumNumbers.toJson(proto);
|
||||
assertThat(rebuilt).isEqualTo("{\"content\":2}");
|
||||
}
|
||||
|
||||
public void testProtoWithAnnotations_serialize() {
|
||||
ProtoWithAnnotations proto = ProtoWithAnnotations.newBuilder()
|
||||
.setId("09f3j20839h032y0329hf30932h0nffn")
|
||||
.setOuterMessage(OuterMessage.newBuilder()
|
||||
.setMonth(14)
|
||||
.setYear(6650)
|
||||
.setLongTimestamp(468406876880768L))
|
||||
.setInnerMessage1(InnerMessage.newBuilder()
|
||||
.setNIdCt(12)
|
||||
.setContent(Type.IMAGE)
|
||||
.addData(Data.newBuilder()
|
||||
.setData("data$$")
|
||||
.setWidth(200))
|
||||
.addData(Data.newBuilder()
|
||||
.setHeight(56)))
|
||||
.build();
|
||||
|
||||
String json = gsonWithLowerHyphen.toJson(proto);
|
||||
assertThat(json).isEqualTo(
|
||||
"{\"id\":\"09f3j20839h032y0329hf30932h0nffn\","
|
||||
+ "\"expiration_date\":{"
|
||||
+ "\"month\":14,"
|
||||
+ "\"year\":6650,"
|
||||
+ "\"timeStamp\":468406876880768"
|
||||
+ "},"
|
||||
// This field should be using hyphens
|
||||
+ "\"inner-message-1\":{"
|
||||
+ "\"n--id-ct\":12,"
|
||||
+ "\"content\":2,"
|
||||
+ "\"$binary_data$\":["
|
||||
+ "{"
|
||||
+ "\"data\":\"data$$\","
|
||||
+ "\"width\":200"
|
||||
+ "},"
|
||||
+ "{"
|
||||
+ "\"height\":56"
|
||||
+ "}]"
|
||||
+ "}"
|
||||
+ "}");
|
||||
|
||||
ProtoWithAnnotations rebuilt = gsonWithLowerHyphen.fromJson(json, ProtoWithAnnotations.class);
|
||||
assertThat(rebuilt).isEqualTo(proto);
|
||||
}
|
||||
}
|
@ -18,6 +18,7 @@ package com.google.gson.protobuf.functional;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.protobuf.ProtoTypeAdapter;
|
||||
import com.google.gson.protobuf.ProtoTypeAdapter.EnumSerialization;
|
||||
import com.google.gson.protobuf.generated.Bag.ProtoWithRepeatedFields;
|
||||
import com.google.gson.protobuf.generated.Bag.SimpleProto;
|
||||
import com.google.protobuf.GeneratedMessage;
|
||||
@ -35,8 +36,13 @@ public class ProtosWithComplexAndRepeatedFieldsTest extends TestCase {
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
gson = new GsonBuilder().registerTypeHierarchyAdapter(
|
||||
GeneratedMessage.class, new ProtoTypeAdapter()).create();
|
||||
gson =
|
||||
new GsonBuilder()
|
||||
.registerTypeHierarchyAdapter(GeneratedMessage.class,
|
||||
ProtoTypeAdapter.newBuilder()
|
||||
.setEnumSerialization(EnumSerialization.NUMBER)
|
||||
.build())
|
||||
.create();
|
||||
}
|
||||
|
||||
public void testSerializeRepeatedFields() {
|
||||
|
@ -18,6 +18,7 @@ package com.google.gson.protobuf.functional;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.protobuf.ProtoTypeAdapter;
|
||||
import com.google.gson.protobuf.ProtoTypeAdapter.EnumSerialization;
|
||||
import com.google.gson.protobuf.generated.Bag.SimpleProto;
|
||||
import com.google.protobuf.Descriptors.Descriptor;
|
||||
import com.google.protobuf.GeneratedMessage;
|
||||
@ -31,7 +32,10 @@ public class ProtosWithPrimitiveTypesTest extends TestCase {
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
gson = new GsonBuilder().registerTypeHierarchyAdapter(
|
||||
GeneratedMessage.class, new ProtoTypeAdapter()).create();
|
||||
GeneratedMessage.class, ProtoTypeAdapter.newBuilder()
|
||||
.setEnumSerialization(EnumSerialization.NUMBER)
|
||||
.build())
|
||||
.create();
|
||||
}
|
||||
|
||||
public void testSerializeEmptyProto() {
|
||||
|
Loading…
Reference in New Issue
Block a user