974 lines
42 KiB
Groovy
974 lines
42 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
|
|
import java.util.Map.Entry
|
|
|
|
|
|
//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 {
|
|
|
|
enum Type {
|
|
MODEL,
|
|
ENUM,
|
|
EXTERNAL
|
|
}
|
|
|
|
static final String GROUP_NAME = "imports"
|
|
|
|
final String name
|
|
final String fullName
|
|
final Type type
|
|
|
|
ImportObject(final String value, final Type type) {
|
|
fullName = value.trim()
|
|
name = fullName.substring(fullName.lastIndexOf('.') + 1)
|
|
this.type = type
|
|
}
|
|
|
|
@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) {
|
|
switch (object.type){
|
|
case ImportObject.Type.MODEL:
|
|
return MODEL
|
|
case ImportObject.Type.ENUM:
|
|
return ENUM
|
|
case ImportObject.Type.EXTERNAL:
|
|
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 Map<String, SchemeObject> getObjectsMap(final String schemeFilePath, 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", ImportObject.Type.EXTERNAL))
|
|
schemeObjects.put("List", new ImportObject("java.util.List", ImportObject.Type.EXTERNAL))
|
|
schemeObjects.put("DateTime", new ImportObject("org.joda.time.DateTime", ImportObject.Type.EXTERNAL))
|
|
|
|
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, ImportObject.Type.EXTERNAL)
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
return schemeObjects
|
|
}
|
|
|
|
}
|
|
|
|
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)
|
|
final Map<String, Map<String, SchemeObject>> overallObjects = new HashMap<>()
|
|
for (final String jsonMapping : jsonModelsMapping) {
|
|
final int indexOfDivider = jsonMapping.indexOf('->')
|
|
final String packageName
|
|
final Map<String, SchemeObject> objects
|
|
if (indexOfDivider == -1) {
|
|
packageName = android.defaultConfig.applicationId + '.logic.model'
|
|
objects = FileUtils.getObjectsMap(jsonMapping.trim(), "${project.projectDir}")
|
|
} else {
|
|
packageName = jsonMapping.substring(indexOfDivider + 2).trim()
|
|
objects = FileUtils.getObjectsMap(jsonMapping.substring(0, indexOfDivider).trim(), "${project.projectDir}")
|
|
}
|
|
if (overallObjects.containsKey(packageName)) {
|
|
overallObjects.get(packageName).putAll(objects)
|
|
} else {
|
|
overallObjects.put(packageName, objects)
|
|
}
|
|
}
|
|
|
|
for (final Entry<String, Map<String, SchemeObject>> fileObjects : overallObjects.entrySet()) {
|
|
final String packageName = fileObjects.key
|
|
final Map<String, SchemeObject> schemeObjects = new HashMap<>(fileObjects.value)
|
|
for (final Entry<String, Map<String, SchemeObject>> externalObjects : overallObjects.entrySet()) {
|
|
if (externalObjects.key == packageName) {
|
|
continue
|
|
}
|
|
for (final SchemeObject externalObject : externalObjects.value.values()) {
|
|
if (externalObject instanceof ImportObject) {
|
|
schemeObjects.put(externalObject.name, externalObject)
|
|
} else if (externalObject instanceof EnumObject) {
|
|
schemeObjects.put(externalObject.name,
|
|
new ImportObject(externalObjects.key + '.' + externalObject.name, ImportObject.Type.ENUM))
|
|
} else if (externalObject instanceof ClassObject) {
|
|
schemeObjects.put(externalObject.name,
|
|
new ImportObject(externalObjects.key + '.' + externalObject.name, ImportObject.Type.MODEL))
|
|
}
|
|
}
|
|
}
|
|
|
|
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, packageName)
|
|
} catch (final Exception exception) {
|
|
throw new Exception("Error on generating code for '" + schemeObject.name + "' : " + exception.getMessage())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
generateJsonModelsTask.description = 'Generates Java classes for JSON models'
|
|
variant.registerJavaGeneratingTask generateJsonModelsTask, generatedModelsDirectory
|
|
} |