BuildScripts/gradle/apiGeneration.gradle

598 lines
25 KiB
Groovy

buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.squareup:javapoet:1.8.0'
}
}
import com.squareup.javapoet.*
import javafx.util.Pair
import javax.lang.model.element.Modifier
abstract class SchemeObject {
List<String> lines = new ArrayList<>();
abstract void writeToFile(File directory, Map<String, SchemeObject> objects)
abstract void readLine(String line, Map<String, SchemeObject> objects)
}
//TODO: constructor
//TODO: YAML
//TODO: datetime
//TODO: missable in future
//TODO: register all maps/collections classes and work with java typical types
class ImportObject extends SchemeObject {
static final String SIGNATURE = "import"
final String name
final String fullName
ImportObject(String firstLine) {
fullName = firstLine.substring(SIGNATURE.length()).trim()
name = fullName.substring(fullName.lastIndexOf('.') + 1)
}
@Override
void writeToFile(final File directory, Map<String, SchemeObject> objects) {
//do nothing - imports are for other objects
}
@Override
void readLine(final String line, Map<String, SchemeObject> objects) {
throw new Exception("Line is not related to import: '" + line + "'")
}
}
class EnumObject extends SchemeObject {
enum Type {
STRING, NUMBER, BOOLEAN
}
static final String SIGNATURE = "enum"
final String name
Type type
Map<String, Object> values = new HashMap<>()
EnumObject(String firstLine) {
name = firstLine.substring(SIGNATURE.length()).trim()
}
@Override
void writeToFile(File directory, Map<String, SchemeObject> objects) {
TypeSpec.Builder enumBuilder = TypeSpec.enumBuilder(name)
.addModifiers(Modifier.PUBLIC)
.addSuperinterface(ClassName.bestGuess("ru.touchin.templates.logansquare.LoganSquareEnum"))
enumBuilder.addField(FieldSpec.builder(ClassName.get(String.class), "valueName",
Modifier.PRIVATE,
Modifier.FINAL)
.addAnnotation(ClassName.bestGuess("android.support.annotation.NonNull"))
.build())
enumBuilder.addMethod(MethodSpec.constructorBuilder()
.addParameter(ClassName.get(String.class), "valueName", Modifier.FINAL)
.addStatement("this.valueName = valueName")
.build())
enumBuilder.addMethod(MethodSpec.methodBuilder("getValueName")
.returns(ClassName.get(String.class))
.addModifiers(Modifier.PUBLIC)
.addAnnotation(ClassName.get(Override.class))
.addAnnotation(ClassName.bestGuess("android.support.annotation.NonNull"))
.addStatement("return valueName")
.build())
enumBuilder.addType(TypeSpec.classBuilder("LoganSquareConverter")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.superclass(ParameterizedTypeName.get(ClassName.bestGuess("ru.touchin.templates.logansquare.LoganSquareEnumConverter"), ClassName.bestGuess(name)))
.addMethod(MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addStatement("super(values())")
.build())
.build())
for (Map.Entry<String, Object> entry : values) {
if (type == Type.STRING) {
enumBuilder.addEnumConstant(entry.key, TypeSpec.anonymousClassBuilder("\$S", entry.value).build())
} else {
enumBuilder.addEnumConstant(entry.key, TypeSpec.anonymousClassBuilder("\$L", entry.value).build())
}
}
JavaFile.builder("com.touchin.sberinkas", enumBuilder.build()).indent(" ").build().writeTo(directory);
}
Type typeOf(String value) {
if (value.equals("true") || value.equals("false")) {
return Type.BOOLEAN
} else {
try {
Integer.parseInt(value)
return Type.NUMBER
} catch (NumberFormatException ignored) {
return Type.STRING
}
}
}
@Override
void readLine(final String line, Map<String, SchemeObject> objects) {
String[] parts = line.split(':')
String name = parts[0].trim();
if (name.isEmpty()) {
throw new Exception("Name of enum is empty")
}
if (values.containsKey(name)) {
throw new Exception("Name '" + value + "' already added to enum")
}
String value = parts[1].trim();
Type type = typeOf(value)
if (this.type == null) {
this.type = type
} else if (this.type != type) {
throw new Exception("Type of value '" + value + "' conflicts with previous value type: " + this.type)
}
values.put(name, value)
}
}
enum FieldType {
BOOLEAN(TypeName.BOOLEAN), INT(TypeName.INT), LONG(TypeName.LONG), FLOAT(TypeName.FLOAT),
STRING(ClassName.get(String.class)), ARRAY(ClassName.get(List.class)), MAP(ClassName.get(Map.class)),
ENUM(null), MODEL(null), IMPORTED_MODEL(null), GENERIC(null)
final TypeName typeName
FieldType(final TypeName typeName) {
this.typeName = typeName
}
static FieldType get(String typeString, Map<String, SchemeObject> objects) {
switch (typeString) {
case "string": return STRING
case "array": return ARRAY
case "map": return MAP
case "int": return INT
case "boolean": return BOOLEAN
case "long": return LONG
case "float": return FLOAT
default:
SchemeObject object = objects.get(typeString);
if (object instanceof EnumObject) {
return ENUM
}
if (object instanceof ImportObject) {
return IMPORTED_MODEL
}
if (object instanceof ClassObject) {
return MODEL
}
return GENERIC
}
}
}
class TypeNameUtils {
static TypeName resolveType(String string, Map<String, SchemeObject> objects) {
String argumentName = FieldInfo.getTypeSimpleName(string)
SchemeObject schemeObject = objects.get(argumentName)
if (schemeObject instanceof ImportObject) {
return ClassName.bestGuess(schemeObject.fullName)
} else {
return ClassName.bestGuess(argumentName)
}
}
static Pair<String, TypeName> getTypeNameWithArguments(TypeName parentTypeName, String genericString, Map<String, SchemeObject> objects) {
List<TypeName> arguments = new ArrayList<>()
genericString = genericString.replace(" ", "")
while (!genericString.isEmpty()) {
int nextComma = genericString.indexOf(',')
int nextLeft = genericString.indexOf('<')
int nextRight = genericString.indexOf('>')
if (nextComma > 0 && nextComma < nextRight && (nextLeft == -1 || nextComma < nextLeft)) {
arguments.add(resolveType(genericString.substring(0, nextComma), objects))
genericString = genericString.substring(nextComma + 1)
continue
}
if (nextRight == -1) {
arguments.add(resolveType(genericString), objects)
break
}
if (nextLeft == -1 || nextRight < nextLeft) {
arguments.add(resolveType(genericString.substring(0, nextRight), objects))
genericString = nextRight < genericString.length() - 1 ? genericString.substring(nextRight + 1) : ""
break
}
TypeName innerType = resolveType(genericString.substring(0, nextLeft), objects)
genericString = genericString.substring(nextLeft + 1)
Pair<String, TypeName> innerArgs = getTypeNameWithArguments(innerType, genericString, objects)
genericString = innerArgs.key.substring(1)
arguments.add(innerArgs.value)
}
return new Pair<String, TypeName>(genericString, ParameterizedTypeName.get(parentTypeName, (TypeName[]) arguments.toArray()))
}
static TypeName resolveTypeName(String typeString, Map<String, SchemeObject> objects) {
String simpleName = FieldInfo.getTypeSimpleName(typeString)
String genericsSuffix = typeString.indexOf("<") > 0 ? typeString.substring(typeString.indexOf("<")) : null
SchemeObject schemeObject = objects.get(simpleName)
if (schemeObject instanceof ImportObject) {
if (genericsSuffix != null) {
return getTypeNameWithArguments(ClassName.bestGuess(schemeObject.fullName), genericsSuffix.substring(1), objects).value
} else {
return ClassName.bestGuess(schemeObject.fullName)
}
} else {
if (genericsSuffix != null) {
return getTypeNameWithArguments(ClassName.bestGuess(simpleName), genericsSuffix.substring(1), objects)
} else {
return ClassName.bestGuess(simpleName)
}
}
}
}
class FieldInfo {
static upperStartName(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)
}
static String getTypeSimpleName(String typeString) {
String result = typeString.trim()
if (result.indexOf('.') > 0) {
result = result.substring(result.lastIndexOf('.') + 1)
}
if (result.indexOf('<') > 0) {
result = result.substring(0, result.indexOf('<'))
}
return result;
}
final String apiName
final boolean nullable
final boolean required
final boolean nonEmptyCollection
final boolean solidCollection
final FieldType fieldType
final TypeName typeName
FieldInfo(String apiName, String typeString, String subTypeString, Map<String, SchemeObject> objects) {
this.apiName = apiName
solidCollection = typeString.endsWith('%')
if (solidCollection) {
typeString = typeString.substring(0, typeString.length() - 1)
}
nonEmptyCollection = typeString.endsWith('|')
if (nonEmptyCollection) {
typeString = typeString.substring(0, typeString.length() - 1)
}
required = typeString.endsWith('*')
if (required) {
typeString = typeString.substring(0, typeString.length() - 1)
}
nullable = typeString.endsWith('?')
if (nullable) {
typeString = typeString.substring(0, typeString.length() - 1)
}
String original = typeString
typeString = getTypeSimpleName(typeString);
fieldType = FieldType.get(typeString, objects);
if (fieldType.typeName != null) {
if (fieldType == FieldType.ARRAY) {
typeName = ParameterizedTypeName.get(fieldType.typeName, TypeNameUtils.resolveTypeName(subTypeString, objects))
} else if (fieldType == FieldType.MAP) {
typeName = ParameterizedTypeName.get(fieldType.typeName, ClassName.get(String.class), TypeNameUtils.resolveTypeName(subTypeString, objects))
} else {
typeName = fieldType.typeName
}
} else if (fieldType != FieldType.GENERIC) {
typeName = TypeNameUtils.resolveTypeName(original, objects)
} else {
// generic
typeName = ClassName.bestGuess(typeString)
}
}
FieldSpec createField(String name) {
return FieldSpec.builder(typeName, name, Modifier.PRIVATE)
.addAnnotation(AnnotationSpec.builder(ClassName.bestGuess("com.bluelinelabs.logansquare.annotation.JsonField"))
.addMember("name", "\$S", apiName)
.build())
.build()
}
static boolean checkNameStartsWith(String name, String prefix) {
return name.length() > prefix.length() && name.startsWith(prefix) && name.charAt(prefix.length()).isUpperCase()
}
String getGetterPrefix(String name) {
if (fieldType != FieldType.BOOLEAN) {
return "get"
}
if (checkNameStartsWith(name, "is")) {
return "is"
} else if (checkNameStartsWith(name, "has")) {
return "has"
} else if (checkNameStartsWith(name, "have")) {
return "have"
}
return "get"
}
MethodSpec createGetter(String name) {
String getterPrefix = getGetterPrefix(name);
final MethodSpec.Builder builder = MethodSpec.methodBuilder(getterPrefix.equals("get")
? getterPrefix + upperStartName(name)
: getterPrefix + upperStartName(name.substring(getterPrefix.length())))
.returns(typeName)
.addModifiers(Modifier.PUBLIC)
if (!typeName.isPrimitive()) {
builder.addAnnotation(AnnotationSpec.builder(nullable
? ClassName.bestGuess("android.support.annotation.Nullable")
: ClassName.bestGuess("android.support.annotation.NonNull"))
.build());
}
if (fieldType == FieldType.MAP) {
builder.addStatement("return \$T.unmodifiableMap(\$L)", ClassName.get(Collections.class), name)
} else if (fieldType == FieldType.ARRAY) {
builder.addStatement("return \$T.unmodifiableList(\$L)", ClassName.get(Collections.class), name)
} else {
builder.addStatement("return \$L", name)
}
return builder.build()
}
MethodSpec createSetter(String name) {
final ParameterSpec.Builder parameterBuilder = ParameterSpec.builder(typeName, name, Modifier.FINAL)
if (!typeName.isPrimitive()) {
parameterBuilder.addAnnotation(AnnotationSpec.builder(nullable
? ClassName.bestGuess("android.support.annotation.Nullable")
: ClassName.bestGuess("android.support.annotation.NonNull"))
.build());
}
final MethodSpec.Builder builder = MethodSpec.methodBuilder("set" + upperStartName(name))
.addParameter(parameterBuilder.build())
.addModifiers(Modifier.PUBLIC)
if (fieldType == FieldType.MAP) {
builder.addStatement("this.\$L = \$T.unmodifiableMap(\$L)", name, ClassName.get(Collections.class), name)
} else if (fieldType == FieldType.ARRAY) {
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 addValidateStatements(String name, MethodSpec.Builder validateMethod) {
final String prefix
if (!nullable) {
prefix = ""
validateMethod.addStatement("validateNotNull(\$L)", name)
} else {
prefix = "if(" + name + "!=null)"
}
if (fieldType == FieldType.ARRAY) {
if (nonEmptyCollection) {
validateMethod.addStatement(prefix + "validateCollectionNotEmpty(\$L)", name)
}
if (solidCollection) {
validateMethod.addStatement(prefix + "validateCollection(\$L, CollectionValidationRule.EXCEPTION_IF_ANY_INVALID)", name)
} else if (nonEmptyCollection) {
validateMethod.addStatement(prefix + "validateCollection(\$L, CollectionValidationRule.EXCEPTION_IF_ALL_INVALID)", name)
} else {
validateMethod.addStatement(prefix + "validateCollection(\$L, CollectionValidationRule.REMOVE_INVALID_ITEMS)", name)
}
} else if (fieldType == FieldType.MAP) {
if (nonEmptyCollection) {
validateMethod.addStatement(prefix + "validateCollectionNotEmpty(\$L.values())", name)
}
validateMethod.addStatement(prefix + "validateCollection(\$L.values(), CollectionValidationRule.EXCEPTION_IF_ANY_INVALID)", name)
} else if (fieldType == FieldType.MODEL) {
validateMethod.addStatement(prefix + "\$L.validate()", name)
} else if (fieldType == FieldType.GENERIC || fieldType == FieldType.IMPORTED_MODEL) {
validateMethod.addStatement("if(\$L instanceof \$T) ((\$T)\$L).validate()", name,
ClassName.bestGuess("ru.touchin.templates.ApiModel"),
ClassName.bestGuess("ru.touchin.templates.ApiModel"), name)
}
}
}
class ClassObject extends SchemeObject {
static final String SIGNATURE = "class"
final String name
final Map<String, FieldInfo> fieldsInfo = new HashMap<>()
final List<String> typeVariables = new ArrayList<>()
TypeName superclass
ClassObject(String firstLine) {
name = firstLine.substring(SIGNATURE.length()).trim()
}
@Override
void writeToFile(File directory, Map<String, SchemeObject> objects) {
TypeSpec.Builder classBuilder = TypeSpec.classBuilder(name)
.addModifiers(Modifier.PUBLIC)
.addAnnotation(AnnotationSpec.builder(ClassName.bestGuess("com.bluelinelabs.logansquare.annotation.JsonObject"))
.build())
if (superclass != null) {
classBuilder.superclass(superclass)
} else {
classBuilder.superclass(ClassName.bestGuess("ru.touchin.templates.logansquare.LoganSquareJsonModel"))
}
for (String typeVariable : typeVariables) {
classBuilder.addTypeVariable(TypeVariableName.get(typeVariable))
}
classBuilder.addMethod(MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.build())
MethodSpec.Builder equalsMethod = MethodSpec.methodBuilder("equals")
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.returns(TypeName.BOOLEAN)
.addParameter(ParameterSpec.builder(ClassName.get(Object.class), "object", Modifier.FINAL)
.addAnnotation(ClassName.bestGuess("android.support.annotation.Nullable"))
.build())
.addStatement("if (this == object) return true")
.addStatement("if (object == null || getClass() != object.getClass()) return false")
.addStatement("final \$T that = (\$T) object", ClassName.bestGuess(name), ClassName.bestGuess(name))
MethodSpec.Builder hashCodeMethod = MethodSpec.methodBuilder("hashCode")
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.returns(TypeName.INT)
MethodSpec.Builder validateMethod = MethodSpec.methodBuilder("validate")
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.addException(ClassName.bestGuess("ValidationException"))
.addStatement("super.validate()")
boolean first = true
CodeBlock.Builder equalsStatement = CodeBlock.builder()
CodeBlock.Builder hashCodeStatement = CodeBlock.builder()
for (Map.Entry<String, FieldInfo> entry : fieldsInfo) {
classBuilder.addField(entry.value.createField(entry.key))
classBuilder.addMethod(entry.value.createGetter(entry.key))
classBuilder.addMethod(entry.value.createSetter(entry.key))
entry.value.addValidateStatements(entry.key, validateMethod)
if (first) {
if (superclass == null) {
hashCodeStatement.add("return \$T.hashCode(\$L", ClassName.bestGuess("ru.touchin.roboswag.core.utils.ObjectUtils"), entry.key)
equalsStatement.add("return \$T.equals(\$L, that.\$L)", ClassName.bestGuess("ru.touchin.roboswag.core.utils.ObjectUtils"),
entry.key, entry.key)
} else {
hashCodeStatement.add("return \$T.hashCode(super.hashCode(), \$L", ClassName.bestGuess("ru.touchin.roboswag.core.utils.ObjectUtils"), entry.key)
equalsStatement.add("return super.equals(that) && \$T.equals(\$L, that.\$L)", ClassName.bestGuess("ru.touchin.roboswag.core.utils.ObjectUtils"),
entry.key, entry.key)
}
} else {
if (entry.value.fieldType == FieldType.MAP) {
equalsStatement.add("\n&& \$T.isMapsEquals(\$L, that.\$L)", ClassName.bestGuess("ru.touchin.roboswag.core.utils.ObjectUtils"),
entry.key, entry.key)
} else if (entry.value.fieldType == FieldType.ARRAY) {
equalsStatement.add("\n&& \$T.isCollectionsEquals(\$L, that.\$L)", ClassName.bestGuess("ru.touchin.roboswag.core.utils.ObjectUtils"),
entry.key, entry.key)
} else {
equalsStatement.add("\n&& \$T.equals(\$L, that.\$L)", ClassName.bestGuess("ru.touchin.roboswag.core.utils.ObjectUtils"),
entry.key, entry.key)
}
hashCodeStatement.add(", \$L", entry.key)
}
first = false
}
equalsStatement.add(";")
hashCodeStatement.add(");")
classBuilder.addMethod(validateMethod.build())
classBuilder.addMethod(equalsMethod.addCode(equalsStatement.build()).build())
classBuilder.addMethod(hashCodeMethod.addCode(hashCodeStatement.build()).build())
JavaFile.builder("com.touchin.sberinkas", classBuilder.build())
.indent(" ")
.build().writeTo(directory);
}
@Override
void readLine(final String line, Map<String, SchemeObject> objects) {
if (line.startsWith("typeVariables")) {
for (String typeVariable : line.substring("typeVariables".length()).replace(" ", "").split(",")) {
typeVariables.add(typeVariable)
}
return
}
if (line.startsWith("extends")) {
superclass = TypeNameUtils.resolveTypeName(line.substring("extends".length()).replace(" ", ""), objects)
return
}
String[] parts = line.split(':')
String fieldName = parts[0].trim();
String apiName = parts[1].trim();
String type = parts[2].trim();
String subType = parts.length > 3 ? parts[3].trim() : null;
if (fieldsInfo.containsKey(fieldName)) {
throw new Exception("Field of '" + name + "' already added: " + fieldName)
}
fieldsInfo.put(fieldName, new FieldInfo(apiName, type, subType, objects))
}
}
android.applicationVariants.all {
variant ->
File generatedModels = new File("${project.buildDir}/generated/source/models/${variant.dirName}")
File schemeFile = new File("${project.projectDir}/src/main/res/raw/scheme.txt")
def apiModelsGenerationTask = tasks.create("apiModelsGeneration${variant.name}") << {
BufferedReader reader = new BufferedReader(new FileReader(schemeFile))
String line
Map<String, SchemeObject> schemeObjects = new HashMap<>()
schemeObjects.put("List", new ImportObject("import java.util.List"))
schemeObjects.put("Map", new ImportObject("import java.util.Map"))
SchemeObject currentSchemeObject = null
while ((line = reader.readLine()) != null) {
if (line.startsWith(EnumObject.SIGNATURE)) {
currentSchemeObject = new EnumObject(line)
schemeObjects.put(currentSchemeObject.name, currentSchemeObject)
} else if (line.startsWith(ClassObject.SIGNATURE)) {
currentSchemeObject = new ClassObject(line)
schemeObjects.put(currentSchemeObject.name, currentSchemeObject)
} else if (line.startsWith(ImportObject.SIGNATURE)) {
currentSchemeObject = new ImportObject(line)
schemeObjects.put(currentSchemeObject.name, currentSchemeObject)
} else if (currentSchemeObject != null) {
currentSchemeObject.lines.add(line)
} else if (!line.trim().isEmpty()) {
throw new Exception("No objects in scheme")
}
}
for (SchemeObject schemeObject : schemeObjects.values()) {
for (String objectLine : schemeObject.lines) {
schemeObject.readLine(objectLine, schemeObjects)
}
schemeObject.writeToFile(generatedModels, schemeObjects)
}
}
apiModelsGenerationTask.description = 'Generates API models'
variant.registerJavaGeneratingTask apiModelsGenerationTask, generatedModels
}