diff --git a/.travis.yml b/.travis.yml index ed7bd28c..baba0273 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,8 +4,8 @@ jdk: android: components: - tools - - build-tools-27.0.3 - - android-27 + - build-tools-28.0.3 + - android-28 - extra-android-support - extra-android-m2repository # workaround for Google changing android-27 package and causing a checksum mismatch diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f1255ce..45b91325 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,15 @@ Change Log ========== +Version 2.6.0 *(2018-10-25)* +---------------------------- +- Improved formatting of generated code. +- Improved logging by putting all debug logging behind the `stagDebug` flag. +- Improve integration of library by switching to new nullability annotations library. +- Wrote unit tests for `KnownTypeAdapters`. +- Fixed bug where code generation was non deterministic by switching to linked versions of `HashSet` and `HashMap`. +- Added support for turning on/off serialization of `null` with `stag.serializeNulls` compiler option. Default is off, which is a behavior change from version 2.5.1. + Version 2.5.1 *(2018-01-17)* ---------------------------- - Fixed bug where types with wildcards caused compilation to fail. diff --git a/README.md b/README.md index 2d5e1c22..a63669a7 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ buildscript { apply plugin: 'net.ltgt.apt' dependencies { - def stagVersion = '2.5.1' + def stagVersion = '2.6.0' compile "com.vimeo.stag:stag-library:$stagVersion" apt "com.vimeo.stag:stag-library-compiler:$stagVersion" } @@ -45,9 +45,10 @@ dependencies { gradle.projectsEvaluated { tasks.withType(JavaCompile) { aptOptions.processorArgs = [ - stagAssumeHungarianNotation: "true", - stagGeneratedPackageName : "com.vimeo.sample.stag.generated", - stagDebug : "true" + "stagAssumeHungarianNotation": "true", + "stagGeneratedPackageName" : "com.vimeo.sample.stag.generated", + "stagDebug " : "true", + "stag.serializeNulls" : "true", ] } } @@ -58,7 +59,7 @@ gradle.projectsEvaluated { apply plugin: 'kotlin-kapt' dependencies { - def stagVersion = '2.5.1' + def stagVersion = '2.6.0' compile "com.vimeo.stag:stag-library:$stagVersion" kapt "com.vimeo.stag:stag-library-compiler:$stagVersion" } @@ -70,6 +71,7 @@ kapt { arg("stagDebug", "true") arg("stagGeneratedPackageName", "com.vimeo.sample.stag.generated") arg("stagAssumeHungarianNotation", "true") + arg("stag.serializeNulls", "true") } } ``` @@ -78,7 +80,7 @@ kapt { ```groovy dependencies { - def stagVersion = '2.5.1' + def stagVersion = '2.6.0' compile "com.vimeo.stag:stag-library:$stagVersion" annotationProcessor "com.vimeo.stag:stag-library-compiler:$stagVersion" } @@ -91,9 +93,10 @@ android { javaCompileOptions { annotationProcessorOptions { arguments = [ - stagAssumeHungarianNotation: 'true' - stagGeneratedPackageName : 'com.vimeo.sample.stag.generated', - stagDebug : 'true' + "stagAssumeHungarianNotation": 'true', + "stagGeneratedPackageName" : 'com.vimeo.sample.stag.generated', + "stagDebug" : 'true', + "stag.serializeNulls" : 'true' ] } } @@ -112,6 +115,10 @@ android { Stag will look for members named `set[variable_name]` and `get[variable_name]`. If your member variables are named using Hungarian notation, then you will need to pass true to this parameter so that for a field named `mField`, Stag will look for `setField` and `getField` instead of `setMField` and `getMField`. Default is false. + - `stag.serializeNulls`: By default this is set to false. If an object has a null field and you send it to be serialized by Gson, it is optional + whether or not that field is serialized into the JSON. If this field is set to `false` null fields will not be serialized, and if set to `true`, + they will be serialized. Prior to stag version 2.6.0, null fields were always serialized to JSON. This should not affect most models. However, if + you have a model that has a nullable field that also has a non null default value, then it might be a good idea to turn this option on. ## Features @@ -249,7 +256,7 @@ class Herd { * You parse the list from JSON using * Gson. */ -MyParsingClass { +class MyParsingClass { private Gson gson = new GsonBuilder() .registerTypeAdapterFactory(new Stag.Factory()) .create(); diff --git a/build.gradle b/build.gradle index 40ae096e..7cde4888 100644 --- a/build.gradle +++ b/build.gradle @@ -1,14 +1,14 @@ buildscript { ext { - kotlinVersion = '1.2.61' + kotlinVersion = '1.2.71' jacocoVersion = '0.7.9' // See http://www.eclemma.org/jacoco/ gsonVersion = '2.8.2' assertJ = '3.9.1' // android dependencies - targetSdk = 27 + targetSdk = 28 minSdk = 14 - buildTools = "27.0.3" + buildTools = "28.0.3" } repositories { google() @@ -17,7 +17,7 @@ buildscript { maven { url "https://plugins.gradle.org/m2/" } } dependencies { - classpath 'com.android.tools.build:gradle:3.1.2' + classpath 'com.android.tools.build:gradle:3.2.1' classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7.3' classpath 'com.dicedmelon.gradle:jacoco-android:0.1.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" @@ -43,5 +43,5 @@ allprojects { subprojects { group = 'com.vimeo.stag' - version = '2.5.1' + version = '2.6.0' } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 207e72b8..f83c6b89 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Apr 09 13:12:02 EDT 2018 +#Thu Oct 25 11:29:47 EDT 2018 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip diff --git a/integration-test-android/build.gradle b/integration-test-android/build.gradle index 729f7c9e..2bcd4248 100644 --- a/integration-test-android/build.gradle +++ b/integration-test-android/build.gradle @@ -34,8 +34,8 @@ android { dependencies { implementation 'org.jetbrains:annotations-java5:16.0.2@jar' - implementation 'com.android.support:appcompat-v7:27.1.1' - implementation 'com.android.support:support-annotations:27.1.1' + implementation 'com.android.support:appcompat-v7:28.0.0' + implementation 'com.android.support:support-annotations:28.0.0' implementation project(':stag-library') annotationProcessor project(':stag-library-compiler') diff --git a/integration-test-java-cross-module/build.gradle b/integration-test-java-cross-module/build.gradle index e4e23f6f..b76a37a4 100644 --- a/integration-test-java-cross-module/build.gradle +++ b/integration-test-java-cross-module/build.gradle @@ -4,10 +4,10 @@ apply plugin: "net.ltgt.apt" dependencies { implementation "com.google.code.gson:gson:$gsonVersion" implementation 'org.jetbrains:annotations-java5:16.0.2@jar' - implementation 'com.android.support:support-annotations:27.1.1' + implementation 'com.android.support:support-annotations:28.0.0' implementation project(':stag-library') - apt project(':stag-library-compiler') + annotationProcessor project(':stag-library-compiler') implementation project(':integration-test-java') diff --git a/integration-test-java/build.gradle b/integration-test-java/build.gradle index 9ea2cd51..3a3558c5 100644 --- a/integration-test-java/build.gradle +++ b/integration-test-java/build.gradle @@ -6,7 +6,7 @@ dependencies { implementation "com.google.code.gson:gson:$gsonVersion" implementation project(':stag-library') - apt project(':stag-library-compiler') + annotationProcessor project(':stag-library-compiler') testImplementation 'junit:junit:4.12' testImplementation 'uk.co.jemos.podam:podam:7.2.0.RELEASE' diff --git a/sample/build.gradle b/sample/build.gradle index 400fcdd9..43e9f6ea 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -45,7 +45,7 @@ android { dependencies { testImplementation 'junit:junit:4.12' - implementation 'com.android.support:appcompat-v7:27.1.1' + implementation 'com.android.support:appcompat-v7:28.0.0' implementation project(':stag-library') annotationProcessor project(':stag-library-compiler') diff --git a/stag-library-compiler/src/main/java/com/vimeo/stag/processor/StagProcessor.java b/stag-library-compiler/src/main/java/com/vimeo/stag/processor/StagProcessor.java index 271b6d3d..b0d9752c 100644 --- a/stag-library-compiler/src/main/java/com/vimeo/stag/processor/StagProcessor.java +++ b/stag-library-compiler/src/main/java/com/vimeo/stag/processor/StagProcessor.java @@ -72,13 +72,14 @@ @AutoService(Processor.class) @SupportedAnnotationTypes(value = {"com.vimeo.stag.UseStag"}) -@SupportedOptions(value = {StagProcessor.OPTION_PACKAGE_NAME, StagProcessor.OPTION_DEBUG, StagProcessor.OPTION_HUNGARIAN_NOTATION}) +@SupportedOptions(value = {StagProcessor.OPTION_PACKAGE_NAME, StagProcessor.OPTION_DEBUG, StagProcessor.OPTION_HUNGARIAN_NOTATION, StagProcessor.OPTION_SERIALIZE_NULLS}) @SupportedSourceVersion(SourceVersion.RELEASE_7) public final class StagProcessor extends AbstractProcessor { static final String OPTION_DEBUG = "stagDebug"; static final String OPTION_PACKAGE_NAME = "stagGeneratedPackageName"; static final String OPTION_HUNGARIAN_NOTATION = "stagAssumeHungarianNotation"; + static final String OPTION_SERIALIZE_NULLS = "stag.serializeNulls"; private static final String DEFAULT_GENERATED_PACKAGE_NAME = "com.vimeo.stag.generated"; private boolean mHasBeenProcessed; @@ -98,6 +99,14 @@ private static boolean getAssumeHungarianNotation(@NotNull ProcessingEnvironment return false; } + private static boolean isSerializeNullsEnabled(@NotNull ProcessingEnvironment processingEnvironment) { + String debugString = processingEnvironment.getOptions().get(OPTION_SERIALIZE_NULLS); + if (debugString != null) { + return Boolean.valueOf(debugString); + } + return false; + } + @NotNull private static String getOptionalPackageName(@NotNull ProcessingEnvironment processingEnvironment) { String packageName = processingEnvironment.getOptions().get(OPTION_PACKAGE_NAME); @@ -159,6 +168,7 @@ public boolean process(Set annotations, RoundEnvironment String packageName = getOptionalPackageName(processingEnv); boolean assumeHungarianNotation = getAssumeHungarianNotation(processingEnv); + boolean enableSerializeNulls = isSerializeNullsEnabled(processingEnv); TypeUtils.initialize(processingEnv.getTypeUtils()); ElementUtils.initialize(processingEnv.getElementUtils()); @@ -190,7 +200,7 @@ public boolean process(Set annotations, RoundEnvironment for (AnnotatedClass annotatedClass : supportedTypesModel.getSupportedTypes()) { TypeElement element = annotatedClass.getElement(); if ((TypeUtils.isConcreteType(element) || TypeUtils.isParameterizedType(element)) && !TypeUtils.isAbstract(element)) { - generateTypeAdapter(supportedTypesModel, element, stagFactoryGenerator); + generateTypeAdapter(supportedTypesModel, element, stagFactoryGenerator, enableSerializeNulls); ClassInfo classInfo = new ClassInfo(element.asType()); ArrayList result = new ArrayList<>(); @@ -242,13 +252,13 @@ private void generateStagFactory(@NotNull String packageName, List typeMirrors = ((DeclaredType) fieldType).getTypeArguments(); StringBuilder result = new StringBuilder("(com.google.gson.reflect.TypeToken<" + fieldType.toString() + ">)com.google.gson.reflect.TypeToken.getParameterized(" + - declaredFieldType.asElement().toString() + ".class"); + declaredFieldType.asElement().toString() + ".class"); /* * Iterate through all the types from the typeArguments and generate type token code accordingly @@ -181,96 +182,6 @@ private static TypeName getTypeTokenFieldTypeName(@NotNull TypeMirror type) { return ParameterizedTypeName.get(ClassName.get(TypeToken.class), typeName); } - @NotNull - private static MethodSpec getReadMethodSpec(@NotNull TypeName typeName, - @NotNull Map elements, - @NotNull AdapterFieldInfo adapterFieldInfo) { - MethodSpec.Builder builder = MethodSpec.methodBuilder("read") - .addParameter(JsonReader.class, "reader") - .returns(typeName) - .addModifiers(Modifier.PUBLIC) - .addAnnotation(Override.class) - .addException(IOException.class); - - builder.addStatement("com.google.gson.stream.JsonToken peek = reader.peek()"); - - builder.beginControlFlow("if (com.google.gson.stream.JsonToken.NULL == peek)"); - builder.addStatement("reader.nextNull()"); - builder.addStatement("return null"); - builder.endControlFlow(); - - - builder.beginControlFlow("if (com.google.gson.stream.JsonToken.BEGIN_OBJECT != peek)"); - builder.addStatement("reader.skipValue()"); - builder.addStatement("return null"); - builder.endControlFlow(); - - builder.addStatement("reader.beginObject()"); - builder.addStatement(typeName + " object = new " + typeName + "()"); - - builder.beginControlFlow("while (reader.hasNext())"); - builder.addStatement("String name = reader.nextName()"); - builder.beginControlFlow("switch (name)"); - - - final List nonNullFields = new ArrayList<>(); - - for (Map.Entry element : elements.entrySet()) { - final FieldAccessor fieldAccessor = element.getKey(); - String name = fieldAccessor.getJsonName(); - - final TypeMirror elementValue = element.getValue(); - - builder.addCode("case \"" + name + "\":\n"); - - String[] alternateJsonNames = fieldAccessor.getAlternateJsonNames(); - if (alternateJsonNames != null && alternateJsonNames.length > 0) { - for (String alternateJsonName : alternateJsonNames) { - builder.addCode("case \"" + alternateJsonName + "\":\n"); - } - } - - String variableType = element.getValue().toString(); - boolean isPrimitive = TypeUtils.isSupportedPrimitive(variableType); - - if (isPrimitive) { - builder.addStatement("\tobject." + - fieldAccessor.createSetterCode(adapterFieldInfo.getAdapterAccessor(elementValue, name) + - ".read(reader, object." + fieldAccessor.createGetterCode() + ")")); - - } else { - builder.addStatement("\tobject." + fieldAccessor.createSetterCode(adapterFieldInfo.getAdapterAccessor(elementValue, name) + - ".read(reader)")); - } - - - builder.addStatement("\tbreak"); - if (fieldAccessor.doesRequireNotNull()) { - if (!TypeUtils.isSupportedPrimitive(elementValue.toString())) { - nonNullFields.add(fieldAccessor); - } - } - } - - builder.addCode("default:\n"); - builder.addStatement("reader.skipValue()"); - builder.addStatement("break"); - builder.endControlFlow(); - builder.endControlFlow(); - - builder.addStatement("reader.endObject()"); - - for (FieldAccessor nonNullField : nonNullFields) { - builder.beginControlFlow("if (object." + nonNullField.createGetterCode() + " == null)"); - builder.addStatement("throw new java.io.IOException(\"" + nonNullField.createGetterCode() + " cannot be null\")"); - builder.endControlFlow(); - } - - builder.addStatement("return object"); - - return builder.build(); - } - @NotNull private static String getInitializationCodeForKnownJsonAdapterType(@NotNull ExecutableElement adapterType, @NotNull StagGenerator stagGenerator, @@ -308,14 +219,14 @@ private static String getInitializationCodeForKnownJsonAdapterType(@NotNull Exec String typeTokenAccessorCode = getTypeTokenCode(fieldType, stagGenerator, typeVarsMap, adapterFieldInfo); fieldAdapterAccessor += "().create(gson, " + typeTokenAccessorCode + ")"; } else if (jsonAdapterType == TypeUtils.JsonAdapterType.JSON_SERIALIZER - || jsonAdapterType == TypeUtils.JsonAdapterType.JSON_DESERIALIZER - || jsonAdapterType == TypeUtils.JsonAdapterType.JSON_SERIALIZER_DESERIALIZER) { + || jsonAdapterType == TypeUtils.JsonAdapterType.JSON_DESERIALIZER + || jsonAdapterType == TypeUtils.JsonAdapterType.JSON_SERIALIZER_DESERIALIZER) { String serializer = null, deserializer = null; if (jsonAdapterType == TypeUtils.JsonAdapterType.JSON_SERIALIZER_DESERIALIZER) { String varName = keyFieldName + "SerializerDeserializer"; String initializer = adapterType.getEnclosingElement().toString() + " " + varName + " = " + - "new " + adapterType; + "new " + adapterType; constructorBuilder.addStatement(initializer); serializer = varName; deserializer = varName; @@ -329,7 +240,7 @@ private static String getInitializationCodeForKnownJsonAdapterType(@NotNull Exec } else { throw new IllegalArgumentException( "@JsonAdapter value must be TypeAdapter, TypeAdapterFactory, " - + "JsonSerializer or JsonDeserializer reference."); + + "JsonSerializer or JsonDeserializer reference."); } String adapterCode = getCleanedFieldInitializer(fieldAdapterAccessor); if (isNullSafe) { @@ -354,73 +265,12 @@ private static String getAdapterForUnknownGenericType(@NotNull TypeMirror fieldT if (fieldName == null) { fieldName = TYPE_ADAPTER_FIELD_PREFIX + adapterFieldInfo.size(); String fieldInitializationCode = "gson.getAdapter(" + - getTypeTokenCode(fieldType, stagGenerator, typeVarsMap, adapterFieldInfo) + ")"; + getTypeTokenCode(fieldType, stagGenerator, typeVarsMap, adapterFieldInfo) + ")"; adapterFieldInfo.addField(fieldType, fieldName, fieldInitializationCode); } return fieldName; } - @NotNull - private static MethodSpec getWriteMethodSpec(@NotNull TypeName typeName, - @NotNull Map memberVariables, - @NotNull AdapterFieldInfo adapterFieldInfo) { - final MethodSpec.Builder builder = MethodSpec.methodBuilder("write") - .addParameter(JsonWriter.class, "writer") - .addParameter(typeName, "object") - .returns(void.class) - .addModifiers(Modifier.PUBLIC) - .addAnnotation(Override.class) - .addException(IOException.class); - - builder.beginControlFlow("if (object == null)"); - builder.addStatement("writer.nullValue()"); - builder.addStatement("return"); - builder.endControlFlow(); - builder.addStatement("writer.beginObject()"); - - for (Map.Entry element : memberVariables.entrySet()) { - FieldAccessor fieldAccessor = element.getKey(); - final String getterCode = fieldAccessor.createGetterCode(); - - String name = fieldAccessor.getJsonName(); - String variableType = element.getValue().toString(); - - boolean isPrimitive = TypeUtils.isSupportedPrimitive(variableType); - - builder.addCode("\n"); - builder.addStatement("writer.name(\"" + name + "\")"); - - if (!isPrimitive) { - builder.beginControlFlow("if (object." + getterCode + " != null) "); - } - - if (!isPrimitive) { - builder.addStatement( - adapterFieldInfo.getAdapterAccessor(element.getValue(), name) + ".write(writer, object." + - getterCode + ")"); - /* - * If the element is annotated with NonNull annotation, throw {@link IOException} if it is null. - */ - builder.endControlFlow(); - builder.beginControlFlow("else"); - if (fieldAccessor.doesRequireNotNull()) { - //throw exception in case the field is annotated as NonNull - builder.addStatement("throw new java.io.IOException(\"" + getterCode + " cannot be null\")"); - } else { - //write null value to the writer if the field is null - builder.addStatement("writer.nullValue()"); - } - builder.endControlFlow(); - } else { - builder.addStatement("writer.value(object." + getterCode + ")"); - } - } - - builder.addCode("\n"); - builder.addStatement("writer.endObject()"); - return builder.build(); - } - /** * Returns the adapter code for the known types. */ @@ -441,20 +291,20 @@ private static String getAdapterAccessor(@NotNull TypeMirror fieldType, } if (TypeUtils.isNativeArray(fieldType)) { - /* - * If the fieldType is of type native arrays such as String[] or int[] - */ + /* + * If the fieldType is of type native arrays such as String[] or int[] + */ TypeMirror arrayInnerType = TypeUtils.getArrayInnerType(fieldType); if (TypeUtils.isSupportedPrimitive(arrayInnerType.toString())) { return KnownTypeAdapterUtils.getNativePrimitiveArrayTypeAdapter(fieldType); } else { String adapterAccessor = getAdapterAccessor(arrayInnerType, stagGenerator, typeVarsMap, - adapterFieldInfo); + adapterFieldInfo); String nativeArrayInstantiator = KnownTypeAdapterUtils.getNativeArrayInstantiator(arrayInnerType); return "new " + TypeUtils.className(ArrayTypeAdapter.class) + "<" + - arrayInnerType.toString() + ">" + - "(" + adapterAccessor + ", " + nativeArrayInstantiator + ")"; + arrayInnerType.toString() + ">" + + "(" + adapterAccessor + ", " + nativeArrayInstantiator + ")"; } } else if (TypeUtils.isSupportedList(fieldType)) { DeclaredType declaredType = (DeclaredType) fieldType; @@ -464,8 +314,8 @@ private static String getAdapterAccessor(@NotNull TypeMirror fieldType, String listInstantiator = KnownTypeAdapterUtils.getListInstantiator(fieldType); String adapterCode = "new " + TypeUtils.className(KnownTypeAdapters.ListTypeAdapter.class) + "<" + param.toString() + "," + - fieldType.toString() + ">" + - "(" + paramAdapterAccessor + ", " + listInstantiator + ")"; + fieldType.toString() + ">" + + "(" + paramAdapterAccessor + ", " + listInstantiator + ")"; fieldName = TYPE_ADAPTER_FIELD_PREFIX + adapterFieldInfo.size(); adapterFieldInfo.addField(fieldType, fieldName, adapterCode); return fieldName; @@ -483,7 +333,7 @@ private static String getAdapterAccessor(@NotNull TypeMirror fieldType, keyAdapterAccessor = getAdapterAccessor(keyType, stagGenerator, typeVarsMap, adapterFieldInfo); valueAdapterAccessor = getAdapterAccessor(valueType, stagGenerator, typeVarsMap, adapterFieldInfo); arguments = "<" + keyType.toString() + ", " + valueType.toString() + ", " + - fieldType.toString() + ">"; + fieldType.toString() + ">"; } else { // If the map does not have any type arguments, use Object as type params in this case keyAdapterAccessor = "new com.vimeo.stag.KnownTypeAdapters.ObjectTypeAdapter(mGson)"; @@ -491,8 +341,8 @@ private static String getAdapterAccessor(@NotNull TypeMirror fieldType, } String adapterCode = "new " + TypeUtils.className(KnownTypeAdapters.MapTypeAdapter.class) + arguments + - "(" + keyAdapterAccessor + ", " + valueAdapterAccessor + ", " + - mapInstantiator + ")"; + "(" + keyAdapterAccessor + ", " + valueAdapterAccessor + ", " + + mapInstantiator + ")"; fieldName = TYPE_ADAPTER_FIELD_PREFIX + adapterFieldInfo.size(); adapterFieldInfo.addField(fieldType, fieldName, adapterCode); return fieldName; @@ -519,8 +369,8 @@ private static AdapterFieldInfo addAdapterFields(@NotNull StagGenerator stagGene if (constructor != null) { TypeUtils.JsonAdapterType jsonAdapterType1 = TypeUtils.getJsonAdapterType(optionalJsonAdapter); String initiazationCode = getInitializationCodeForKnownJsonAdapterType(constructor, stagGenerator, - typeVarsMap, constructorBuilder, fieldType, - jsonAdapterType1, result, fieldAccessor.isJsonAdapterNullSafe(), fieldAccessor.getJsonName()); + typeVarsMap, constructorBuilder, fieldType, + jsonAdapterType1, result, fieldAccessor.isJsonAdapterNullSafe(), fieldAccessor.getJsonName()); String fieldName = TYPE_ADAPTER_FIELD_PREFIX + result.size(); result.addFieldToAccessor(fieldAccessor.getJsonName(), fieldName, fieldType, initiazationCode); @@ -564,9 +414,9 @@ public TypeSpec createTypeAdapterSpec(@NotNull StagGenerator stagGenerator) { String className = FileGenUtils.unescapeEscapedString(mInfo.getTypeAdapterClassName()); TypeSpec.Builder adapterBuilder = TypeSpec.classBuilder(className) .addAnnotation(AnnotationSpec.builder(SuppressWarnings.class) - .addMember("value", "\"unchecked\"") - .addMember("value", "\"rawtypes\"") - .build()) + .addMember("value", "\"unchecked\"") + .addMember("value", "\"rawtypes\"") + .build()) .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .superclass(ParameterizedTypeName.get(ClassName.get(TypeAdapter.class), typeVariableName)); @@ -605,14 +455,12 @@ public TypeSpec createTypeAdapterSpec(@NotNull StagGenerator stagGenerator) { AdapterFieldInfo adapterFieldInfo = addAdapterFields(stagGenerator, constructorBuilder, memberVariables, typeVarsMap); - - MethodSpec writeMethod = getWriteMethodSpec(typeVariableName, memberVariables, adapterFieldInfo); - MethodSpec readMethod = getReadMethodSpec(typeVariableName, memberVariables, adapterFieldInfo); + MethodSpec writeMethod = WriteSpecGenerator.getWriteMethodSpec(typeVariableName, memberVariables, adapterFieldInfo, mEnableSerializeNulls); + MethodSpec readMethod = ReadSpecGenerator.getReadMethodSpec(typeVariableName, memberVariables, adapterFieldInfo); adapterBuilder.addField(Gson.class, "mGson", Modifier.FINAL, Modifier.PRIVATE); constructorBuilder.addStatement("this.mGson = gson"); - for (Map.Entry fieldInfo : adapterFieldInfo.mTypeTokenAccessorFields.entrySet()) { String originalFieldName = FileGenUtils.unescapeEscapedString(fieldInfo.getValue().accessorVariable); TypeName typeName = getTypeTokenFieldTypeName(fieldInfo.getValue().type); @@ -642,9 +490,12 @@ public TypeSpec createTypeAdapterSpec(@NotNull StagGenerator stagGenerator) { private static class FieldInfo { - @NotNull final TypeMirror type; - @NotNull final String initializationCode; - @NotNull final String accessorVariable; + @NotNull + final TypeMirror type; + @NotNull + final String initializationCode; + @NotNull + final String accessorVariable; FieldInfo(@NotNull TypeMirror type, @NotNull String initializationCode, @NotNull String accessorVariable) { this.type = type; @@ -653,20 +504,20 @@ private static class FieldInfo { } } - private static class AdapterFieldInfo { - - //Type.toString -> Accessor Map - @NotNull - private final Map mAdapterAccessor; + public static class AdapterFieldInfo { //FieldName -> Accessor Map - @NotNull final Map mFieldAdapterAccessor; - + @NotNull + final Map mFieldAdapterAccessor; //Type.toString -> Accessor Map - @NotNull final Map mAdapterFields; - + @NotNull + final Map mAdapterFields; //Type.toString -> Type Token Accessor Map - @NotNull final Map mTypeTokenAccessorFields; + @NotNull + final Map mTypeTokenAccessorFields; + //Type.toString -> Accessor Map + @NotNull + private final Map mAdapterAccessor; AdapterFieldInfo(int capacity) { mAdapterFields = new LinkedHashMap<>(capacity); @@ -675,7 +526,7 @@ private static class AdapterFieldInfo { mTypeTokenAccessorFields = new LinkedHashMap<>(); } - String getAdapterAccessor(@NotNull TypeMirror typeMirror, @NotNull String fieldName) { + public String getAdapterAccessor(@NotNull TypeMirror typeMirror, @NotNull String fieldName) { FieldInfo adapterAccessor = mFieldAdapterAccessor.get(fieldName); return adapterAccessor != null ? adapterAccessor.accessorVariable : mAdapterAccessor.get(typeMirror.toString()); } diff --git a/stag-library-compiler/src/main/java/com/vimeo/stag/processor/generators/typeadapter/ReadSpecGenerator.java b/stag-library-compiler/src/main/java/com/vimeo/stag/processor/generators/typeadapter/ReadSpecGenerator.java new file mode 100644 index 00000000..a35c74e2 --- /dev/null +++ b/stag-library-compiler/src/main/java/com/vimeo/stag/processor/generators/typeadapter/ReadSpecGenerator.java @@ -0,0 +1,110 @@ +package com.vimeo.stag.processor.generators.typeadapter; + +import com.google.gson.stream.JsonReader; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.TypeName; +import com.vimeo.stag.processor.generators.TypeAdapterGenerator; +import com.vimeo.stag.processor.generators.model.accessor.FieldAccessor; +import com.vimeo.stag.processor.utils.TypeUtils; + +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import javax.lang.model.element.Modifier; +import javax.lang.model.type.TypeMirror; + +public class ReadSpecGenerator { + @NotNull + public static MethodSpec getReadMethodSpec(@NotNull TypeName typeName, + @NotNull Map elements, + @NotNull TypeAdapterGenerator.AdapterFieldInfo adapterFieldInfo) { + MethodSpec.Builder builder = MethodSpec.methodBuilder("read") + .addParameter(JsonReader.class, "reader") + .returns(typeName) + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override.class) + .addException(IOException.class); + + builder.addStatement("com.google.gson.stream.JsonToken peek = reader.peek()"); + + builder.beginControlFlow("if (com.google.gson.stream.JsonToken.NULL == peek)"); + builder.addStatement("reader.nextNull()"); + builder.addStatement("return null"); + builder.endControlFlow(); + + + builder.beginControlFlow("if (com.google.gson.stream.JsonToken.BEGIN_OBJECT != peek)"); + builder.addStatement("reader.skipValue()"); + builder.addStatement("return null"); + builder.endControlFlow(); + + builder.addStatement("reader.beginObject()"); + builder.addStatement(typeName + " object = new " + typeName + "()"); + + builder.beginControlFlow("while (reader.hasNext())"); + builder.addStatement("String name = reader.nextName()"); + builder.beginControlFlow("switch (name)"); + + + final List nonNullFields = new ArrayList<>(); + + for (Map.Entry element : elements.entrySet()) { + final FieldAccessor fieldAccessor = element.getKey(); + String name = fieldAccessor.getJsonName(); + + final TypeMirror elementValue = element.getValue(); + + builder.addCode("case \"" + name + "\":\n"); + + String[] alternateJsonNames = fieldAccessor.getAlternateJsonNames(); + if (alternateJsonNames != null && alternateJsonNames.length > 0) { + for (String alternateJsonName : alternateJsonNames) { + builder.addCode("case \"" + alternateJsonName + "\":\n"); + } + } + + String variableType = element.getValue().toString(); + boolean isPrimitive = TypeUtils.isSupportedPrimitive(variableType); + + if (isPrimitive) { + builder.addStatement("\tobject." + + fieldAccessor.createSetterCode(adapterFieldInfo.getAdapterAccessor(elementValue, name) + + ".read(reader, object." + fieldAccessor.createGetterCode() + ")")); + + } else { + builder.addStatement("\tobject." + fieldAccessor.createSetterCode(adapterFieldInfo.getAdapterAccessor(elementValue, name) + + ".read(reader)")); + } + + + builder.addStatement("\tbreak"); + if (fieldAccessor.doesRequireNotNull()) { + if (!TypeUtils.isSupportedPrimitive(elementValue.toString())) { + nonNullFields.add(fieldAccessor); + } + } + } + + builder.addCode("default:\n"); + builder.addStatement("reader.skipValue()"); + builder.addStatement("break"); + builder.endControlFlow(); + builder.endControlFlow(); + + builder.addStatement("reader.endObject()"); + + for (FieldAccessor nonNullField : nonNullFields) { + builder.beginControlFlow("if (object." + nonNullField.createGetterCode() + " == null)"); + builder.addStatement("throw new java.io.IOException(\"" + nonNullField.createGetterCode() + " cannot be null\")"); + builder.endControlFlow(); + } + + builder.addStatement("return object"); + + return builder.build(); + } +} diff --git a/stag-library-compiler/src/main/java/com/vimeo/stag/processor/generators/typeadapter/WriteSpecGenerator.java b/stag-library-compiler/src/main/java/com/vimeo/stag/processor/generators/typeadapter/WriteSpecGenerator.java new file mode 100644 index 00000000..4edff787 --- /dev/null +++ b/stag-library-compiler/src/main/java/com/vimeo/stag/processor/generators/typeadapter/WriteSpecGenerator.java @@ -0,0 +1,113 @@ +package com.vimeo.stag.processor.generators.typeadapter; + +import com.google.gson.stream.JsonWriter; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.TypeName; +import com.vimeo.stag.processor.generators.TypeAdapterGenerator; +import com.vimeo.stag.processor.generators.model.accessor.FieldAccessor; +import com.vimeo.stag.processor.utils.TypeUtils; + +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.util.Map; + +import javax.lang.model.element.Modifier; +import javax.lang.model.type.TypeMirror; + +public class WriteSpecGenerator { + + @NotNull + public static MethodSpec getWriteMethodSpec(@NotNull TypeName typeName, @NotNull Map memberVariables, + @NotNull TypeAdapterGenerator.AdapterFieldInfo adapterFieldInfo, boolean serializeNulls) { + final MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("write") + .addParameter(JsonWriter.class, "writer") + .addParameter(typeName, "object") + .returns(void.class) + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override.class) + .addException(IOException.class); + + methodBuilder.beginControlFlow("if (object == null)"); + methodBuilder.addStatement("writer.nullValue()"); + methodBuilder.addStatement("return"); + methodBuilder.endControlFlow(); + methodBuilder.addStatement("writer.beginObject()"); + + for (Map.Entry element : memberVariables.entrySet()) { + FieldAccessor fieldAccessor = element.getKey(); + final String getterCode = fieldAccessor.createGetterCode(); + + String name = fieldAccessor.getJsonName(); + String variableType = element.getValue().toString(); + + boolean isPrimitive = TypeUtils.isSupportedPrimitive(variableType); + if (serializeNulls) { + specForSerializedNullsEnabled(methodBuilder, element, adapterFieldInfo, fieldAccessor, getterCode, name, isPrimitive); + } else { + specForSerializedNullsDisabled(methodBuilder, element, adapterFieldInfo, fieldAccessor, getterCode, name, isPrimitive); + } + } + + methodBuilder.addCode("\n"); + methodBuilder.addStatement("writer.endObject()"); + return methodBuilder.build(); + } + + private static void specForSerializedNullsDisabled(@NotNull MethodSpec.Builder methodBuilder, @NotNull Map.Entry element, + @NotNull TypeAdapterGenerator.AdapterFieldInfo adapterFieldInfo, @NotNull FieldAccessor fieldAccessor, + @NotNull String getterCode, @NotNull String name, boolean isPrimitive) { + methodBuilder.addCode("\n"); + if (!isPrimitive) { + methodBuilder.beginControlFlow("if (object." + getterCode + " != null) "); + } + methodBuilder.addStatement("writer.name(\"" + name + "\")"); + if (!isPrimitive) { + methodBuilder.addStatement( + adapterFieldInfo.getAdapterAccessor(element.getValue(), name) + ".write(writer, object." + + getterCode + ")"); + /* + * If the element is annotated with NotNull annotation, throw {@link IOException} if it is null. + */ + if (fieldAccessor.doesRequireNotNull()) { + methodBuilder.endControlFlow(); + methodBuilder.beginControlFlow("else if (object." + getterCode + " == null)"); + methodBuilder.addStatement("throw new java.io.IOException(\"" + getterCode + + " cannot be null\")"); + } + methodBuilder.endControlFlow(); + } else { + methodBuilder.addStatement("writer.value(object." + getterCode + ")"); + } + } + + private static void specForSerializedNullsEnabled(@NotNull MethodSpec.Builder methodBuilder, @NotNull Map.Entry element, + @NotNull TypeAdapterGenerator.AdapterFieldInfo adapterFieldInfo, + @NotNull FieldAccessor fieldAccessor, @NotNull String getterCode, @NotNull String name, boolean isPrimitive) { + methodBuilder.addCode("\n"); + methodBuilder.addStatement("writer.name(\"" + name + "\")"); + if (!isPrimitive) { + methodBuilder.beginControlFlow("if (object." + getterCode + " != null) "); + } + if (!isPrimitive) { + methodBuilder.addStatement( + adapterFieldInfo.getAdapterAccessor(element.getValue(), name) + ".write(writer, object." + + getterCode + ")"); + /* + * If the element is annotated with NotNull annotation, throw {@link IOException} if it is null. + */ + methodBuilder.endControlFlow(); + methodBuilder.beginControlFlow("else"); + if (fieldAccessor.doesRequireNotNull()) { + //throw exception in case the field is annotated as NotNull + methodBuilder.addStatement("throw new java.io.IOException(\"" + getterCode + " cannot be null\")"); + } else { + //write null value to the writer if the field is null + methodBuilder.addStatement("writer.nullValue()"); + } + methodBuilder.endControlFlow(); + } else { + methodBuilder.addStatement("writer.value(object." + getterCode + ")"); + } + } +}