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 lines = new ArrayList<>(); abstract void writeToFile(File directory, Map objects) abstract void readLine(String line, Map objects) } 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 objects) { //do nothing - imports are for other objects } @Override void readLine(final String line, Map objects) { throw new Exception("Line is not forimport object: '" + line + "'") } } class EnumObject extends SchemeObject { enum Type { STRING, NUMBER, BOOLEAN } static final String SIGNATURE = "enum" final String name Type type Map values = new HashMap<>() EnumObject(String firstLine) { name = firstLine.substring(SIGNATURE.length()).trim() } @Override void writeToFile(File directory, Map 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 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()).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 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)), ENUM(null), MODEL(null), GENERIC(null) final TypeName typeName FieldType(final TypeName typeName) { this.typeName = typeName } static FieldType get(String typeString, Map objects) { switch (typeString) { case "string": return STRING case "int": return INT case "boolean": return BOOLEAN case "long": return LONG case "float": return FLOAT default: if (objects.get(typeString) instanceof EnumObject) { return ENUM } if (objects.get(typeString) != null) { return MODEL } return GENERIC } } } 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 FieldType fieldType final TypeName typeName FieldInfo(String apiName, String typeString, Map objects) { this.apiName = apiName 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 genericsSuffix = typeString.indexOf("<") > 0 ? typeString.substring(typeString.indexOf("<")) : null typeString = getTypeSimpleName(typeString); fieldType = FieldType.get(typeString, objects); if (fieldType.typeName != null) { typeName = fieldType.typeName } else if (fieldType != FieldType.GENERIC) { SchemeObject schemeObject = objects.get(typeString) if (schemeObject instanceof ImportObject) { if (genericsSuffix != null) { typeName = getTypeNameWithArguments(ClassName.bestGuess(schemeObject.fullName), genericsSuffix.substring(1), objects).value } else { typeName = ClassName.bestGuess(schemeObject.fullName) } } else { if (genericsSuffix != null) { typeName = getTypeNameWithArguments(ClassName.bestGuess(typeString), genericsSuffix.substring(1), objects) } else { typeName = ClassName.bestGuess(typeString) } } } else { // generic typeName = ClassName.bestGuess(typeString) } } TypeName resolveType(String string, Map objects) { String argumentName = getTypeSimpleName(string) SchemeObject schemeObject = objects.get(argumentName) if (schemeObject instanceof ImportObject) { return ClassName.bestGuess(schemeObject.fullName) } else { return ClassName.bestGuess(argumentName) } } Pair getTypeNameWithArguments(TypeName parentTypeName, String genericString, Map objects) { List arguments = new ArrayList<>() genericString = genericString.replace(" ", "") while (!genericString.isEmpty()) { println "proc " + genericString 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 innerArgs = getTypeNameWithArguments(innerType, genericString, objects) genericString = innerArgs.key.substring(1) arguments.add(innerArgs.value) } return new Pair(genericString, ParameterizedTypeName.get(parentTypeName, (TypeName[]) arguments.toArray())) } 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() } MethodSpec createGetter(String name) { final MethodSpec.Builder builder = MethodSpec.methodBuilder("get" + upperStartName(name)) .returns(typeName) .addModifiers(Modifier.PUBLIC) .addStatement("return " + name) if (!typeName.isPrimitive()) { builder.addAnnotation(AnnotationSpec.builder(nullable ? ClassName.bestGuess("android.support.annotation.Nullable") : ClassName.bestGuess("android.support.annotation.NonNull")) .build()); } 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) .addStatement("this." + name + " = " + name) return builder.build() } } class ClassObject extends SchemeObject { static final String SIGNATURE = "class" final String name final Map fieldsInfo = new HashMap<>() ClassObject(String firstLine) { name = firstLine.substring(SIGNATURE.length()).trim() } @Override void writeToFile(File directory, Map objects) { TypeSpec.Builder classBuilder = TypeSpec.classBuilder(name) .addModifiers(Modifier.PUBLIC) .superclass(ClassName.bestGuess("ru.touchin.templates.logansquare.LoganSquareJsonModel")) .addAnnotation(AnnotationSpec.builder(ClassName.bestGuess("com.bluelinelabs.logansquare.annotation.JsonObject")) .build()) 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) boolean first = true CodeBlock.Builder equalsStatement = CodeBlock.builder() CodeBlock.Builder hashCodeStatement = CodeBlock.builder() for (Map.Entry entry : fieldsInfo) { classBuilder.addField(entry.value.createField(entry.key)) classBuilder.addMethod(entry.value.createGetter(entry.key)) classBuilder.addMethod(entry.value.createSetter(entry.key)) if (first) { equalsStatement.add("return \$T.equals(\$L, that.\$L)", ClassName.bestGuess("ru.touchin.roboswag.core.utils.ObjectUtils"), entry.key, entry.key) hashCodeStatement.add("return \$T.hashCode(\$L", ClassName.bestGuess("ru.touchin.roboswag.core.utils.ObjectUtils"), 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(equalsMethod.addCode(equalsStatement.build()).build()) classBuilder.addMethod(hashCodeMethod.addCode(hashCodeStatement.build()).build()) JavaFile.builder("com.touchin.sberinkas", classBuilder.build()) .build().writeTo(directory); } @Override void readLine(final String line, Map objects) { String[] parts = line.split(':') String fieldName = parts[0].trim(); String apiName = parts[1].trim(); String type = parts[2].trim(); if (fieldsInfo.containsKey(fieldName)) { throw new Exception("Field of '" + name + "' already added: " + fieldName) } fieldsInfo.put(fieldName, new FieldInfo(apiName, type, 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 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 }