Merge pull request #8 from TouchInstinct/feature/api_generator
Feature/api generator
This commit is contained in:
commit
25b469178c
|
|
@ -0,0 +1,932 @@
|
|||
/*
|
||||
* 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
|
||||
}
|
||||
Loading…
Reference in New Issue