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 abstract class SchemeObject { abstract void writeToFile(File directory, Map objects, String appPackage) } //TODO: missable in future //TODO: move out of allvariants - too much //TODO: refactor code 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, Map objects, String appPackage) { //do nothing - imports are for other objects } } class EnumObject extends SchemeObject { enum Type { STRING, NUMBER, BOOLEAN } static final String PREFIX = "enum " final String name Type type Map values = new HashMap<>() EnumObject(String name, Map values) { this.name = name.trim() for (Map.Entry entry : values) { final apiValue = entry.value.trim() if (apiValue.isEmpty()) { throw new Exception("Name of enum is empty") } if (this.values.containsKey(entry.key)) { throw new Exception("Name '" + value + "' already added to enum") } Type type = typeOf(apiValue) 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) } this.values.put(entry.key, apiValue) } } @Override void writeToFile(File directory, Map objects, String appPackage) { 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(appPackage, 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 } } } } enum FieldType { BOOLEAN(TypeName.BOOLEAN, ClassName.get(Boolean.class), false), INT(TypeName.INT, ClassName.get(Integer.class), false), LONG(TypeName.LONG, ClassName.get(Long.class), false), FLOAT(TypeName.FLOAT, ClassName.get(Float.class), false), DOUBLE(TypeName.DOUBLE, ClassName.get(Double.class), false), STRING(ClassName.get(String.class), false), 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_MODEL(true), GENERIC(true) final TypeName primitiveTypeName final TypeName nonPrimitiveTypeName final boolean ableToInnerValidate FieldType(final boolean ableToInnerValidate) { this(null, null, ableToInnerValidate) } FieldType(final TypeName typeName, final boolean ableToInnerValidate) { this(typeName, typeName, ableToInnerValidate) } FieldType(final TypeName primitiveTypeName, final TypeName nonPrimitiveTypeName, final boolean ableToInnerValidate) { this.primitiveTypeName = primitiveTypeName this.nonPrimitiveTypeName = nonPrimitiveTypeName this.ableToInnerValidate = ableToInnerValidate } static FieldType get(String typeString, Map 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: 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).value } 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 name final String apiName boolean nullable boolean missable boolean nonEmptyCollection boolean solidCollection final FieldType fieldType final TypeName typeName FieldInfo(String name, Map parameters, Map objects) { this.name = name apiName = parameters.containsKey("apiName") ? parameters.get("apiName") : name String flagsString = parameters.get("flags"); if (flagsString != null) { List flags = Arrays.asList(flagsString.replace(" ", "").split(",")) nullable = flags.contains("nullable") missable = flags.contains("missable") nonEmptyCollection = flags.contains("non-empty") solidCollection = flags.contains("solid") } String typeString = parameters.get("type") if (typeString == null) { throw new Exception("Missed type for field: " + name) } fieldType = FieldType.get(getTypeSimpleName(typeString), objects); if (fieldType.nonPrimitiveTypeName != null) { if (fieldType == FieldType.LIST) { typeName = TypeNameUtils.resolveTypeName(typeString, objects) if (!typeName.toString().startsWith("java.util.List")) { throw new Exception("Unsupported list type '" + typeName.toString() + "' of field: " + name + ". Supports only List<*>") } } else if (fieldType == FieldType.MAP) { typeName = TypeNameUtils.resolveTypeName(typeString, objects) if (!typeName.toString().startsWith("java.util.Map") } } else { typeName = nullable ? fieldType.nonPrimitiveTypeName : fieldType.primitiveTypeName } } else if (fieldType != FieldType.GENERIC) { typeName = TypeNameUtils.resolveTypeName(typeString, objects) } else { // generic typeName = ClassName.bestGuess(typeString) } } FieldSpec createField() { 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() { 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 getterPrefix = getGetterPrefix(); 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.LIST) { builder.addStatement("return \$T.unmodifiableList(\$L)", ClassName.get(Collections.class), name) } else { builder.addStatement("return \$L", name) } return builder.build() } MethodSpec createSetter() { 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.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 addValidateStatements(MethodSpec.Builder validateMethod) { if (!nullable) { validateMethod.addStatement("validateNotNull(\$L)", name) } if (!fieldType.ableToInnerValidate) { return } if (fieldType == FieldType.GENERIC || fieldType == FieldType.IMPORTED_MODEL) { validateMethod .beginControlFlow("if (\$L instanceof \$T)", name, ClassName.bestGuess("ru.touchin.templates.ApiModel")) .addStatement("((\$T) \$L).validate()", ClassName.bestGuess("ru.touchin.templates.ApiModel"), name) .endControlFlow() return } if (nullable) { validateMethod.beginControlFlow("if (\$L != null)", name) } if (fieldType == 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 (fieldType == FieldType.MAP) { if (nonEmptyCollection) { validateMethod.addStatement("validateCollectionNotEmpty(\$L.values())", name) } validateMethod.addStatement("validateCollection(\$L.values(), CollectionValidationRule.EXCEPTION_IF_ANY_INVALID)", name) } else if (fieldType == FieldType.MODEL) { validateMethod.addStatement("\$L.validate()", name) } else { throw new Exception("Unexpected able to validate field type '" + fieldType + "' of field " + name) } if (nullable) { validateMethod.endControlFlow() } } } class ClassObject extends SchemeObject { static final String PREFIX = "class " final String name final Map info final List fields = new ArrayList<>() final List typeVariables = new ArrayList<>() TypeName superclass ClassObject(String name, Map info) { this.name = name this.info = info } void resolveFieldsAndProperties(Map objects) { final List fieldNames = new ArrayList<>() for (final Map.Entry entry : info.entrySet()) { 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.replace(" ", ""), objects) continue } if (fieldNames.contains(entry.key)) { throw new Exception("Duplicate field name: " + name) } fieldNames.add(entry.key) if (entry.value instanceof Map) { fields.add(new FieldInfo(entry.key, (Map) entry.value, objects)) } else { Map parameters = new HashMap<>() parameters.put("type", entry.value.toString()) fields.add(new FieldInfo(entry.key, parameters, objects)) } } } @Override void writeToFile(File directory, Map objects, String appPackage) { TypeSpec.Builder classBuilder = TypeSpec.classBuilder(name) .addModifiers(Modifier.PUBLIC) .addAnnotation(AnnotationSpec.builder(ClassName.bestGuess("com.bluelinelabs.logansquare.annotation.JsonObject")) .addMember("serializeNullObjects", "true") .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) .addStatement("super()") .build()) MethodSpec.Builder fullConstructorBuilder = (superclass == null) ? MethodSpec.constructorBuilder() .addModifiers(Modifier.PUBLIC) .addStatement("super()") : null 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()) .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)) 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 (FieldInfo field : fields) { classBuilder.addField(field.createField()) classBuilder.addMethod(field.createGetter()) classBuilder.addMethod(field.createSetter()) field.addValidateStatements(validateMethod) if (fullConstructorBuilder != null) { fullConstructorBuilder.addParameter(ParameterSpec.builder(field.typeName, field.name, Modifier.FINAL) .addAnnotation(field.nullable ? ClassName.bestGuess("android.support.annotation.Nullable") : ClassName.bestGuess("android.support.annotation.NonNull")) .build()) if (field.fieldType == FieldType.LIST) { fullConstructorBuilder.addStatement("this.\$L = \$T.unmodifiableList(\$L)", field.name, ClassName.get(Collections.class), field.name) } else if (field.fieldType == FieldType.MAP) { fullConstructorBuilder.addStatement("this.\$L = \$T.unmodifiableMap(\$L)", field.name, ClassName.get(Collections.class), field.name) } else { fullConstructorBuilder.addStatement("this.\$L = \$L", field.name, field.name) } } if (first) { if (superclass == null) { hashCodeStatement.add("return \$T.hashCode(\$L", ClassName.bestGuess("ru.touchin.roboswag.core.utils.ObjectUtils"), field.name) equalsStatement.add("return \$T.equals(\$L, that.\$L)", ClassName.bestGuess("ru.touchin.roboswag.core.utils.ObjectUtils"), field.name, field.name) } else { hashCodeStatement.add("return \$T.hashCode(super.hashCode(), \$L", ClassName.bestGuess("ru.touchin.roboswag.core.utils.ObjectUtils"), field.name) equalsStatement.add("return super.equals(that) && \$T.equals(\$L, that.\$L)", ClassName.bestGuess("ru.touchin.roboswag.core.utils.ObjectUtils"), field.name, field.name) } } else { if (field.fieldType == FieldType.MAP) { equalsStatement.add("\n\t\t&& \$T.isMapsEquals(\$L, that.\$L)", ClassName.bestGuess("ru.touchin.roboswag.core.utils.ObjectUtils"), field.name, field.name) } else if (field.fieldType == FieldType.LIST) { equalsStatement.add("\n\t\t&& \$T.isCollectionsEquals(\$L, that.\$L)", ClassName.bestGuess("ru.touchin.roboswag.core.utils.ObjectUtils"), field.name, field.name) } else { equalsStatement.add("\n\t\t&& \$T.equals(\$L, that.\$L)", ClassName.bestGuess("ru.touchin.roboswag.core.utils.ObjectUtils"), field.name, field.name) } hashCodeStatement.add(", \$L", field.name) } first = false } equalsStatement.add(";\n") hashCodeStatement.add(");\n") classBuilder.addMethod(validateMethod.build()) classBuilder.addMethod(equalsMethod.addCode(equalsStatement.build()).build()) classBuilder.addMethod(hashCodeMethod.addCode(hashCodeStatement.build()).build()) if (fullConstructorBuilder != null) { classBuilder.addMethod(fullConstructorBuilder.build()) } println "!!!!!" + appPackage JavaFile.builder(appPackage, classBuilder.build()) .indent(" ") .build().writeTo(directory); } } class FileUtils { static void purgeDirectory(File dir) { for (File file : dir.listFiles()) { if (file.isDirectory()) purgeDirectory(file); file.delete(); } } } android.applicationVariants.all { variant -> File generatedModels = new File("${project.buildDir}/generated/source/api/model/${variant.dirName}") String modelsPackage = android.extensions.findByName("apiGeneratorModelsPackage") String schemePath = android.extensions.findByName("apiGeneratorSchemePath") if (modelsPackage == null) { modelsPackage = android.defaultConfig.applicationId + '.logic.api.model' } if (schemePath == null) { return } File schemeFile = new File(schemePath) if (!schemeFile.exists()) { schemeFile = new File("${project.projectDir}", schemePath) } def apiModelsGenerationTask = tasks.create("apiModelsGeneration${variant.name}") << { Yaml yaml = new Yaml(); Map 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) { for (final Map.Entry 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) } } } else { throw new Exception("Yaml file '" + fileName + "' is invalid") } } FileUtils.purgeDirectory(generatedModels) for (SchemeObject schemeObject : schemeObjects.values()) { if (schemeObject instanceof ClassObject) { schemeObject.resolveFieldsAndProperties(schemeObjects) } schemeObject.writeToFile(generatedModels, schemeObjects, modelsPackage) } } apiModelsGenerationTask.description = 'Generates API models' variant.registerJavaGeneratingTask apiModelsGenerationTask, generatedModels }