BuildScripts/gradle/jsonModelsGeneration.gradle

932 lines
40 KiB
Groovy

/*
* Copyright (c) 2017 Touch Instinct
*
* This file is part of RoboSwag library.
*
* 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.
*
*/
apply plugin: 'com.android.application'
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'org.yaml:snakeyaml:1.8'
classpath 'com.squareup:javapoet:1.8.0'
}
}
import com.squareup.javapoet.*
import javafx.util.Pair
import org.yaml.snakeyaml.Yaml
import javax.lang.model.element.Modifier
//TODO: missable in future
//TODO: NUMBER/BOOLEAN enums in future
//TODO: maybe save md5-hashes to check if files/scheme changed
class Types {
static final TypeName LOGAN_SQUARE_ENUM = ClassName.bestGuess("ru.touchin.templates.logansquare.LoganSquareEnum")
static final TypeName LOGAN_SQUARE_ENUM_CONVERTER = ClassName.bestGuess("ru.touchin.templates.logansquare.LoganSquareEnumConverter")
static final TypeName NULLABLE = ClassName.bestGuess("android.support.annotation.Nullable")
static final TypeName NON_NULL = ClassName.bestGuess("android.support.annotation.NonNull")
static final TypeName JSON_OBJECT = ClassName.bestGuess("com.bluelinelabs.logansquare.annotation.JsonObject")
static final TypeName JSON_FIELD = ClassName.bestGuess("com.bluelinelabs.logansquare.annotation.JsonField")
static final TypeName COLLECTIONS = ClassName.get(Collections.class)
static final TypeName API_MODEL = ClassName.bestGuess("ru.touchin.templates.ApiModel")
static final TypeName LOGAN_SQUARE_JSON_MODEL = ClassName.bestGuess("ru.touchin.templates.logansquare.LoganSquareJsonModel")
static final TypeName OBJECT_UTILS = ClassName.bestGuess("ru.touchin.roboswag.core.utils.ObjectUtils")
static final TypeName OBJECT_OUTPUT_STREAM = ClassName.bestGuess("java.io.ObjectOutputStream")
static final TypeName OBJECT_INPUT_STREAM = ClassName.bestGuess("java.io.ObjectInputStream")
}
/**
* Abstract object of scheme for generation. Includes:
* - models of objects (classes);
* - enums;
* - imports to resolve external types usages.
*/
abstract class SchemeObject {
/**
* Override to write scheme object code into *.java file.
* @param directory Directory to store *.java file;
* @param objects Other objects to resolve types etc.;
* @param packageName Package name of models/enums classes.
*/
abstract void writeToFile(File directory, Map<String, SchemeObject> objects, String packageName)
}
/**
* Object that's describing import of external type which is not generating by generator.
*
* Registerer it in scheme file like:
*
* imports:
* - android.support.v4.util.Pair
* - java.util.Date
*/
class ImportObject extends SchemeObject {
static final String GROUP_NAME = "imports"
final String name
final String fullName
ImportObject(String value) {
fullName = value.trim()
name = fullName.substring(fullName.lastIndexOf('.') + 1)
}
@Override
void writeToFile(final File directory, final Map<String, SchemeObject> objects, final String packageName) {
//do nothing - imports are only to resolve external types in other models
}
}
/**
* Object that's describing enum model. Where enum values are associating with values storing in JSON.
* Associating JSON values could be of types: string, boolean and int/long (numbers).
*
* Registerer it in scheme file like:
*
* enum MyEnum:
* VALUE_ONE: value_one
* VALUE_TWO: value_two
*/
class EnumObject extends SchemeObject {
enum Type {
STRING("\$S"),
NUMBER("\$LL"),
BOOLEAN("\$L")
final String format
Type(final String format) {
this.format = format
}
}
static final String PREFIX = "enum "
static Type typeOf(final String jsonValue) {
if (jsonValue.equals("true") || jsonValue.equals("false")) {
//TODO: BOOLEAN in future
return Type.STRING
}
try {
Integer.parseInt(jsonValue)
//TODO: NUMBER in future
return Type.STRING
} catch (final NumberFormatException ignored) {
return Type.STRING
}
}
final String name
Type type
final Map<String, Object> values = new HashMap<>()
EnumObject(final String enumName, final Map<String, String> jsonValues) {
this.name = enumName.trim()
if (this.name.isEmpty()) {
throw new Exception("Name of enum is empty")
}
for (final Map.Entry<String, String> entry : jsonValues) {
final enumValue = entry.key.trim()
final jsonValue = entry.value.trim()
if (jsonValue.isEmpty() || enumValue.isEmpty()) {
throw new Exception("Value of enum is empty")
}
if (this.values.containsKey(enumValue)) {
throw new Exception("Value '" + enumValue + "' already registered into enum")
}
final Type type = typeOf(jsonValue)
if (this.type == null) {
this.type = type
} else if (this.type != type) {
throw new Exception("Type of value '" + jsonValues + "' conflicts with previous value type: " + this.type)
}
this.values.put(enumValue, jsonValue)
}
}
@Override
void writeToFile(final File directory, final Map<String, SchemeObject> objects, final String packageName) {
final TypeSpec.Builder enumBuilder = TypeSpec.enumBuilder(name).addModifiers(Modifier.PUBLIC)
.addSuperinterface(Types.LOGAN_SQUARE_ENUM)
.addField(FieldSpec.builder(ClassName.get(String.class), "valueName", Modifier.PRIVATE, Modifier.FINAL)
.addAnnotation(Types.NON_NULL)
.build())
.addMethod(MethodSpec.constructorBuilder()
.addParameter(ClassName.get(String.class), "valueName", Modifier.FINAL)
.addStatement("this.valueName = valueName")
.build())
.addMethod(MethodSpec.methodBuilder("getValueName")
.returns(ClassName.get(String.class))
.addModifiers(Modifier.PUBLIC)
.addAnnotation(ClassName.get(Override.class))
.addAnnotation(Types.NON_NULL)
.addStatement("return valueName")
.build())
.addType(TypeSpec.classBuilder("LoganSquareConverter").addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.superclass(ParameterizedTypeName.get(Types.LOGAN_SQUARE_ENUM_CONVERTER, ClassName.bestGuess(name)))
.addMethod(MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC)
.addStatement("super(values())")
.build())
.build())
for (final Map.Entry<String, Object> enumValue : values) {
enumBuilder.addEnumConstant(enumValue.key, TypeSpec.anonymousClassBuilder(type.format, enumValue.value).build())
}
JavaFile.builder(packageName, enumBuilder.build())
.indent(" ")
.build()
.writeTo(directory)
}
}
/**
* Type of field of class.
*/
enum FieldType {
BOOLEAN(TypeName.BOOLEAN, ClassName.get(Boolean.class), false, "writeBoolean", "readBoolean"),
INT(TypeName.INT, ClassName.get(Integer.class), false, "writeInt", "readInt"),
LONG(TypeName.LONG, ClassName.get(Long.class), false, "writeLong", "readLong"),
FLOAT(TypeName.FLOAT, ClassName.get(Float.class), false, "writeFloat", "readFloat"),
DOUBLE(TypeName.DOUBLE, ClassName.get(Double.class), false, "writeDouble", "readDouble"),
STRING(ClassName.get(String.class), false, "writeUTF", "readUTF"),
LIST(ClassName.get(List.class), true),
MAP(ClassName.get(Map.class), true),
DATE_TIME(ClassName.bestGuess("org.joda.time.DateTime"), false),
ENUM(false),
MODEL(true),
IMPORTED_CLASS(true),
TYPE_ARGUMENT(true)
// primitive type for using if use in Non-Null case
final TypeName primitiveTypeName
final TypeName nonPrimitiveTypeName
// flag to check if such type could be validate via ApiModel class methods in some way
final boolean ableToInnerValidate
final String serializationMethodName
final String deserializationMethodName
FieldType(final boolean ableToInnerValidate) {
this(null, null, ableToInnerValidate, null, null)
}
FieldType(final TypeName typeName, final boolean ableToInnerValidate) {
this(typeName, typeName, ableToInnerValidate, null, null)
}
FieldType(final TypeName typeName, final boolean ableToInnerValidate,
final String serializationMethodName, final String deserializationMethodName) {
this(typeName, typeName, ableToInnerValidate, serializationMethodName, deserializationMethodName)
}
FieldType(final TypeName primitiveTypeName, final TypeName nonPrimitiveTypeName, final boolean ableToInnerValidate,
final String serializationMethodName, final String deserializationMethodName) {
this.primitiveTypeName = primitiveTypeName
this.nonPrimitiveTypeName = nonPrimitiveTypeName
this.ableToInnerValidate = ableToInnerValidate
this.serializationMethodName = serializationMethodName
this.deserializationMethodName = deserializationMethodName
}
/**
* Returning type of field by string of type.
* @param typeString String to get type from;
* @param objects Map of registered objects to check if such type is object from that map;
* @return Typeof field.
*/
static FieldType get(final String typeString, final Map<String, SchemeObject> objects) {
switch (typeString) {
case "string": case "String": return STRING
case "List": case "Collection": case "LinkedList": case "ArrayList": return LIST
case "Map": case "HashMap": case "TreeMap": case "LinkedHashMap": return MAP
case "int": case "Integer": return INT
case "boolean": case "Boolean": return BOOLEAN
case "long": case "Long": return LONG
case "float": case "Float": return FLOAT
case "double": case "Double": return DOUBLE
case "date": case "datetime": case "DateTime": return DATE_TIME
default:
final SchemeObject object = objects.get(typeString)
if (object instanceof ImportObject) {
return IMPORTED_CLASS
}
if (object instanceof EnumObject) {
return ENUM
}
if (object instanceof ClassObject) {
return MODEL
}
return TYPE_ARGUMENT
}
}
}
class TypeNameUtils {
/**
* Returns type arguments part from full string that represents type.
* Sample: 'List<String>' -> '<String>' or 'Map<Integer, String>' -> '<Integer,String>'.
* @param fullTypeString Full string represents type. E.g. 'Map<String, String>';
* @return Type arguments part of string. Like '<String,String>'
*/
private static String extractTypeArgumentsString(final String fullTypeString) {
final int startOfTypeArguments = fullTypeString.indexOf("<");
return startOfTypeArguments > 0 ? fullTypeString.substring(startOfTypeArguments).replace(" ", "") : null
}
/**
* Returns base type name without package and type arguments.
* Sample: 'java.lang.List<String>' -> 'List'
* @param fullTypeString Full type name string;
* @return Base type name.
*/
static String extractBaseTypeString(final String fullTypeString) {
String result = fullTypeString.replace(" ", "")
if (result.indexOf('<') > 0) {
result = result.substring(0, result.indexOf('<'))
}
if (result.indexOf('.') > 0) {
result = result.substring(result.lastIndexOf('.') + 1)
}
return result
}
/**
* Resolving type name of base type from string.
* Sample: 'List<String>' -> 'java.util.List' typeName.
* @param typeString String represents type;
* @param objects Objects to resolve some external imported classes;
* @return Type name of base type.
*/
private static TypeName resolveBaseTypeName(final String typeString, final Map<String, SchemeObject> objects) {
final String baseTypeString = extractBaseTypeString(typeString)
final SchemeObject associatedObject = objects.get(baseTypeString)
if (associatedObject instanceof ImportObject) {
return ClassName.bestGuess(associatedObject.fullName)
}
return ClassName.bestGuess(baseTypeString)
}
private static Pair<String, TypeName> getTypeNameWithArguments(final TypeName parentTypeName, String typeArgumentsString,
final Map<String, SchemeObject> objects) {
final List<TypeName> typeArguments = new ArrayList<>()
while (!typeArgumentsString.isEmpty()) {
final int nextComma = typeArgumentsString.indexOf(',')
final int nextLeft = typeArgumentsString.indexOf('<')
final int nextRight = typeArgumentsString.indexOf('>')
if (nextLeft == 0) {
throw new Exception("Unexpected symbol '<'")
}
if (nextRight == -1) {
throw new Exception("Missed symbol '>'")
}
if (nextRight == 0) {
throw new Exception("Argument missed")
}
if (nextComma > 0 && nextComma < nextRight && (nextLeft == -1 || nextComma < nextLeft)) {
// when there are several types divided by comma and current have no arguments
// Samples: String, Integer>; String, List<Integer>>
typeArguments.add(resolveBaseTypeName(typeArgumentsString.substring(0, nextComma), objects))
typeArgumentsString = typeArgumentsString.substring(nextComma + 1)
continue
}
if (nextLeft == -1 || nextRight < nextLeft) {
// when there it is last argument in scope
// Samples: String>; String>, Integer>; String>>
typeArguments.add(resolveBaseTypeName(typeArgumentsString.substring(0, nextRight), objects))
// stop parsing if there is last argument
typeArgumentsString = nextRight < typeArgumentsString.length() - 1 ? typeArgumentsString.substring(nextRight + 1) : ""
break
}
// when it is element with type arguments
// Sample: List<String>>
final TypeName baseTypeName = resolveBaseTypeName(typeArgumentsString.substring(0, nextLeft), objects)
if (typeArgumentsString.length() < nextLeft + 2) {
throw new Exception("No data after '<'")
}
typeArgumentsString = typeArgumentsString.substring(nextLeft + 1)
final Pair<String, TypeName> innerArgs = getTypeNameWithArguments(baseTypeName, typeArgumentsString, objects)
typeArgumentsString = innerArgs.key.substring(1)
typeArguments.add(innerArgs.value)
}
return new Pair<String, TypeName>(typeArgumentsString, ParameterizedTypeName.get(parentTypeName, (TypeName[]) typeArguments.toArray()))
}
/**
* Resolves TypeName from raw type string.
* @param typeString String describes type. E.g. 'java.lang.List<String>'
* @param objects Map of registered objects.
* @return Resolved TypeName.
*/
static TypeName resolveTypeName(final String typeString, final Map<String, SchemeObject> objects) {
final TypeName baseTypeName = resolveBaseTypeName(typeString, objects)
final String typeArgumentsString = extractTypeArgumentsString(typeString)
if (typeArgumentsString == null) {
return baseTypeName
}
try {
final Pair<String, TypeName> result = getTypeNameWithArguments(baseTypeName, typeArgumentsString.substring(1), objects)
if (!result.key.isEmpty()) {
throw new Exception("Useless symbols '" + result.key + "'")
}
return result.getValue()
} catch (final Exception exception) {
throw new Exception("Error resolving type of '" + typeString + "' : " + exception.getMessage())
}
}
}
/**
* Represents info about field of JSON model.
* - name - is field actual name;
* - jsonName - field name association with JSON parameter name. By default equals 'name' property;
* - type - type of field;
* - nullable - 'nullable' flag, true if field could contains null and associated JSON value could be null;
* - missable - 'missable' flag, true if JSON parameter associated with field could be missed in JSON object;
* - nonEmptyCollection - 'non-empty' flag, true if JSON parameter could contains collection and that collection souldn't be empty;
* - solidCollection - 'solid' flag, true if JSON parameter could contains collection and that collection can't contains any invalid element.
*/
class FieldInfo {
private static final String DEFAULT_GETTER_PREFIX = "get"
private static upperStartName(final String name) {
if (name.isEmpty()) {
throw new Exception("Empty name of field")
}
if (name.length() == 1) {
return name.charAt(0).toUpperCase()
}
return name.charAt(0).toUpperCase().toString() + name.substring(1)
}
private static boolean checkNameStartsWithPrefix(final String name, final String prefix) {
return name.length() > prefix.length() && name.startsWith(prefix) && name.charAt(prefix.length()).isUpperCase()
}
final String name
final String jsonName
boolean nullable
boolean missable
boolean nonEmptyCollection
boolean solidCollection
final FieldType type
final TypeName typeName
FieldInfo(final String fieldName, final Map<String, String> parameters, final Map<String, SchemeObject> objects) {
this.name = fieldName.trim()
if (name.isEmpty()) {
throw new Exception("name is empty")
}
jsonName = parameters.containsKey("jsonName") ? parameters.get("jsonName") : fieldName
if (jsonName.isEmpty()) {
throw new Exception("jsonName is empty")
}
final String flagsString = parameters.get("flags")
if (flagsString != null) {
final List<String> flags = Arrays.asList(flagsString.replace(" ", "").split(","))
for (final String flag : flags) {
switch (flag) {
case "nullable":
nullable = true
break
case "missable":
missable = true
break
case "non-empty":
nonEmptyCollection = true
break
case "solid":
solidCollection = true
break
default: throw new Exception("Unexpected flag: " + flag)
}
}
}
if (parameters.get("type") == null) {
throw new Exception("Missed type")
}
final String typeString = parameters.get("type").trim()
if (typeString.isEmpty()) {
throw new Exception("Empty type")
}
type = FieldType.get(TypeNameUtils.extractBaseTypeString(typeString), objects)
if (type.nonPrimitiveTypeName != null) {
if (type == FieldType.LIST) {
typeName = TypeNameUtils.resolveTypeName(typeString, objects)
if (!typeName.toString().startsWith("java.util.List")) {
throw new Exception("Unsupported list type '" + typeName.toString() + "' of field: " + fieldName + ". Supports only List<*>")
}
} else if (type == FieldType.MAP) {
typeName = TypeNameUtils.resolveTypeName(typeString, objects)
if (!typeName.toString().startsWith("java.util.Map<String")) {
throw new Exception("Unsupported map type of field: " + fieldName + ". Supports only Map<String, *>")
}
} else {
typeName = nullable ? type.nonPrimitiveTypeName : type.primitiveTypeName
}
} else if (type != FieldType.TYPE_ARGUMENT) {
typeName = TypeNameUtils.resolveTypeName(typeString, objects)
} else {
// generic
typeName = ClassName.bestGuess(typeString)
}
}
FieldSpec generateFieldCode() {
return FieldSpec.builder(typeName, name, Modifier.PRIVATE)
.addAnnotation(AnnotationSpec.builder(Types.JSON_FIELD)
.addMember("name", "\$S", jsonName)
.build())
.build()
}
private String getGetterPrefix() {
if (type != FieldType.BOOLEAN) {
return DEFAULT_GETTER_PREFIX
}
if (checkNameStartsWithPrefix(name, "is")) {
return "is"
} else if (checkNameStartsWithPrefix(name, "has")) {
return "has"
} else if (checkNameStartsWithPrefix(name, "have")) {
return "have"
}
return DEFAULT_GETTER_PREFIX
}
MethodSpec generateGetterCode() {
final String getterPrefix = getGetterPrefix()
final MethodSpec.Builder builder = MethodSpec.methodBuilder(getterPrefix.equals(DEFAULT_GETTER_PREFIX)
? getterPrefix + upperStartName(name)
: getterPrefix + upperStartName(name.substring(getterPrefix.length())))
.addModifiers(Modifier.PUBLIC)
.returns(typeName)
if (!typeName.isPrimitive()) {
builder.addAnnotation(AnnotationSpec.builder(nullable ? Types.NULLABLE : Types.NON_NULL).build())
}
if (type == FieldType.MAP) {
builder.addStatement("return \$T.unmodifiableMap(\$L)", Types.COLLECTIONS, name)
} else if (type == FieldType.LIST) {
builder.addStatement("return \$T.unmodifiableList(\$L)", Types.COLLECTIONS, name)
} else {
builder.addStatement("return \$L", name)
}
return builder.build()
}
MethodSpec generateSetterCode() {
final ParameterSpec.Builder parameterBuilder = ParameterSpec.builder(typeName, name, Modifier.FINAL)
if (!typeName.isPrimitive()) {
parameterBuilder.addAnnotation(AnnotationSpec.builder(nullable ? Types.NULLABLE : Types.NON_NULL).build())
}
final MethodSpec.Builder builder = MethodSpec.methodBuilder("set" + upperStartName(name))
.addModifiers(Modifier.PUBLIC)
.addParameter(parameterBuilder.build())
if (type == FieldType.MAP) {
builder.addStatement("this.\$L = \$T.unmodifiableMap(\$L)", name, ClassName.get(Collections.class), name)
} else if (type == FieldType.LIST) {
builder.addStatement("this.\$L = \$T.unmodifiableList(\$L)", name, ClassName.get(Collections.class), name)
} else {
builder.addStatement("this.\$L = \$L", name, name)
}
return builder.build()
}
void generateValidationCode(MethodSpec.Builder validateMethod) {
if (!nullable) {
validateMethod.addStatement("validateNotNull(\$L)", name)
}
if (!type.ableToInnerValidate) {
return
}
if (type == FieldType.TYPE_ARGUMENT || type == FieldType.IMPORTED_CLASS) {
validateMethod
.beginControlFlow("if (\$L instanceof \$T)", name, Types.API_MODEL)
.addStatement("((\$T) \$L).validate()", Types.API_MODEL, name)
.endControlFlow()
return
}
if (nullable) {
validateMethod.beginControlFlow("if (\$L != null)", name)
}
if (type == FieldType.LIST) {
if (nonEmptyCollection) {
validateMethod.addStatement("validateCollectionNotEmpty(\$L)", name)
}
if (solidCollection) {
validateMethod.addStatement("validateCollection(\$L, CollectionValidationRule.EXCEPTION_IF_ANY_INVALID)", name)
} else if (nonEmptyCollection) {
validateMethod.addStatement("validateCollection(\$L, CollectionValidationRule.EXCEPTION_IF_ALL_INVALID)", name)
} else {
validateMethod.addStatement("validateCollection(\$L, CollectionValidationRule.REMOVE_INVALID_ITEMS)", name)
}
} else if (type == FieldType.MAP) {
if (nonEmptyCollection) {
validateMethod.addStatement("validateCollectionNotEmpty(\$L.values())", name)
}
validateMethod.addStatement("validateCollection(\$L.values(), CollectionValidationRule.EXCEPTION_IF_ANY_INVALID)", name)
} else if (type == FieldType.MODEL) {
validateMethod.addStatement("\$L.validate()", name)
} else {
throw new Exception("Unexpected able to validate field type '" + type + "' of field " + name)
}
if (nullable) {
validateMethod.endControlFlow()
}
}
}
/**
* Object that's describing class model.
* Contains name and info about each field. Field parameters are describing in FieldInfo class.
* Samples:
*
* class MySimpleClass:
* id: int
* name: string
*
* class MyNonSimpleClass:
* typeArguments: TItem, TResponse
* extends: BaseResponse<TResponse>
* id:
* jsonName: _id
* type: int
* name:
* type: string
* flags: nullable
* items:
* type: List<TItem>
* flags: non-empty, solid
*/
class ClassObject extends SchemeObject {
static final String PREFIX = "class "
final String name
final Map<String, String> fieldsInfo
final List<FieldInfo> fields = new ArrayList<>()
final List<String> typeVariables = new ArrayList<>()
TypeName superclass
ClassObject(final String name, final Map<String, String> fieldsInfo) {
this.name = name.trim()
this.fieldsInfo = fieldsInfo
}
void resolveFieldsInfo(final Map<String, SchemeObject> objects) {
final Set<String> fieldNames = new HashSet<>()
for (final Map.Entry entry : fieldsInfo.entrySet()) {
if (fieldNames.contains(entry.key)) {
throw new Exception("Duplicate field name: " + name)
}
if (entry.key.equals("typeVariables")) {
for (String typeVariable : entry.value.replace(" ", "").split(",")) {
typeVariables.add(typeVariable)
}
continue
}
if (entry.key.equals("extends")) {
superclass = TypeNameUtils.resolveTypeName(entry.value, objects)
continue
}
fieldNames.add(entry.key)
try {
if (entry.value instanceof Map) {
fields.add(new FieldInfo(entry.key, (Map<String, String>) entry.value, objects))
} else {
final Map<String, String> parameters = new HashMap<>()
parameters.put("type", entry.value)
fields.add(new FieldInfo(entry.key, parameters, objects))
}
} catch (final Exception exception) {
throw new Exception("Error on parsing field '" + entry.key + "' : " + exception.getMessage())
}
}
}
@Override
void writeToFile(final File directory, final Map<String, SchemeObject> objects, final String packageName) {
final TypeSpec.Builder classBuilder = TypeSpec.classBuilder(name).addModifiers(Modifier.PUBLIC)
.addAnnotation(AnnotationSpec.builder(Types.JSON_OBJECT).addMember("serializeNullObjects", "true").build())
.superclass(superclass != null ? superclass : Types.LOGAN_SQUARE_JSON_MODEL)
// adds type variables
for (String typeVariable : typeVariables) {
classBuilder.addTypeVariable(TypeVariableName.get(typeVariable))
}
// adds default constructor
classBuilder.addMethod(MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC).addStatement("super()").build())
// creates full constructor only if it is extends from LoganSquareJsonModel,
// else we can't create constructor as parent constructor could also have parameters
final MethodSpec.Builder fullConstructorBuilder = superclass == null ?
MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC).addStatement("super()")
: null
// creates validate() method
final MethodSpec.Builder validateMethod = MethodSpec.methodBuilder("validate").addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.addException(ClassName.bestGuess("ValidationException"))
.addStatement("super.validate()")
// creates writeObject() method
final MethodSpec.Builder serializeMethod = MethodSpec.methodBuilder("writeObject").addModifiers(Modifier.PRIVATE)
.addException(ClassName.get(IOException.class))
.addParameter(ParameterSpec.builder(Types.OBJECT_OUTPUT_STREAM, "outputStream", Modifier.FINAL)
.addAnnotation(Types.NON_NULL)
.build())
// creates readObject() method
final MethodSpec.Builder deserializeMethod = MethodSpec.methodBuilder("readObject").addModifiers(Modifier.PRIVATE)
.addAnnotation(AnnotationSpec.builder(SuppressWarnings.class).addMember("value", "\$S", "unchecked").build())
.addException(ClassName.get(IOException.class))
.addException(ClassName.get(ClassNotFoundException.class))
.addParameter(ParameterSpec.builder(Types.OBJECT_INPUT_STREAM, "inputStream", Modifier.FINAL)
.addAnnotation(Types.NON_NULL)
.build())
boolean first = true
final CodeBlock.Builder equalsStatement = CodeBlock.builder()
final CodeBlock.Builder hashCodeStatement = CodeBlock.builder()
for (final FieldInfo field : fields) {
classBuilder.addField(field.generateFieldCode())
classBuilder.addMethod(field.generateGetterCode())
classBuilder.addMethod(field.generateSetterCode())
field.generateValidationCode(validateMethod)
final String serializeMethodName = (!field.nullable && field.type.serializationMethodName != null
? field.type.serializationMethodName : "writeObject");
final String deserializeMethodName = (!field.nullable && field.type.deserializationMethodName != null
? field.type.deserializationMethodName : "readObject");
serializeMethod.addStatement("outputStream.\$L(\$L)", serializeMethodName, field.name)
if (deserializeMethodName.equals("readObject")) {
deserializeMethod.addStatement("\$L = (\$T) inputStream.\$L()", field.name, field.typeName, deserializeMethodName)
} else {
deserializeMethod.addStatement("\$L = inputStream.\$L()", field.name, deserializeMethodName)
}
if (fullConstructorBuilder != null) {
fullConstructorBuilder.addParameter(ParameterSpec.builder(field.typeName, field.name, Modifier.FINAL)
.addAnnotation(field.nullable ? Types.NULLABLE : Types.NON_NULL)
.build())
if (field.type == FieldType.LIST) {
fullConstructorBuilder.addStatement("this.\$L = \$T.unmodifiableList(\$L)", field.name, Types.COLLECTIONS, field.name)
} else if (field.type == FieldType.MAP) {
fullConstructorBuilder.addStatement("this.\$L = \$T.unmodifiableMap(\$L)", field.name, Types.COLLECTIONS, field.name)
} else {
fullConstructorBuilder.addStatement("this.\$L = \$L", field.name, field.name)
}
}
if (first) {
if (superclass == null) {
hashCodeStatement.add("return \$T.hashCode(", Types.OBJECT_UTILS)
equalsStatement.add("return ", Types.OBJECT_UTILS)
} else {
hashCodeStatement.add("return \$T.hashCode(super.hashCode(), ", Types.OBJECT_UTILS)
equalsStatement.add("return super.equals(that) && ", Types.OBJECT_UTILS, field.name, field.name)
}
} else {
hashCodeStatement.add(", ")
equalsStatement.add("\n\t\t&& ")
}
if (field.type == FieldType.MAP) {
equalsStatement.add("\$T.isMapsEquals(\$L, that.\$L)", Types.OBJECT_UTILS, field.name, field.name)
} else if (field.type == FieldType.LIST) {
equalsStatement.add("\$T.isCollectionsEquals(\$L, that.\$L)", Types.OBJECT_UTILS, field.name, field.name)
} else {
equalsStatement.add("\$T.equals(\$L, that.\$L)", Types.OBJECT_UTILS, field.name, field.name)
}
hashCodeStatement.add("\$L", field.name)
first = false
}
equalsStatement.add(";\n")
hashCodeStatement.add(");\n")
// creates validate() method
classBuilder.addMethod(validateMethod.build())
// adds equals() method
classBuilder.addMethod(MethodSpec.methodBuilder("equals").addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.returns(TypeName.BOOLEAN)
.addParameter(ParameterSpec.builder(ClassName.get(Object.class), "object", Modifier.FINAL).addAnnotation(Types.NULLABLE).build())
.beginControlFlow("if (this == object)").addStatement("return true").endControlFlow()
.beginControlFlow("if (object == null || getClass() != object.getClass())").addStatement("return false").endControlFlow()
.addStatement("final \$T that = (\$T) object", ClassName.bestGuess(name), ClassName.bestGuess(name))
.addCode(equalsStatement.build()).build())
// adds hashCode() method
classBuilder.addMethod(MethodSpec.methodBuilder("hashCode").addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.returns(TypeName.INT)
.addCode(hashCodeStatement.build()).build())
// adds writeObject() method
classBuilder.addMethod(serializeMethod.build())
// adds readObject() method
classBuilder.addMethod(deserializeMethod.build())
if (fullConstructorBuilder != null) {
classBuilder.addMethod(fullConstructorBuilder.build())
}
JavaFile.builder(packageName, classBuilder.build())
.indent(" ")
.build()
.writeTo(directory)
}
}
class FileUtils {
static void purgeDirectory(final File directory) {
for (File file : directory.listFiles()) {
if (file.isDirectory()) {
purgeDirectory(file)
}
file.delete()
}
}
static void generateJsonModelsCode(final File generatedModelsDirectory, final String schemeFilePath,
final String modelsPackage, final String projectDir) {
if (schemeFilePath == null) {
return
}
File schemeFile = new File(schemeFilePath)
if (!schemeFile.exists()) {
schemeFile = new File(projectDir, schemeFilePath)
}
if (!schemeFile.exists()) {
throw new Exception("JSON models scheme file not found at '" + schemeFilePath)
}
final Yaml yaml = new Yaml()
final Map<String, SchemeObject> schemeObjects = new HashMap<>()
schemeObjects.put("Map", new ImportObject("java.util.Map"))
schemeObjects.put("List", new ImportObject("java.util.List"))
schemeObjects.put("DateTime", new ImportObject("org.joda.time.DateTime"))
for (final Object data : yaml.loadAll(new FileReader(schemeFile))) {
if (!(data instanceof Map)) {
throw new Exception("Yaml file '" + schemeFile + "' is invalid")
}
for (final Map.Entry<String, Object> entry : data.entrySet()) {
if (entry.key.equals(ImportObject.GROUP_NAME)) {
for (String importString : (Iterable) entry.value) {
final ImportObject importObject = new ImportObject(importString)
schemeObjects.put(importObject.name, importObject)
}
} else if (entry.key.startsWith(EnumObject.PREFIX)) {
final EnumObject enumObject = new EnumObject(entry.key.substring(EnumObject.PREFIX.length()), entry.value)
schemeObjects.put(enumObject.name, enumObject)
} else if (entry.key.startsWith(ClassObject.PREFIX)) {
final ClassObject classObject = new ClassObject(entry.key.substring(ClassObject.PREFIX.length()), entry.value)
schemeObjects.put(classObject.name, classObject)
} else {
throw new Exception("Unexpected scheme object: " + entry.key)
}
}
}
for (final SchemeObject schemeObject : schemeObjects.values()) {
if (schemeObject instanceof ClassObject) {
try {
schemeObject.resolveFieldsInfo(schemeObjects)
} catch (final Exception exception) {
throw new Exception("Error on parsing class '" + schemeObject.name + "' : " + exception.getMessage())
}
}
try {
schemeObject.writeToFile(generatedModelsDirectory, schemeObjects, modelsPackage)
} catch (final Exception exception) {
throw new Exception("Error on generating code for '" + schemeObject.name + "' : " + exception.getMessage())
}
}
}
}
android.applicationVariants.all {
variant ->
final File generatedModelsDirectory = new File("${project.buildDir}/generated/source/jsonModels/${variant.dirName}")
/**
* Generating Java classes describing JSON models from specific YAML scheme.
*/
def generateJsonModelsTask = tasks.create("generateJsonModels${variant.name}") << {
final List<String> jsonModelsMapping = android.extensions.findByName("jsonModelsMapping")
FileUtils.purgeDirectory(generatedModelsDirectory)
for (final String jsonMapping : jsonModelsMapping) {
final int indexOfDivider = jsonMapping.indexOf('->')
if (indexOfDivider == -1) {
FileUtils.generateJsonModelsCode(generatedModelsDirectory,
jsonMapping.trim(),
android.defaultConfig.applicationId + '.logic.model',
"${project.projectDir}")
} else {
FileUtils.generateJsonModelsCode(generatedModelsDirectory,
jsonMapping.substring(0, indexOfDivider).trim(),
jsonMapping.substring(indexOfDivider + 2).trim(),
"${project.projectDir}")
}
}
}
generateJsonModelsTask.description = 'Generates Java classes for JSON models'
variant.registerJavaGeneratingTask generateJsonModelsTask, generatedModelsDirectory
}