From 0d61f27543e5fe8ffa062e9851e8b485625a1ba7 Mon Sep 17 00:00:00 2001 From: Gavriil Sitnikov Date: Fri, 21 Apr 2017 02:58:09 +0300 Subject: [PATCH 01/17] api generation enum generation code --- gradle/apiGeneration.gradle | 168 ++++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 gradle/apiGeneration.gradle diff --git a/gradle/apiGeneration.gradle b/gradle/apiGeneration.gradle new file mode 100644 index 0000000..b9bef10 --- /dev/null +++ b/gradle/apiGeneration.gradle @@ -0,0 +1,168 @@ +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.squareup:javapoet:1.8.0' + } +} + +import com.squareup.javapoet.* + +import javax.lang.model.element.Modifier + +interface SchemeObject { + + void writeToFile(File directory) + + void readLine(String line) + +} + +class EnumObject implements 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) { + 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) { + enumBuilder.addEnumConstant(entry.key, TypeSpec.anonymousClassBuilder(entry.value.toString()).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 if (value.startsWith('"') && value.endsWith('"')) { + return Type.STRING + } + try { + Integer.parseInt(value) + return Type.NUMBER + } catch (NumberFormatException ignored) { + throw new Exception("Can't define type of value: " + value) + } + } + + @Override + void readLine(final String line) { + 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) + } + +} + +class ClassObject implements SchemeObject { + + static final String SIGNATURE = "class" + + final String name + + ClassObject(String firstLine) { + name = firstLine.substring(SIGNATURE.length()).trim() + } + + @Override + void writeToFile(File directory) { + + } + + @Override + void readLine(final String line) { + } + +} + +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 + List schemeObjects = new ArrayList<>() + SchemeObject currentSchemeObject = null + while ((line = reader.readLine()) != null) { + if (line.startsWith(EnumObject.SIGNATURE)) { + currentSchemeObject = new EnumObject(line) + schemeObjects.add(currentSchemeObject) + } else if (line.startsWith(ClassObject.SIGNATURE)) { + currentSchemeObject = new EnumObject(line) + schemeObjects.add(currentSchemeObject) + } else if (currentSchemeObject != null) { + currentSchemeObject.readLine(line) + } else if (!line.trim().isEmpty()) { + throw new Exception("No objects in scheme") + } + } + + for (SchemeObject schemeObject : schemeObjects) { + schemeObject.writeToFile(generatedModels) + } + } + + apiModelsGenerationTask.description = 'Generates API models' + variant.registerJavaGeneratingTask apiModelsGenerationTask, generatedModels +} \ No newline at end of file From 063262650129cf1e08a2e833dc97bc8c8f1bf72e Mon Sep 17 00:00:00 2001 From: Gavriil Sitnikov Date: Fri, 21 Apr 2017 20:05:04 +0300 Subject: [PATCH 02/17] class basic generation logic added --- gradle/apiGeneration.gradle | 232 ++++++++++++++++++++++++++++++------ 1 file changed, 197 insertions(+), 35 deletions(-) diff --git a/gradle/apiGeneration.gradle b/gradle/apiGeneration.gradle index b9bef10..ad47a60 100644 --- a/gradle/apiGeneration.gradle +++ b/gradle/apiGeneration.gradle @@ -71,7 +71,11 @@ class EnumObject implements SchemeObject { .build()) for (Map.Entry entry : values) { - enumBuilder.addEnumConstant(entry.key, TypeSpec.anonymousClassBuilder(entry.value.toString()).build()) + 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); @@ -80,14 +84,13 @@ class EnumObject implements SchemeObject { Type typeOf(String value) { if (value.equals("true") || value.equals("false")) { return Type.BOOLEAN - } else if (value.startsWith('"') && value.endsWith('"')) { - return Type.STRING - } - try { - Integer.parseInt(value) - return Type.NUMBER - } catch (NumberFormatException ignored) { - throw new Exception("Can't define type of value: " + value) + } else { + try { + Integer.parseInt(value) + return Type.NUMBER + } catch (NumberFormatException ignored) { + return Type.STRING + } } } @@ -113,11 +116,111 @@ class EnumObject implements SchemeObject { } +enum FieldType { + + BOOLEAN(TypeName.BOOLEAN), INT(TypeName.INT), LONG(TypeName.LONG), FLOAT(TypeName.FLOAT), CUSTOM(null); + + final TypeName typeName; + + FieldType(final TypeName typeName) { + this.typeName = typeName + } + + static FieldType get(String typeString) { + switch (typeString) { + case "boolean": return BOOLEAN + case "int": return INT + case "long": return LONG + case "float": return FLOAT + default: return CUSTOM + } + } + +} + +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) + } + + final String apiName + final boolean nullable + final boolean required + final FieldType fieldType + final TypeName typeName + + FieldInfo(String apiName, String typeString) { + 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) + } + fieldType = FieldType.get(typeString); + if (fieldType != FieldType.CUSTOM) { + typeName = fieldType.typeName + } else if (typeString == "string") { + typeName = ClassName.get(String.class) + } else { + 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() + } + + 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 implements SchemeObject { static final String SIGNATURE = "class" final String name + final Map fieldsInfo = new HashMap<>() ClassObject(String firstLine) { name = firstLine.substring(SIGNATURE.length()).trim() @@ -125,44 +228,103 @@ class ClassObject implements SchemeObject { @Override void writeToFile(File directory) { + 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) { + 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)) } } -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") +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}") << { + def apiModelsGenerationTask = tasks.create("apiModelsGeneration${variant.name}") << { - BufferedReader reader = new BufferedReader(new FileReader(schemeFile)) - String line - List schemeObjects = new ArrayList<>() - SchemeObject currentSchemeObject = null - while ((line = reader.readLine()) != null) { - if (line.startsWith(EnumObject.SIGNATURE)) { - currentSchemeObject = new EnumObject(line) - schemeObjects.add(currentSchemeObject) - } else if (line.startsWith(ClassObject.SIGNATURE)) { - currentSchemeObject = new EnumObject(line) - schemeObjects.add(currentSchemeObject) - } else if (currentSchemeObject != null) { - currentSchemeObject.readLine(line) - } else if (!line.trim().isEmpty()) { - throw new Exception("No objects in scheme") + BufferedReader reader = new BufferedReader(new FileReader(schemeFile)) + String line + List schemeObjects = new ArrayList<>() + SchemeObject currentSchemeObject = null + while ((line = reader.readLine()) != null) { + if (line.startsWith(EnumObject.SIGNATURE)) { + currentSchemeObject = new EnumObject(line) + schemeObjects.add(currentSchemeObject) + } else if (line.startsWith(ClassObject.SIGNATURE)) { + currentSchemeObject = new ClassObject(line) + schemeObjects.add(currentSchemeObject) + } else if (currentSchemeObject != null) { + currentSchemeObject.readLine(line) + } else if (!line.trim().isEmpty()) { + throw new Exception("No objects in scheme") + } + } + + for (SchemeObject schemeObject : schemeObjects) { + schemeObject.writeToFile(generatedModels) } } - for (SchemeObject schemeObject : schemeObjects) { - schemeObject.writeToFile(generatedModels) - } - } - - apiModelsGenerationTask.description = 'Generates API models' - variant.registerJavaGeneratingTask apiModelsGenerationTask, generatedModels + apiModelsGenerationTask.description = 'Generates API models' + variant.registerJavaGeneratingTask apiModelsGenerationTask, generatedModels } \ No newline at end of file From e833d0ccdb2b59affaceb2d298aa5f9e1cc8626b Mon Sep 17 00:00:00 2001 From: Gavriil Sitnikov Date: Tue, 25 Apr 2017 01:45:19 +0300 Subject: [PATCH 03/17] generics for fields resolved --- gradle/apiGeneration.gradle | 169 ++++++++++++++++++++++++++++++------ 1 file changed, 142 insertions(+), 27 deletions(-) diff --git a/gradle/apiGeneration.gradle b/gradle/apiGeneration.gradle index ad47a60..01d510f 100644 --- a/gradle/apiGeneration.gradle +++ b/gradle/apiGeneration.gradle @@ -8,18 +8,45 @@ buildscript { } import com.squareup.javapoet.* +import javafx.util.Pair import javax.lang.model.element.Modifier -interface SchemeObject { +abstract class SchemeObject { - void writeToFile(File directory) + List lines = new ArrayList<>(); - void readLine(String line) + abstract void writeToFile(File directory, Map objects) + + abstract void readLine(String line, Map objects) } -class EnumObject implements SchemeObject { +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 @@ -37,7 +64,7 @@ class EnumObject implements SchemeObject { } @Override - void writeToFile(File directory) { + void writeToFile(File directory, Map objects) { TypeSpec.Builder enumBuilder = TypeSpec.enumBuilder(name) .addModifiers(Modifier.PUBLIC) .addSuperinterface(ClassName.bestGuess("ru.touchin.templates.logansquare.LoganSquareEnum")) @@ -95,7 +122,7 @@ class EnumObject implements SchemeObject { } @Override - void readLine(final String line) { + void readLine(final String line, Map objects) { String[] parts = line.split(':') String name = parts[0].trim(); if (name.isEmpty()) { @@ -118,21 +145,31 @@ class EnumObject implements SchemeObject { enum FieldType { - BOOLEAN(TypeName.BOOLEAN), INT(TypeName.INT), LONG(TypeName.LONG), FLOAT(TypeName.FLOAT), CUSTOM(null); + 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; + final TypeName typeName FieldType(final TypeName typeName) { this.typeName = typeName } - static FieldType get(String typeString) { + static FieldType get(String typeString, Map objects) { switch (typeString) { - case "boolean": return BOOLEAN + case "string": return STRING case "int": return INT + case "boolean": return BOOLEAN case "long": return LONG case "float": return FLOAT - default: return CUSTOM + default: + if (objects.get(typeString) instanceof EnumObject) { + return ENUM + } + if (objects.get(typeString) != null) { + return MODEL + } + return GENERIC } } @@ -150,13 +187,24 @@ class FieldInfo { 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) { + FieldInfo(String apiName, String typeString, Map objects) { this.apiName = apiName required = typeString.endsWith('*') if (required) { @@ -166,16 +214,73 @@ class FieldInfo { if (nullable) { typeString = typeString.substring(0, typeString.length() - 1) } - fieldType = FieldType.get(typeString); - if (fieldType != FieldType.CUSTOM) { + 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 (typeString == "string") { - typeName = ClassName.get(String.class) + } 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")) @@ -215,7 +320,7 @@ class FieldInfo { } -class ClassObject implements SchemeObject { +class ClassObject extends SchemeObject { static final String SIGNATURE = "class" @@ -227,7 +332,7 @@ class ClassObject implements SchemeObject { } @Override - void writeToFile(File directory) { + void writeToFile(File directory, Map objects) { TypeSpec.Builder classBuilder = TypeSpec.classBuilder(name) .addModifiers(Modifier.PUBLIC) .superclass(ClassName.bestGuess("ru.touchin.templates.logansquare.LoganSquareJsonModel")) @@ -256,6 +361,7 @@ class ClassObject implements SchemeObject { 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)) @@ -277,11 +383,12 @@ class ClassObject implements SchemeObject { classBuilder.addMethod(equalsMethod.addCode(equalsStatement.build()).build()) classBuilder.addMethod(hashCodeMethod.addCode(hashCodeStatement.build()).build()) - JavaFile.builder("com.touchin.sberinkas", classBuilder.build()).build().writeTo(directory); + JavaFile.builder("com.touchin.sberinkas", classBuilder.build()) + .build().writeTo(directory); } @Override - void readLine(final String line) { + void readLine(final String line, Map objects) { String[] parts = line.split(':') String fieldName = parts[0].trim(); String apiName = parts[1].trim(); @@ -290,7 +397,7 @@ class ClassObject implements SchemeObject { if (fieldsInfo.containsKey(fieldName)) { throw new Exception("Field of '" + name + "' already added: " + fieldName) } - fieldsInfo.put(fieldName, new FieldInfo(apiName, type)) + fieldsInfo.put(fieldName, new FieldInfo(apiName, type, objects)) } } @@ -304,24 +411,32 @@ android.applicationVariants.all { BufferedReader reader = new BufferedReader(new FileReader(schemeFile)) String line - List schemeObjects = new ArrayList<>() + 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.add(currentSchemeObject) + schemeObjects.put(currentSchemeObject.name, currentSchemeObject) } else if (line.startsWith(ClassObject.SIGNATURE)) { currentSchemeObject = new ClassObject(line) - schemeObjects.add(currentSchemeObject) + 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.readLine(line) + currentSchemeObject.lines.add(line) } else if (!line.trim().isEmpty()) { throw new Exception("No objects in scheme") } } - for (SchemeObject schemeObject : schemeObjects) { - schemeObject.writeToFile(generatedModels) + for (SchemeObject schemeObject : schemeObjects.values()) { + for (String objectLine : schemeObject.lines) { + schemeObject.readLine(objectLine, schemeObjects) + } + schemeObject.writeToFile(generatedModels, schemeObjects) } } From a39a66d291e7cc958c90f1bee63ec519d9aa1fcf Mon Sep 17 00:00:00 2001 From: Gavriil Sitnikov Date: Wed, 26 Apr 2017 02:38:13 +0300 Subject: [PATCH 04/17] extends and type variables logic added --- gradle/apiGeneration.gradle | 149 ++++++++++++++++++++++-------------- 1 file changed, 91 insertions(+), 58 deletions(-) diff --git a/gradle/apiGeneration.gradle b/gradle/apiGeneration.gradle index 01d510f..7e86984 100644 --- a/gradle/apiGeneration.gradle +++ b/gradle/apiGeneration.gradle @@ -21,7 +21,7 @@ abstract class SchemeObject { abstract void readLine(String line, Map objects) } - +//TODO: collection/equals collection/map/equals map/validation/check for useless types/YAML class ImportObject extends SchemeObject { static final String SIGNATURE = "import" @@ -175,6 +175,69 @@ enum FieldType { } +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) { @@ -214,73 +277,19 @@ class FieldInfo { if (nullable) { typeString = typeString.substring(0, typeString.length() - 1) } - String genericsSuffix = typeString.indexOf("<") > 0 ? typeString.substring(typeString.indexOf("<")) : null + String original = typeString 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) - } - } + typeName = TypeNameUtils.resolveTypeName(original, objects) } 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")) @@ -326,6 +335,8 @@ class ClassObject extends SchemeObject { final String name final Map fieldsInfo = new HashMap<>() + final List typeVariables = new ArrayList<>() + TypeName superclass ClassObject(String firstLine) { name = firstLine.substring(SIGNATURE.length()).trim() @@ -335,9 +346,19 @@ class ClassObject extends SchemeObject { 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()) + + 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()) @@ -389,6 +410,18 @@ class ClassObject extends SchemeObject { @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(); From 4e01cd8168812621a57417be98fff43742ed8aea Mon Sep 17 00:00:00 2001 From: Gavriil Sitnikov Date: Wed, 26 Apr 2017 20:18:20 +0300 Subject: [PATCH 05/17] generator collections/maps support added --- gradle/apiGeneration.gradle | 67 ++++++++++++++++++++++++++++++------- 1 file changed, 54 insertions(+), 13 deletions(-) diff --git a/gradle/apiGeneration.gradle b/gradle/apiGeneration.gradle index 7e86984..a1c2a68 100644 --- a/gradle/apiGeneration.gradle +++ b/gradle/apiGeneration.gradle @@ -21,7 +21,9 @@ abstract class SchemeObject { abstract void readLine(String line, Map objects) } -//TODO: collection/equals collection/map/equals map/validation/check for useless types/YAML +//TODO: missable in future +//TODO: validation/check for useless types +//TODO: register all maps/collections classes and work with java typical types class ImportObject extends SchemeObject { static final String SIGNATURE = "import" @@ -41,7 +43,7 @@ class ImportObject extends SchemeObject { @Override void readLine(final String line, Map objects) { - throw new Exception("Line is not forimport object: '" + line + "'") + throw new Exception("Line is not related to import: '" + line + "'") } } @@ -146,7 +148,7 @@ class EnumObject extends SchemeObject { enum FieldType { BOOLEAN(TypeName.BOOLEAN), INT(TypeName.INT), LONG(TypeName.LONG), FLOAT(TypeName.FLOAT), - STRING(ClassName.get(String.class)), + STRING(ClassName.get(String.class)), ARRAY(ClassName.get(List.class)), MAP(ClassName.get(Map.class)), ENUM(null), MODEL(null), GENERIC(null) final TypeName typeName @@ -158,6 +160,8 @@ enum FieldType { 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 @@ -267,7 +271,7 @@ class FieldInfo { final FieldType fieldType final TypeName typeName - FieldInfo(String apiName, String typeString, Map objects) { + FieldInfo(String apiName, String typeString, String subTypeString, Map objects) { this.apiName = apiName required = typeString.endsWith('*') if (required) { @@ -281,7 +285,13 @@ class FieldInfo { typeString = getTypeSimpleName(typeString); fieldType = FieldType.get(typeString, objects); if (fieldType.typeName != null) { - typeName = fieldType.typeName + 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 { @@ -302,13 +312,20 @@ class FieldInfo { 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()); } + + 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() } @@ -323,7 +340,15 @@ class FieldInfo { final MethodSpec.Builder builder = MethodSpec.methodBuilder("set" + upperStartName(name)) .addParameter(parameterBuilder.build()) .addModifiers(Modifier.PUBLIC) - .addStatement("this." + name + " = " + name) + + 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() } @@ -388,12 +413,27 @@ class ClassObject extends SchemeObject { 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) + 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 { - equalsStatement.add("\n&& \$T.equals(\$L, that.\$L)", ClassName.bestGuess("ru.touchin.roboswag.core.utils.ObjectUtils"), - entry.key, entry.key) + 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 @@ -426,11 +466,12 @@ class ClassObject extends SchemeObject { 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, objects)) + fieldsInfo.put(fieldName, new FieldInfo(apiName, type, subType, objects)) } } From e39b8f562520326a6c9200a3c2e4a7fae8f69b5b Mon Sep 17 00:00:00 2001 From: Gavriil Sitnikov Date: Thu, 27 Apr 2017 01:55:06 +0300 Subject: [PATCH 06/17] generation of validation added --- gradle/apiGeneration.gradle | 66 ++++++++++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 5 deletions(-) diff --git a/gradle/apiGeneration.gradle b/gradle/apiGeneration.gradle index a1c2a68..2cdfd3d 100644 --- a/gradle/apiGeneration.gradle +++ b/gradle/apiGeneration.gradle @@ -21,8 +21,8 @@ abstract class SchemeObject { abstract void readLine(String line, Map objects) } +//TODO: datetime //TODO: missable in future -//TODO: validation/check for useless types //TODO: register all maps/collections classes and work with java typical types class ImportObject extends SchemeObject { @@ -107,7 +107,7 @@ class EnumObject extends SchemeObject { } } - JavaFile.builder("com.touchin.sberinkas", enumBuilder.build()).build().writeTo(directory); + JavaFile.builder("com.touchin.sberinkas", enumBuilder.build()).indent(" ").build().writeTo(directory); } Type typeOf(String value) { @@ -149,7 +149,7 @@ 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), GENERIC(null) + ENUM(null), MODEL(null), IMPORTED_MODEL(null), GENERIC(null) final TypeName typeName @@ -167,10 +167,14 @@ enum FieldType { case "long": return LONG case "float": return FLOAT default: - if (objects.get(typeString) instanceof EnumObject) { + SchemeObject object = objects.get(typeString); + if (object instanceof EnumObject) { return ENUM } - if (objects.get(typeString) != null) { + if (object instanceof ImportObject) { + return IMPORTED_MODEL + } + if (object instanceof ClassObject) { return MODEL } return GENERIC @@ -268,11 +272,21 @@ class FieldInfo { 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) @@ -352,6 +366,39 @@ class FieldInfo { 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 { @@ -404,6 +451,12 @@ class ClassObject extends SchemeObject { .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() @@ -412,6 +465,7 @@ class ClassObject extends SchemeObject { 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) @@ -441,10 +495,12 @@ class ClassObject extends SchemeObject { 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); } From 83f5f01bddc30426098a5ae42e4e361b85e36eb1 Mon Sep 17 00:00:00 2001 From: Gavriil Sitnikov Date: Thu, 27 Apr 2017 18:57:35 +0300 Subject: [PATCH 07/17] generation of API for boolean names fixed --- gradle/apiGeneration.gradle | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/gradle/apiGeneration.gradle b/gradle/apiGeneration.gradle index 2cdfd3d..5344323 100644 --- a/gradle/apiGeneration.gradle +++ b/gradle/apiGeneration.gradle @@ -21,6 +21,8 @@ abstract class SchemeObject { 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 @@ -322,8 +324,29 @@ class FieldInfo { .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) { - final MethodSpec.Builder builder = MethodSpec.methodBuilder("get" + upperStartName(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()) { From d7d28acb3e62b28cb077811521a1154e1f3a8655 Mon Sep 17 00:00:00 2001 From: Gavriil Sitnikov Date: Thu, 27 Apr 2017 23:38:42 +0300 Subject: [PATCH 08/17] generation of full constructor and date time added --- gradle/apiGeneration.gradle | 99 ++++++++++++++++++++++++++++++------- 1 file changed, 80 insertions(+), 19 deletions(-) diff --git a/gradle/apiGeneration.gradle b/gradle/apiGeneration.gradle index 5344323..baa4701 100644 --- a/gradle/apiGeneration.gradle +++ b/gradle/apiGeneration.gradle @@ -21,11 +21,8 @@ abstract class SchemeObject { 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" @@ -149,25 +146,61 @@ class EnumObject extends SchemeObject { 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) + BOOLEAN(TypeName.BOOLEAN, ClassName.get(Boolean.class)), + INT(TypeName.INT, ClassName.get(Integer.class)), + LONG(TypeName.LONG, ClassName.get(Long.class)), + FLOAT(TypeName.FLOAT, ClassName.get(Float.class)), + DOUBLE(TypeName.DOUBLE, ClassName.get(Double.class)), + STRING(ClassName.get(String.class)), + ARRAY(ClassName.get(List.class)), + MAP(ClassName.get(Map.class)), + DATE_TIME(ClassName.bestGuess("org.joda.time.DateTime")), + ENUM, + MODEL, + IMPORTED_MODEL, + GENERIC - final TypeName typeName + final TypeName primitiveTypeName + final TypeName nonPrimitiveTypeName + + FieldType() { + this(null, null) + } FieldType(final TypeName typeName) { - this.typeName = typeName + this(typeName, typeName) + } + + FieldType(final TypeName primitiveTypeName, final TypeName nonPrimitiveTypeName) { + this.primitiveTypeName = primitiveTypeName + this.nonPrimitiveTypeName = nonPrimitiveTypeName } 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 + case "string": + case "String": return STRING + case "List": + case "Collection": + case "LinkedList": + case "ArrayList": return ARRAY + 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) { @@ -300,13 +333,13 @@ class FieldInfo { String original = typeString typeString = getTypeSimpleName(typeString); fieldType = FieldType.get(typeString, objects); - if (fieldType.typeName != null) { + if (fieldType.nonPrimitiveTypeName != null) { if (fieldType == FieldType.ARRAY) { - typeName = ParameterizedTypeName.get(fieldType.typeName, TypeNameUtils.resolveTypeName(subTypeString, objects)) + typeName = ParameterizedTypeName.get(fieldType.nonPrimitiveTypeName, TypeNameUtils.resolveTypeName(subTypeString, objects)) } else if (fieldType == FieldType.MAP) { - typeName = ParameterizedTypeName.get(fieldType.typeName, ClassName.get(String.class), TypeNameUtils.resolveTypeName(subTypeString, objects)) + typeName = ParameterizedTypeName.get(fieldType.nonPrimitiveTypeName, ClassName.get(String.class), TypeNameUtils.resolveTypeName(subTypeString, objects)) } else { - typeName = fieldType.typeName + typeName = nullable ? fieldType.nonPrimitiveTypeName : fieldType.primitiveTypeName } } else if (fieldType != FieldType.GENERIC) { typeName = TypeNameUtils.resolveTypeName(original, objects) @@ -456,8 +489,15 @@ class ClassObject extends SchemeObject { 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) @@ -489,6 +529,22 @@ class ClassObject extends SchemeObject { classBuilder.addMethod(entry.value.createGetter(entry.key)) classBuilder.addMethod(entry.value.createSetter(entry.key)) entry.value.addValidateStatements(entry.key, validateMethod) + + if (fullConstructorBuilder != null) { + fullConstructorBuilder.addParameter(ParameterSpec.builder(entry.value.typeName, entry.key, Modifier.FINAL) + .addAnnotation(entry.value.nullable + ? ClassName.bestGuess("android.support.annotation.Nullable") + : ClassName.bestGuess("android.support.annotation.NonNull")) + .build()) + if (entry.value.fieldType == FieldType.ARRAY) { + fullConstructorBuilder.addStatement("this.\$L = \$T.unmodifiableList(\$L)", entry.key, ClassName.get(Collections.class), entry.key) + } else if (entry.value.fieldType == FieldType.MAP) { + fullConstructorBuilder.addStatement("this.\$L = \$T.unmodifiableMap(\$L)", entry.key, ClassName.get(Collections.class), entry.key) + } else { + fullConstructorBuilder.addStatement("this.\$L = \$L", entry.key, entry.key) + } + } + if (first) { if (superclass == null) { hashCodeStatement.add("return \$T.hashCode(\$L", ClassName.bestGuess("ru.touchin.roboswag.core.utils.ObjectUtils"), entry.key) @@ -522,6 +578,10 @@ class ClassObject extends SchemeObject { classBuilder.addMethod(equalsMethod.addCode(equalsStatement.build()).build()) classBuilder.addMethod(hashCodeMethod.addCode(hashCodeStatement.build()).build()) + if (fullConstructorBuilder != null) { + classBuilder.addMethod(fullConstructorBuilder.build()) + } + JavaFile.builder("com.touchin.sberinkas", classBuilder.build()) .indent(" ") .build().writeTo(directory); @@ -566,6 +626,7 @@ android.applicationVariants.all { String line Map schemeObjects = new HashMap<>() schemeObjects.put("List", new ImportObject("import java.util.List")) + schemeObjects.put("DateTime", new ImportObject("import org.joda.time.DateTime")) schemeObjects.put("Map", new ImportObject("import java.util.Map")) SchemeObject currentSchemeObject = null while ((line = reader.readLine()) != null) { From a2524eb48bd6d84b9e7396e48d3984d006e7c701 Mon Sep 17 00:00:00 2001 From: Gavriil Sitnikov Date: Fri, 28 Apr 2017 02:07:40 +0300 Subject: [PATCH 09/17] generator YAML scheme parsing added --- gradle/apiGeneration.gradle | 402 +++++++++++++++++++----------------- 1 file changed, 213 insertions(+), 189 deletions(-) diff --git a/gradle/apiGeneration.gradle b/gradle/apiGeneration.gradle index baa4701..13da08a 100644 --- a/gradle/apiGeneration.gradle +++ b/gradle/apiGeneration.gradle @@ -3,35 +3,34 @@ buildscript { 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 { - List lines = new ArrayList<>(); - abstract void writeToFile(File directory, Map objects) - abstract void readLine(String line, Map objects) - } -//TODO: YAML //TODO: missable in future +//TODO: dynamic package class ImportObject extends SchemeObject { - static final String SIGNATURE = "import" + static final String GROUP_NAME = "imports" final String name final String fullName - ImportObject(String firstLine) { - fullName = firstLine.substring(SIGNATURE.length()).trim() + ImportObject(String value) { + fullName = value.trim() name = fullName.substring(fullName.lastIndexOf('.') + 1) } @@ -40,11 +39,6 @@ class ImportObject extends SchemeObject { //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 { @@ -53,15 +47,32 @@ class EnumObject extends SchemeObject { STRING, NUMBER, BOOLEAN } - static final String SIGNATURE = "enum" + static final String PREFIX = "enum " final String name Type type Map values = new HashMap<>() - EnumObject(String firstLine) { - name = firstLine.substring(SIGNATURE.length()).trim() + 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 @@ -122,58 +133,40 @@ class EnumObject extends SchemeObject { } } - @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, ClassName.get(Boolean.class)), - INT(TypeName.INT, ClassName.get(Integer.class)), - LONG(TypeName.LONG, ClassName.get(Long.class)), - FLOAT(TypeName.FLOAT, ClassName.get(Float.class)), - DOUBLE(TypeName.DOUBLE, ClassName.get(Double.class)), - STRING(ClassName.get(String.class)), - ARRAY(ClassName.get(List.class)), - MAP(ClassName.get(Map.class)), - DATE_TIME(ClassName.bestGuess("org.joda.time.DateTime")), - ENUM, - MODEL, - IMPORTED_MODEL, - GENERIC + 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() { - this(null, null) + FieldType(final boolean ableToInnerValidate) { + this(null, null, ableToInnerValidate) } - FieldType(final TypeName typeName) { - this(typeName, typeName) + FieldType(final TypeName typeName, final boolean ableToInnerValidate) { + this(typeName, typeName, ableToInnerValidate) } - FieldType(final TypeName primitiveTypeName, final TypeName nonPrimitiveTypeName) { + 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) { @@ -183,7 +176,7 @@ enum FieldType { case "List": case "Collection": case "LinkedList": - case "ArrayList": return ARRAY + case "ArrayList": return LIST case "Map": case "HashMap": case "TreeMap": @@ -272,7 +265,7 @@ class TypeNameUtils { } } else { if (genericsSuffix != null) { - return getTypeNameWithArguments(ClassName.bestGuess(simpleName), genericsSuffix.substring(1), objects) + return getTypeNameWithArguments(ClassName.bestGuess(simpleName), genericsSuffix.substring(1), objects).value } else { return ClassName.bestGuess(simpleName) } @@ -304,52 +297,56 @@ class FieldInfo { return result; } + final String name final String apiName - final boolean nullable - final boolean required - final boolean nonEmptyCollection - final boolean solidCollection + boolean nullable + boolean missable + boolean nonEmptyCollection + 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) + 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") } - nonEmptyCollection = typeString.endsWith('|') - if (nonEmptyCollection) { - typeString = typeString.substring(0, typeString.length() - 1) + + String typeString = parameters.get("type") + if (typeString == null) { + throw new Exception("Missed type for field: " + name) } - 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); + fieldType = FieldType.get(getTypeSimpleName(typeString), objects); if (fieldType.nonPrimitiveTypeName != null) { - if (fieldType == FieldType.ARRAY) { - typeName = ParameterizedTypeName.get(fieldType.nonPrimitiveTypeName, TypeNameUtils.resolveTypeName(subTypeString, objects)) + 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 = ParameterizedTypeName.get(fieldType.nonPrimitiveTypeName, ClassName.get(String.class), TypeNameUtils.resolveTypeName(subTypeString, objects)) + 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(original, objects) + typeName = TypeNameUtils.resolveTypeName(typeString, objects) } else { // generic typeName = ClassName.bestGuess(typeString) } } - FieldSpec createField(String name) { + FieldSpec createField() { return FieldSpec.builder(typeName, name, Modifier.PRIVATE) .addAnnotation(AnnotationSpec.builder(ClassName.bestGuess("com.bluelinelabs.logansquare.annotation.JsonField")) .addMember("name", "\$S", apiName) @@ -361,7 +358,7 @@ class FieldInfo { return name.length() > prefix.length() && name.startsWith(prefix) && name.charAt(prefix.length()).isUpperCase() } - String getGetterPrefix(String name) { + String getGetterPrefix() { if (fieldType != FieldType.BOOLEAN) { return "get" } @@ -375,8 +372,8 @@ class FieldInfo { return "get" } - MethodSpec createGetter(String name) { - String getterPrefix = getGetterPrefix(name); + MethodSpec createGetter() { + String getterPrefix = getGetterPrefix(); final MethodSpec.Builder builder = MethodSpec.methodBuilder(getterPrefix.equals("get") ? getterPrefix + upperStartName(name) : getterPrefix + upperStartName(name.substring(getterPrefix.length()))) @@ -391,7 +388,7 @@ class FieldInfo { if (fieldType == FieldType.MAP) { builder.addStatement("return \$T.unmodifiableMap(\$L)", ClassName.get(Collections.class), name) - } else if (fieldType == FieldType.ARRAY) { + } else if (fieldType == FieldType.LIST) { builder.addStatement("return \$T.unmodifiableList(\$L)", ClassName.get(Collections.class), name) } else { builder.addStatement("return \$L", name) @@ -399,7 +396,7 @@ class FieldInfo { return builder.build() } - MethodSpec createSetter(String name) { + MethodSpec createSetter() { final ParameterSpec.Builder parameterBuilder = ParameterSpec.builder(typeName, name, Modifier.FINAL) if (!typeName.isPrimitive()) { parameterBuilder.addAnnotation(AnnotationSpec.builder(nullable @@ -413,7 +410,7 @@ class FieldInfo { if (fieldType == FieldType.MAP) { builder.addStatement("this.\$L = \$T.unmodifiableMap(\$L)", name, ClassName.get(Collections.class), name) - } else if (fieldType == FieldType.ARRAY) { + } 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) @@ -422,36 +419,46 @@ class FieldInfo { return builder.build() } - void addValidateStatements(String name, MethodSpec.Builder validateMethod) { - final String prefix + void addValidateStatements(MethodSpec.Builder validateMethod) { if (!nullable) { - prefix = "" validateMethod.addStatement("validateNotNull(\$L)", name) - } else { - prefix = "if(" + name + "!=null)" } - if (fieldType == FieldType.ARRAY) { + 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(prefix + "validateCollectionNotEmpty(\$L)", name) + validateMethod.addStatement("validateCollectionNotEmpty(\$L)", name) } if (solidCollection) { - validateMethod.addStatement(prefix + "validateCollection(\$L, CollectionValidationRule.EXCEPTION_IF_ANY_INVALID)", name) + validateMethod.addStatement("validateCollection(\$L, CollectionValidationRule.EXCEPTION_IF_ANY_INVALID)", name) } else if (nonEmptyCollection) { - validateMethod.addStatement(prefix + "validateCollection(\$L, CollectionValidationRule.EXCEPTION_IF_ALL_INVALID)", name) + validateMethod.addStatement("validateCollection(\$L, CollectionValidationRule.EXCEPTION_IF_ALL_INVALID)", name) } else { - validateMethod.addStatement(prefix + "validateCollection(\$L, CollectionValidationRule.REMOVE_INVALID_ITEMS)", name) + validateMethod.addStatement("validateCollection(\$L, CollectionValidationRule.REMOVE_INVALID_ITEMS)", name) } } else if (fieldType == FieldType.MAP) { if (nonEmptyCollection) { - validateMethod.addStatement(prefix + "validateCollectionNotEmpty(\$L.values())", name) + validateMethod.addStatement("validateCollectionNotEmpty(\$L.values())", name) } - validateMethod.addStatement(prefix + "validateCollection(\$L.values(), CollectionValidationRule.EXCEPTION_IF_ANY_INVALID)", name) + validateMethod.addStatement("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) + validateMethod.addStatement("\$L.validate()", name) + } else { + throw new Exception("Unexpected able to validate field type '" + fieldType + "' of field " + name) + } + if (nullable) { + validateMethod.endControlFlow() } } @@ -459,15 +466,47 @@ class FieldInfo { class ClassObject extends SchemeObject { - static final String SIGNATURE = "class" + static final String PREFIX = "class " final String name - final Map fieldsInfo = new HashMap<>() + final Map info + final List fields = new ArrayList<>() final List typeVariables = new ArrayList<>() TypeName superclass - ClassObject(String firstLine) { - name = firstLine.substring(SIGNATURE.length()).trim() + 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 @@ -505,8 +544,12 @@ class ClassObject extends SchemeObject { .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") + .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") @@ -524,55 +567,55 @@ class ClassObject extends SchemeObject { 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) + 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(entry.value.typeName, entry.key, Modifier.FINAL) - .addAnnotation(entry.value.nullable + 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 (entry.value.fieldType == FieldType.ARRAY) { - fullConstructorBuilder.addStatement("this.\$L = \$T.unmodifiableList(\$L)", entry.key, ClassName.get(Collections.class), entry.key) - } else if (entry.value.fieldType == FieldType.MAP) { - fullConstructorBuilder.addStatement("this.\$L = \$T.unmodifiableMap(\$L)", entry.key, ClassName.get(Collections.class), entry.key) + 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", entry.key, entry.key) + 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"), entry.key) + 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"), - entry.key, entry.key) + field.name, field.name) } else { - hashCodeStatement.add("return \$T.hashCode(super.hashCode(), \$L", ClassName.bestGuess("ru.touchin.roboswag.core.utils.ObjectUtils"), entry.key) + 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"), - entry.key, entry.key) + field.name, field.name) } } 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) + 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.equals(\$L, that.\$L)", ClassName.bestGuess("ru.touchin.roboswag.core.utils.ObjectUtils"), - entry.key, entry.key) + 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", entry.key) + hashCodeStatement.add(", \$L", field.name) } first = false } - equalsStatement.add(";") - hashCodeStatement.add(");") + equalsStatement.add(";\n") + hashCodeStatement.add(");\n") classBuilder.addMethod(validateMethod.build()) classBuilder.addMethod(equalsMethod.addCode(equalsStatement.build()).build()) @@ -587,68 +630,49 @@ class ClassObject extends SchemeObject { .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") + File schemeFile2 = new File("${project.projectDir}/src/main/res/raw/scheme2.yaml") + + String fileName = "scheme2.yaml" def apiModelsGenerationTask = tasks.create("apiModelsGeneration${variant.name}") << { - BufferedReader reader = new BufferedReader(new FileReader(schemeFile)) - String line + Yaml yaml = new Yaml(); Map schemeObjects = new HashMap<>() - schemeObjects.put("List", new ImportObject("import java.util.List")) - schemeObjects.put("DateTime", new ImportObject("import org.joda.time.DateTime")) - 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") + 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(schemeFile2))) { + 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") } } for (SchemeObject schemeObject : schemeObjects.values()) { - for (String objectLine : schemeObject.lines) { - schemeObject.readLine(objectLine, schemeObjects) + if (schemeObject instanceof ClassObject) { + schemeObject.resolveFieldsAndProperties(schemeObjects) } schemeObject.writeToFile(generatedModels, schemeObjects) } From 873fb09fe5c71a61553e2ba7cf47a302c577c162 Mon Sep 17 00:00:00 2001 From: Gavriil Sitnikov Date: Fri, 28 Apr 2017 20:23:33 +0300 Subject: [PATCH 10/17] generator parameters added: scheme path + models package --- gradle/apiGeneration.gradle | 54 +++++++++++++++++++++++++++---------- 1 file changed, 40 insertions(+), 14 deletions(-) diff --git a/gradle/apiGeneration.gradle b/gradle/apiGeneration.gradle index 13da08a..8985314 100644 --- a/gradle/apiGeneration.gradle +++ b/gradle/apiGeneration.gradle @@ -1,3 +1,5 @@ +apply plugin: 'com.android.application' + buildscript { repositories { jcenter() @@ -17,11 +19,12 @@ import javax.lang.model.element.Modifier abstract class SchemeObject { - abstract void writeToFile(File directory, Map objects) + abstract void writeToFile(File directory, Map objects, String appPackage) } //TODO: missable in future -//TODO: dynamic package +//TODO: move out of allvariants - too much +//TODO: refactor code class ImportObject extends SchemeObject { static final String GROUP_NAME = "imports" @@ -35,7 +38,7 @@ class ImportObject extends SchemeObject { } @Override - void writeToFile(final File directory, Map objects) { + void writeToFile(final File directory, Map objects, String appPackage) { //do nothing - imports are for other objects } @@ -76,7 +79,7 @@ class EnumObject extends SchemeObject { } @Override - void writeToFile(File directory, Map objects) { + 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")) @@ -117,7 +120,7 @@ class EnumObject extends SchemeObject { } } - JavaFile.builder("com.touchin.sberinkas", enumBuilder.build()).indent(" ").build().writeTo(directory); + JavaFile.builder(appPackage, enumBuilder.build()).indent(" ").build().writeTo(directory); } Type typeOf(String value) { @@ -510,10 +513,11 @@ class ClassObject extends SchemeObject { } @Override - void writeToFile(File directory, Map objects) { + 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) { @@ -624,20 +628,42 @@ class ClassObject extends SchemeObject { if (fullConstructorBuilder != null) { classBuilder.addMethod(fullConstructorBuilder.build()) } - - JavaFile.builder("com.touchin.sberinkas", classBuilder.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/models/${variant.dirName}") - File schemeFile2 = new File("${project.projectDir}/src/main/res/raw/scheme2.yaml") + 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' + } - String fileName = "scheme2.yaml" + 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}") << { @@ -647,7 +673,7 @@ android.applicationVariants.all { 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(schemeFile2))) { + 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)) { @@ -669,12 +695,12 @@ android.applicationVariants.all { 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) + schemeObject.writeToFile(generatedModels, schemeObjects, modelsPackage) } } From db1a6ec7bae61dc4f2f2ef2e41cdd287fd4c82a8 Mon Sep 17 00:00:00 2001 From: Gavriil Sitnikov Date: Sat, 29 Apr 2017 22:47:57 +0300 Subject: [PATCH 11/17] generator code refactoring --- gradle/apiGeneration.gradle | 811 +++++++++++++++++++++--------------- 1 file changed, 481 insertions(+), 330 deletions(-) diff --git a/gradle/apiGeneration.gradle b/gradle/apiGeneration.gradle index 8985314..51226b3 100644 --- a/gradle/apiGeneration.gradle +++ b/gradle/apiGeneration.gradle @@ -1,3 +1,22 @@ +/* + * 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 { @@ -10,21 +29,61 @@ buildscript { } } - 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: NUMBER/BOOLEAN enums in future + +//TODO: move out of allvariants - too much +//TODO: serialization/deserialization + +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") } -//TODO: missable in future -//TODO: move out of allvariants - too much -//TODO: refactor code + +/** + * 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 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" @@ -38,106 +97,126 @@ class ImportObject extends SchemeObject { } @Override - void writeToFile(final File directory, Map objects, String appPackage) { - //do nothing - imports are for other objects + void writeToFile(final File directory, final Map 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, NUMBER, BOOLEAN + 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 - Map values = new HashMap<>() + final Map values = new HashMap<>() - EnumObject(String name, Map values) { - this.name = name.trim() + EnumObject(final String enumName, final Map jsonValues) { + this.name = enumName.trim() + if (this.name.isEmpty()) { + throw new Exception("Name of enum is empty") + } - for (Map.Entry entry : values) { - final apiValue = entry.value.trim() - if (apiValue.isEmpty()) { - throw new Exception("Name of enum is empty") + for (final Map.Entry 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(entry.key)) { - throw new Exception("Name '" + value + "' already added to enum") + if (this.values.containsKey(enumValue)) { + throw new Exception("Value '" + enumValue + "' already registered into enum") } - Type type = typeOf(apiValue) + final Type type = typeOf(jsonValue) 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) + throw new Exception("Type of value '" + jsonValues + "' conflicts with previous value type: " + this.type) } - this.values.put(entry.key, apiValue) + this.values.put(enumValue, jsonValue) } } @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")) + void writeToFile(final File directory, final Map objects, final String packageName) { + final TypeSpec.Builder enumBuilder = TypeSpec.enumBuilder(name).addModifiers(Modifier.PUBLIC) + .addSuperinterface(Types.LOGAN_SQUARE_ENUM) - enumBuilder.addField(FieldSpec.builder(ClassName.get(String.class), "valueName", - Modifier.PRIVATE, - Modifier.FINAL) - .addAnnotation(ClassName.bestGuess("android.support.annotation.NonNull")) + .addField(FieldSpec.builder(ClassName.get(String.class), "valueName", Modifier.PRIVATE, Modifier.FINAL) + .addAnnotation(Types.NON_NULL) .build()) - enumBuilder.addMethod(MethodSpec.constructorBuilder() + .addMethod(MethodSpec.constructorBuilder() .addParameter(ClassName.get(String.class), "valueName", Modifier.FINAL) .addStatement("this.valueName = valueName") .build()) - enumBuilder.addMethod(MethodSpec.methodBuilder("getValueName") + .addMethod(MethodSpec.methodBuilder("getValueName") .returns(ClassName.get(String.class)) .addModifiers(Modifier.PUBLIC) .addAnnotation(ClassName.get(Override.class)) - .addAnnotation(ClassName.bestGuess("android.support.annotation.NonNull")) + .addAnnotation(Types.NON_NULL) .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) + .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 (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()) - } + for (final Map.Entry enumValue : values) { + enumBuilder.addEnumConstant(enumValue.key, TypeSpec.anonymousClassBuilder(type.format, enumValue.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 - } - } + JavaFile.builder(packageName, enumBuilder.build()) + .indent(" ") + .build() + .writeTo(directory) } } +/** + * Type of field of class. + */ enum FieldType { BOOLEAN(TypeName.BOOLEAN, ClassName.get(Boolean.class), false), @@ -151,11 +230,13 @@ enum FieldType { DATE_TIME(ClassName.bestGuess("org.joda.time.DateTime"), false), ENUM(false), MODEL(true), - IMPORTED_MODEL(true), - GENERIC(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 FieldType(final boolean ableToInnerValidate) { @@ -172,43 +253,35 @@ enum FieldType { this.ableToInnerValidate = ableToInnerValidate } - static FieldType get(String typeString, Map objects) { + /** + * 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 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 + 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); + final SchemeObject object = objects.get(typeString) + if (object instanceof ImportObject) { + return IMPORTED_CLASS + } if (object instanceof EnumObject) { return ENUM } - if (object instanceof ImportObject) { - return IMPORTED_MODEL - } if (object instanceof ClassObject) { return MODEL } - return GENERIC + return TYPE_ARGUMENT } } @@ -216,70 +289,122 @@ enum FieldType { 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) - } + private static String extractTypeArgumentsString(final String fullTypeString) { + final int startOfTypeArguments = fullTypeString.indexOf("<"); + return startOfTypeArguments > 0 ? fullTypeString.substring(startOfTypeArguments).replace(" ", "") : null } - 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 + /** + * Returns base type name without package and type arguments. + * Sample: 'java.lang.List' -> '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 + } + + private static TypeName resolveBaseTypeName(final String typeStringWithoutArguments, final Map objects) { + final String baseTypeString = extractBaseTypeString(typeStringWithoutArguments) + final SchemeObject associatedObject = objects.get(baseTypeString) + if (associatedObject instanceof ImportObject) { + return ClassName.bestGuess(associatedObject.fullName) + } + return ClassName.bestGuess(baseTypeString) + } + + private static Pair getTypeNameWithArguments(final TypeName parentTypeName, String typeArgumentsString, + final Map objects) { + final List 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) { - arguments.add(resolveType(genericString), objects) - break + 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> + typeArguments.add(resolveBaseTypeName(typeArgumentsString.substring(0, nextComma), objects)) + typeArgumentsString = typeArgumentsString.substring(nextComma + 1) + continue } if (nextLeft == -1 || nextRight < nextLeft) { - arguments.add(resolveType(genericString.substring(0, nextRight), objects)) - genericString = nextRight < genericString.length() - 1 ? genericString.substring(nextRight + 1) : "" + // 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 } - 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) + // when it is element with type arguments + // Sample: List> + 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 innerArgs = getTypeNameWithArguments(baseTypeName, typeArgumentsString, objects) + typeArgumentsString = innerArgs.key.substring(1) + typeArguments.add(innerArgs.value) } - return new Pair(genericString, ParameterizedTypeName.get(parentTypeName, (TypeName[]) arguments.toArray())) + return new Pair(typeArgumentsString, ParameterizedTypeName.get(parentTypeName, (TypeName[]) typeArguments.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) + /** + * Resolves TypeName from raw type string. + * @param typeString String describes type. E.g. 'java.lang.List' + * @param objects Map of registered objects. + * @return Resolved TypeName. + */ + static TypeName resolveTypeName(final String typeString, final Map objects) { + final TypeName baseTypeName = resolveBaseTypeName(typeString, objects) + final String typeArgumentsString = extractTypeArgumentsString(typeString) + if (typeArgumentsString != null) { + try { + final Pair result = getTypeNameWithArguments(baseTypeName, typeArgumentsString.substring(1), objects) + if (!result.key.isEmpty()) { + throw new Exception("Useless symbols '" + result.key + "'") + } + } catch (final Exception exception) { + throw new Exception("Error resolving type of '" + typeString + "' : " + exception.getMessage()) } } + return baseTypeName } } +/** + * 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 { - static upperStartName(String name) { + private static final String DEFAULT_GETTER_PREFIX = "get" + + private static upperStartName(final String name) { if (name.isEmpty()) { throw new Exception("Empty name of field") } @@ -289,59 +414,74 @@ class FieldInfo { 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; + 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 apiName + final String jsonName boolean nullable boolean missable boolean nonEmptyCollection boolean solidCollection - final FieldType fieldType + final FieldType type final TypeName typeName - FieldInfo(String name, Map parameters, Map objects) { - this.name = name - apiName = parameters.containsKey("apiName") ? parameters.get("apiName") : name + FieldInfo(final String fieldName, final Map parameters, final Map 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") + } - String flagsString = parameters.get("flags"); + final 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") + final List 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) + } + } } - String typeString = parameters.get("type") - if (typeString == null) { - throw new Exception("Missed type for field: " + name) + if (parameters.get("type") == null) { + throw new Exception("Missed type") } - fieldType = FieldType.get(getTypeSimpleName(typeString), objects); - if (fieldType.nonPrimitiveTypeName != null) { - if (fieldType == FieldType.LIST) { + 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: " + name + ". Supports only List<*>") + throw new Exception("Unsupported list type '" + typeName.toString() + "' of field: " + fieldName + ". Supports only List<*>") } - } else if (fieldType == FieldType.MAP) { + } else if (type == FieldType.MAP) { typeName = TypeNameUtils.resolveTypeName(typeString, objects) if (!typeName.toString().startsWith("java.util.Map") + throw new Exception("Unsupported map type of field: " + fieldName + ". Supports only Map") } } else { - typeName = nullable ? fieldType.nonPrimitiveTypeName : fieldType.primitiveTypeName + typeName = nullable ? type.nonPrimitiveTypeName : type.primitiveTypeName } - } else if (fieldType != FieldType.GENERIC) { + } else if (type != FieldType.TYPE_ARGUMENT) { typeName = TypeNameUtils.resolveTypeName(typeString, objects) } else { // generic @@ -349,71 +489,64 @@ class FieldInfo { } } - FieldSpec createField() { + FieldSpec generateFieldCode() { return FieldSpec.builder(typeName, name, Modifier.PRIVATE) - .addAnnotation(AnnotationSpec.builder(ClassName.bestGuess("com.bluelinelabs.logansquare.annotation.JsonField")) - .addMember("name", "\$S", apiName) + .addAnnotation(AnnotationSpec.builder(Types.JSON_FIELD) + .addMember("name", "\$S", jsonName) .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" + private String getGetterPrefix() { + if (type != FieldType.BOOLEAN) { + return DEFAULT_GETTER_PREFIX } - if (checkNameStartsWith(name, "is")) { + if (checkNameStartsWithPrefix(name, "is")) { return "is" - } else if (checkNameStartsWith(name, "has")) { + } else if (checkNameStartsWithPrefix(name, "has")) { return "has" - } else if (checkNameStartsWith(name, "have")) { + } else if (checkNameStartsWithPrefix(name, "have")) { return "have" } - return "get" + return DEFAULT_GETTER_PREFIX } - MethodSpec createGetter() { - String getterPrefix = getGetterPrefix(); - final MethodSpec.Builder builder = MethodSpec.methodBuilder(getterPrefix.equals("get") + 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()))) - .returns(typeName) .addModifiers(Modifier.PUBLIC) + .returns(typeName) + if (!typeName.isPrimitive()) { - builder.addAnnotation(AnnotationSpec.builder(nullable - ? ClassName.bestGuess("android.support.annotation.Nullable") - : ClassName.bestGuess("android.support.annotation.NonNull")) - .build()); + builder.addAnnotation(AnnotationSpec.builder(nullable ? Types.NULLABLE : Types.NON_NULL).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) + 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 createSetter() { + MethodSpec generateSetterCode() { 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()); + parameterBuilder.addAnnotation(AnnotationSpec.builder(nullable ? Types.NULLABLE : Types.NON_NULL).build()) } - final MethodSpec.Builder builder = MethodSpec.methodBuilder("set" + upperStartName(name)) - .addParameter(parameterBuilder.build()) - .addModifiers(Modifier.PUBLIC) - if (fieldType == FieldType.MAP) { + 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 (fieldType == FieldType.LIST) { + } 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) @@ -422,24 +555,24 @@ class FieldInfo { return builder.build() } - void addValidateStatements(MethodSpec.Builder validateMethod) { + void generateValidationCode(MethodSpec.Builder validateMethod) { if (!nullable) { validateMethod.addStatement("validateNotNull(\$L)", name) } - if (!fieldType.ableToInnerValidate) { + if (!type.ableToInnerValidate) { return } - if (fieldType == FieldType.GENERIC || fieldType == FieldType.IMPORTED_MODEL) { + if (type == FieldType.TYPE_ARGUMENT || type == FieldType.IMPORTED_CLASS) { validateMethod - .beginControlFlow("if (\$L instanceof \$T)", name, ClassName.bestGuess("ru.touchin.templates.ApiModel")) - .addStatement("((\$T) \$L).validate()", ClassName.bestGuess("ru.touchin.templates.ApiModel"), name) + .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 (fieldType == FieldType.LIST) { + if (type == FieldType.LIST) { if (nonEmptyCollection) { validateMethod.addStatement("validateCollectionNotEmpty(\$L)", name) } @@ -450,15 +583,15 @@ class FieldInfo { } else { validateMethod.addStatement("validateCollection(\$L, CollectionValidationRule.REMOVE_INVALID_ITEMS)", name) } - } else if (fieldType == FieldType.MAP) { + } 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 (fieldType == FieldType.MODEL) { + } else if (type == FieldType.MODEL) { validateMethod.addStatement("\$L.validate()", name) } else { - throw new Exception("Unexpected able to validate field type '" + fieldType + "' of field " + name) + throw new Exception("Unexpected able to validate field type '" + type + "' of field " + name) } if (nullable) { validateMethod.endControlFlow() @@ -467,24 +600,50 @@ class FieldInfo { } +/** + * 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 + * id: + * jsonName: _id + * type: int + * name: + * type: string + * flags: nullable + * items: + * type: List + * flags: non-empty, solid + */ class ClassObject extends SchemeObject { static final String PREFIX = "class " final String name - final Map info + final Map fieldsInfo final List fields = new ArrayList<>() final List typeVariables = new ArrayList<>() TypeName superclass - ClassObject(String name, Map info) { - this.name = name - this.info = info + ClassObject(final String name, final Map fieldsInfo) { + this.name = name.trim() + this.fieldsInfo = fieldsInfo } - void resolveFieldsAndProperties(Map objects) { - final List fieldNames = new ArrayList<>() - for (final Map.Entry entry : info.entrySet()) { + void resolveFieldsInfo(final Map objects) { + final Set 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) @@ -493,100 +652,70 @@ class ClassObject extends SchemeObject { } if (entry.key.equals("extends")) { - superclass = TypeNameUtils.resolveTypeName(entry.value.replace(" ", ""), objects) + superclass = TypeNameUtils.resolveTypeName(entry.value, 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)) + try { + if (entry.value instanceof Map) { + fields.add(new FieldInfo(entry.key, (Map) entry.value, objects)) + } else { + final Map 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(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")) - } + void writeToFile(final File directory, final Map 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) + // add type variables for (String typeVariable : typeVariables) { classBuilder.addTypeVariable(TypeVariableName.get(typeVariable)) } - classBuilder.addMethod(MethodSpec.constructorBuilder() - .addModifiers(Modifier.PUBLIC) - .addStatement("super()") - .build()) + // add default constructor + classBuilder.addMethod(MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC).addStatement("super()").build()) - MethodSpec.Builder fullConstructorBuilder = (superclass == null) ? - MethodSpec.constructorBuilder() - .addModifiers(Modifier.PUBLIC) - .addStatement("super()") + // create 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 - 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) + // create validate() method + final 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() + final CodeBlock.Builder equalsStatement = CodeBlock.builder() + final CodeBlock.Builder hashCodeStatement = CodeBlock.builder() - for (FieldInfo field : fields) { - classBuilder.addField(field.createField()) - classBuilder.addMethod(field.createGetter()) - classBuilder.addMethod(field.createSetter()) - field.addValidateStatements(validateMethod) + for (final FieldInfo field : fields) { + classBuilder.addField(field.generateFieldCode()) + classBuilder.addMethod(field.generateGetterCode()) + classBuilder.addMethod(field.generateSetterCode()) + field.generateValidationCode(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")) + .addAnnotation(field.nullable ? Types.NULLABLE : Types.NON_NULL) .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) + 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) } @@ -594,54 +723,66 @@ class ClassObject extends SchemeObject { 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) + hashCodeStatement.add("return \$T.hashCode(", Types.OBJECT_UTILS) + equalsStatement.add("return ", Types.OBJECT_UTILS) } 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) + hashCodeStatement.add("return \$T.hashCode(super.hashCode(), ", Types.OBJECT_UTILS) + equalsStatement.add("return super.equals(that) && ", Types.OBJECT_UTILS, 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) + 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") + // create validate() method classBuilder.addMethod(validateMethod.build()) - classBuilder.addMethod(equalsMethod.addCode(equalsStatement.build()).build()) - classBuilder.addMethod(hashCodeMethod.addCode(hashCodeStatement.build()).build()) + // add 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()) + // add hashCode() method + classBuilder.addMethod(MethodSpec.methodBuilder("hashCode").addModifiers(Modifier.PUBLIC) + .addAnnotation(Override.class) + .returns(TypeName.INT) + .addCode(hashCodeStatement.build()).build()) if (fullConstructorBuilder != null) { classBuilder.addMethod(fullConstructorBuilder.build()) } - println "!!!!!" + appPackage - JavaFile.builder(appPackage, classBuilder.build()) + JavaFile.builder(packageName, classBuilder.build()) .indent(" ") - .build().writeTo(directory); + .build() + .writeTo(directory) } } class FileUtils { - static void purgeDirectory(File dir) { - for (File file : dir.listFiles()) { - if (file.isDirectory()) purgeDirectory(file); - file.delete(); + static void purgeDirectory(final File directory) { + for (File file : directory.listFiles()) { + if (file.isDirectory()) { + purgeDirectory(file) + } + file.delete() } } @@ -667,40 +808,50 @@ android.applicationVariants.all { def apiModelsGenerationTask = tasks.create("apiModelsGeneration${variant.name}") << { - Yaml yaml = new Yaml(); + 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 { + if (!(data instanceof Map)) { throw new Exception("Yaml file '" + fileName + "' is invalid") } + + 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) + } + } } + FileUtils.purgeDirectory(generatedModels) + for (SchemeObject schemeObject : schemeObjects.values()) { if (schemeObject instanceof ClassObject) { - schemeObject.resolveFieldsAndProperties(schemeObjects) + try { + schemeObject.resolveFieldsInfo(schemeObjects) + } catch (final Exception exception) { + throw new Exception("Error on parsing class '" + schemeObject.name + "' : " + exception.getMessage()) + } + } + try { + schemeObject.writeToFile(generatedModels, schemeObjects, modelsPackage) + } catch (final Exception exception) { + throw new Exception("Error on generating code for '" + schemeObject.name + "' : " + exception.getMessage()) } - schemeObject.writeToFile(generatedModels, schemeObjects, modelsPackage) } } From 1f563d1a4dc61b307ad996d052e669851ea9de13 Mon Sep 17 00:00:00 2001 From: Gavriil Sitnikov Date: Sat, 29 Apr 2017 22:48:37 +0300 Subject: [PATCH 12/17] added todo --- gradle/apiGeneration.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/gradle/apiGeneration.gradle b/gradle/apiGeneration.gradle index 51226b3..5b2927a 100644 --- a/gradle/apiGeneration.gradle +++ b/gradle/apiGeneration.gradle @@ -41,6 +41,7 @@ import javax.lang.model.element.Modifier //TODO: move out of allvariants - too much //TODO: serialization/deserialization +//TODO: setup generation by map yaml->package class Types { From 920cd85f2c25b7f4854b2fab74ed6f5a8064d0eb Mon Sep 17 00:00:00 2001 From: Gavriil Sitnikov Date: Sun, 30 Apr 2017 18:01:18 +0300 Subject: [PATCH 13/17] serialization/deserialization added --- gradle/apiGeneration.gradle | 126 ++++++++++++++++++++++++------------ 1 file changed, 86 insertions(+), 40 deletions(-) diff --git a/gradle/apiGeneration.gradle b/gradle/apiGeneration.gradle index 5b2927a..ccc05f4 100644 --- a/gradle/apiGeneration.gradle +++ b/gradle/apiGeneration.gradle @@ -38,8 +38,8 @@ 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 -//TODO: move out of allvariants - too much //TODO: serialization/deserialization //TODO: setup generation by map yaml->package @@ -55,6 +55,8 @@ class Types { 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") } @@ -220,12 +222,12 @@ class EnumObject extends SchemeObject { */ 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), + 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), @@ -239,19 +241,29 @@ enum FieldType { 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) + this(null, null, ableToInnerValidate, null, null) } FieldType(final TypeName typeName, final boolean ableToInnerValidate) { - this(typeName, typeName, ableToInnerValidate) + this(typeName, typeName, ableToInnerValidate, null, null) } - FieldType(final TypeName primitiveTypeName, final TypeName nonPrimitiveTypeName, final boolean ableToInnerValidate) { + 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 } /** @@ -679,26 +691,42 @@ class ClassObject extends SchemeObject { .addAnnotation(AnnotationSpec.builder(Types.JSON_OBJECT).addMember("serializeNullObjects", "true").build()) .superclass(superclass != null ? superclass : Types.LOGAN_SQUARE_JSON_MODEL) - // add type variables + // adds type variables for (String typeVariable : typeVariables) { classBuilder.addTypeVariable(TypeVariableName.get(typeVariable)) } - // add default constructor + // adds default constructor classBuilder.addMethod(MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC).addStatement("super()").build()) - // create full constructor only if it is extends from LoganSquareJsonModel, + // 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 - // create validate() method + // 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() @@ -708,6 +736,16 @@ class ClassObject extends SchemeObject { 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) @@ -748,9 +786,9 @@ class ClassObject extends SchemeObject { equalsStatement.add(";\n") hashCodeStatement.add(");\n") - // create validate() method + // creates validate() method classBuilder.addMethod(validateMethod.build()) - // add equals() method + // adds equals() method classBuilder.addMethod(MethodSpec.methodBuilder("equals").addModifiers(Modifier.PUBLIC) .addAnnotation(Override.class) .returns(TypeName.BOOLEAN) @@ -759,11 +797,15 @@ class ClassObject extends SchemeObject { .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()) - // add hashCode() method + // 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()) @@ -791,26 +833,32 @@ class FileUtils { 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' - } + 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}") << { + String modelsPackage = android.extensions.findByName("apiGeneratorModelsPackage") + String schemeFilePath = android.extensions.findByName("apiGeneratorSchemePath") + if (modelsPackage == null) { + modelsPackage = android.defaultConfig.applicationId + '.logic.api.model' + } - if (schemePath == null) { - return - } + if (schemeFilePath == null) { + return + } - File schemeFile = new File(schemePath) - if (!schemeFile.exists()) { - schemeFile = new File("${project.projectDir}", schemePath) - } + File schemeFile = new File(schemeFilePath) + if (!schemeFile.exists()) { + schemeFile = new File("${project.projectDir}", schemeFilePath) + } + if (!schemeFile.exists()) { + throw new Exception("JSON models scheme file not found at '" + schemeFilePath + "' or at '${project.projectDir}/" + schemeFilePath + "'") + } + FileUtils.purgeDirectory(generatedModelsDirectory) - def apiModelsGenerationTask = tasks.create("apiModelsGeneration${variant.name}") << { - - Yaml yaml = new Yaml() - Map schemeObjects = new HashMap<>() + final Yaml yaml = new Yaml() + final 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")) @@ -838,9 +886,7 @@ android.applicationVariants.all { } } - FileUtils.purgeDirectory(generatedModels) - - for (SchemeObject schemeObject : schemeObjects.values()) { + for (final SchemeObject schemeObject : schemeObjects.values()) { if (schemeObject instanceof ClassObject) { try { schemeObject.resolveFieldsInfo(schemeObjects) @@ -849,13 +895,13 @@ android.applicationVariants.all { } } try { - schemeObject.writeToFile(generatedModels, schemeObjects, modelsPackage) + schemeObject.writeToFile(generatedModelsDirectory, schemeObjects, modelsPackage) } catch (final Exception exception) { throw new Exception("Error on generating code for '" + schemeObject.name + "' : " + exception.getMessage()) } } } - apiModelsGenerationTask.description = 'Generates API models' - variant.registerJavaGeneratingTask apiModelsGenerationTask, generatedModels + generateJsonModelsTask.description = 'Generates Java classes for JSON models' + variant.registerJavaGeneratingTask generateJsonModelsTask, generatedModelsDirectory } \ No newline at end of file From a8714126e3e2b9a2c9914a8b64806f988c8bb304 Mon Sep 17 00:00:00 2001 From: Gavriil Sitnikov Date: Tue, 2 May 2017 00:18:57 +0300 Subject: [PATCH 14/17] todo comment --- gradle/apiGeneration.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/apiGeneration.gradle b/gradle/apiGeneration.gradle index ccc05f4..90f3a75 100644 --- a/gradle/apiGeneration.gradle +++ b/gradle/apiGeneration.gradle @@ -40,7 +40,7 @@ import javax.lang.model.element.Modifier //TODO: NUMBER/BOOLEAN enums in future //TODO: maybe save md5-hashes to check if files/scheme changed -//TODO: serialization/deserialization +//TODO: bug with 'type: List' //TODO: setup generation by map yaml->package class Types { From 492cda3f52156fd73cba0c440a4aff30cdc9b9f4 Mon Sep 17 00:00:00 2001 From: Gavriil Sitnikov Date: Tue, 2 May 2017 17:25:35 +0300 Subject: [PATCH 15/17] bug with generics fixed --- gradle/apiGeneration.gradle | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/gradle/apiGeneration.gradle b/gradle/apiGeneration.gradle index 90f3a75..f152b0f 100644 --- a/gradle/apiGeneration.gradle +++ b/gradle/apiGeneration.gradle @@ -40,7 +40,6 @@ import javax.lang.model.element.Modifier //TODO: NUMBER/BOOLEAN enums in future //TODO: maybe save md5-hashes to check if files/scheme changed -//TODO: bug with 'type: List' //TODO: setup generation by map yaml->package class Types { @@ -388,19 +387,20 @@ class TypeNameUtils { static TypeName resolveTypeName(final String typeString, final Map objects) { final TypeName baseTypeName = resolveBaseTypeName(typeString, objects) final String typeArgumentsString = extractTypeArgumentsString(typeString) - if (typeArgumentsString != null) { - try { - final Pair result = getTypeNameWithArguments(baseTypeName, typeArgumentsString.substring(1), objects) - if (!result.key.isEmpty()) { - throw new Exception("Useless symbols '" + result.key + "'") - } - } catch (final Exception exception) { - throw new Exception("Error resolving type of '" + typeString + "' : " + exception.getMessage()) - } + if (typeArgumentsString == null) { + return baseTypeName } - return baseTypeName - } + try { + final Pair 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()) + } + } } /** @@ -743,7 +743,7 @@ class ClassObject extends SchemeObject { serializeMethod.addStatement("outputStream.\$L(\$L)", serializeMethodName, field.name) if (deserializeMethodName.equals("readObject")) { deserializeMethod.addStatement("\$L = (\$T) inputStream.\$L()", field.name, field.typeName, deserializeMethodName) - } else{ + } else { deserializeMethod.addStatement("\$L = inputStream.\$L()", field.name, deserializeMethodName) } From b36a2965dc9ca88ffb7ceb2876d71515e2343f78 Mon Sep 17 00:00:00 2001 From: Gavriil Sitnikov Date: Tue, 2 May 2017 17:50:41 +0300 Subject: [PATCH 16/17] renaming of son generation script --- ...ion.gradle => jsonModelsGeneration.gradle} | 132 ++++++++++-------- 1 file changed, 72 insertions(+), 60 deletions(-) rename gradle/{apiGeneration.gradle => jsonModelsGeneration.gradle} (91%) diff --git a/gradle/apiGeneration.gradle b/gradle/jsonModelsGeneration.gradle similarity index 91% rename from gradle/apiGeneration.gradle rename to gradle/jsonModelsGeneration.gradle index f152b0f..c219855 100644 --- a/gradle/apiGeneration.gradle +++ b/gradle/jsonModelsGeneration.gradle @@ -40,8 +40,6 @@ import javax.lang.model.element.Modifier //TODO: NUMBER/BOOLEAN enums in future //TODO: maybe save md5-hashes to check if files/scheme changed -//TODO: setup generation by map yaml->package - class Types { static final TypeName LOGAN_SQUARE_ENUM = ClassName.bestGuess("ru.touchin.templates.logansquare.LoganSquareEnum") @@ -829,6 +827,65 @@ class FileUtils { } } + 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 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 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 { @@ -838,66 +895,21 @@ android.applicationVariants.all { * Generating Java classes describing JSON models from specific YAML scheme. */ def generateJsonModelsTask = tasks.create("generateJsonModels${variant.name}") << { - String modelsPackage = android.extensions.findByName("apiGeneratorModelsPackage") - String schemeFilePath = android.extensions.findByName("apiGeneratorSchemePath") - if (modelsPackage == null) { - modelsPackage = android.defaultConfig.applicationId + '.logic.api.model' - } + final List jsonModelsMapping = android.extensions.findByName("jsonModelsMapping") - if (schemeFilePath == null) { - return - } - - File schemeFile = new File(schemeFilePath) - if (!schemeFile.exists()) { - schemeFile = new File("${project.projectDir}", schemeFilePath) - } - if (!schemeFile.exists()) { - throw new Exception("JSON models scheme file not found at '" + schemeFilePath + "' or at '${project.projectDir}/" + schemeFilePath + "'") - } FileUtils.purgeDirectory(generatedModelsDirectory) - - final Yaml yaml = new Yaml() - final 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)) { - throw new Exception("Yaml file '" + fileName + "' is invalid") - } - - 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) - } - } - } - - 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()) + 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}") } } } From 485927dd9e231007c559490c12e278c8965472cb Mon Sep 17 00:00:00 2001 From: Gavriil Sitnikov Date: Wed, 3 May 2017 16:02:29 +0300 Subject: [PATCH 17/17] some parts additionally commented --- gradle/jsonModelsGeneration.gradle | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/gradle/jsonModelsGeneration.gradle b/gradle/jsonModelsGeneration.gradle index c219855..20fb624 100644 --- a/gradle/jsonModelsGeneration.gradle +++ b/gradle/jsonModelsGeneration.gradle @@ -299,6 +299,12 @@ enum FieldType { class TypeNameUtils { + /** + * Returns type arguments part from full string that represents type. + * Sample: 'List' -> '' or 'Map' -> ''. + * @param fullTypeString Full string represents type. E.g. 'Map'; + * @return Type arguments part of string. Like '' + */ private static String extractTypeArgumentsString(final String fullTypeString) { final int startOfTypeArguments = fullTypeString.indexOf("<"); return startOfTypeArguments > 0 ? fullTypeString.substring(startOfTypeArguments).replace(" ", "") : null @@ -321,8 +327,15 @@ class TypeNameUtils { return result } - private static TypeName resolveBaseTypeName(final String typeStringWithoutArguments, final Map objects) { - final String baseTypeString = extractBaseTypeString(typeStringWithoutArguments) + /** + * Resolving type name of base type from string. + * Sample: 'List' -> '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 objects) { + final String baseTypeString = extractBaseTypeString(typeString) final SchemeObject associatedObject = objects.get(baseTypeString) if (associatedObject instanceof ImportObject) { return ClassName.bestGuess(associatedObject.fullName)