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) } //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 objects) { //do nothing - imports are for other objects } @Override void readLine(final String line, Map 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 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()).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 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 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 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 getTypeNameWithArguments(TypeName parentTypeName, String genericString, Map objects) { List 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 innerArgs = getTypeNameWithArguments(innerType, genericString, objects) genericString = innerArgs.key.substring(1) arguments.add(innerArgs.value) } return new Pair(genericString, ParameterizedTypeName.get(parentTypeName, (TypeName[]) arguments.toArray())) } static TypeName resolveTypeName(String typeString, Map 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 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 fieldsInfo = new HashMap<>() final List typeVariables = new ArrayList<>() TypeName superclass 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) .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 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 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 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 }