diff --git a/gradle/jsonModelsGeneration.gradle b/gradle/jsonModelsGeneration.gradle index 20fb624..bac0bcf 100644 --- a/gradle/jsonModelsGeneration.gradle +++ b/gradle/jsonModelsGeneration.gradle @@ -34,9 +34,10 @@ import javafx.util.Pair import org.yaml.snakeyaml.Yaml import javax.lang.model.element.Modifier +import java.util.Map.Entry -//TODO: missable in future +//TODO: optional in future //TODO: NUMBER/BOOLEAN enums in future //TODO: maybe save md5-hashes to check if files/scheme changed @@ -49,6 +50,10 @@ class Types { 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 COLLECTION = ClassName.get(Collection.class) + static final TypeName ARRAY_LIST = ClassName.get(ArrayList.class) + static final TypeName MAP = ClassName.get(Map.class) + static final TypeName HASH_MAP = ClassName.get(HashMap.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") @@ -86,14 +91,24 @@ abstract class SchemeObject { */ class ImportObject extends SchemeObject { + enum Type { + MODEL, + ENUM, + EXTERNAL + } + static final String GROUP_NAME = "imports" final String name final String fullName + final Type type + final ClassObject relatedModel - ImportObject(String value) { + ImportObject(final String value, final Type type, final ClassObject relatedModel) { fullName = value.trim() name = fullName.substring(fullName.lastIndexOf('.') + 1) + this.type = type + this.relatedModel = relatedModel } @Override @@ -154,7 +169,7 @@ class EnumObject extends SchemeObject { throw new Exception("Name of enum is empty") } - for (final Map.Entry entry : jsonValues) { + for (final Entry entry : jsonValues) { final enumValue = entry.key.trim() final jsonValue = entry.value.trim() if (jsonValue.isEmpty() || enumValue.isEmpty()) { @@ -202,7 +217,7 @@ class EnumObject extends SchemeObject { .build()) .build()) - for (final Map.Entry enumValue : values) { + for (final Entry enumValue : values) { enumBuilder.addEnumConstant(enumValue.key, TypeSpec.anonymousClassBuilder(type.format, enumValue.value).build()) } @@ -283,7 +298,14 @@ enum FieldType { default: final SchemeObject object = objects.get(typeString) if (object instanceof ImportObject) { - return IMPORTED_CLASS + switch (object.type) { + case ImportObject.Type.MODEL: + return MODEL + case ImportObject.Type.ENUM: + return ENUM + case ImportObject.Type.EXTERNAL: + return IMPORTED_CLASS + } } if (object instanceof EnumObject) { return ENUM @@ -420,7 +442,7 @@ class TypeNameUtils { * - 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; + * - optional - 'optional' 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. */ @@ -445,7 +467,7 @@ class FieldInfo { final String name final String jsonName boolean nullable - boolean missable + boolean optional boolean nonEmptyCollection boolean solidCollection final FieldType type @@ -469,8 +491,8 @@ class FieldInfo { case "nullable": nullable = true break - case "missable": - missable = true + case "optional": + optional = true break case "non-empty": nonEmptyCollection = true @@ -503,7 +525,7 @@ class FieldInfo { throw new Exception("Unsupported map type of field: " + fieldName + ". Supports only Map") } } else { - typeName = nullable ? type.nonPrimitiveTypeName : type.primitiveTypeName + typeName = couldContainsNull() ? type.nonPrimitiveTypeName : type.primitiveTypeName } } else if (type != FieldType.TYPE_ARGUMENT) { typeName = TypeNameUtils.resolveTypeName(typeString, objects) @@ -513,6 +535,10 @@ class FieldInfo { } } + boolean couldContainsNull() { + return nullable || optional + } + FieldSpec generateFieldCode() { return FieldSpec.builder(typeName, name, Modifier.PRIVATE) .addAnnotation(AnnotationSpec.builder(Types.JSON_FIELD) @@ -544,13 +570,13 @@ class FieldInfo { .returns(typeName) if (!typeName.isPrimitive()) { - builder.addAnnotation(AnnotationSpec.builder(nullable ? Types.NULLABLE : Types.NON_NULL).build()) + builder.addAnnotation(AnnotationSpec.builder(couldContainsNull() ? Types.NULLABLE : Types.NON_NULL).build()) } - if (type == FieldType.MAP) { - builder.addStatement("return \$T.unmodifiableMap(\$L)", Types.COLLECTIONS, name) - } else if (type == FieldType.LIST) { + if (type == FieldType.LIST) { builder.addStatement("return \$T.unmodifiableList(\$L)", Types.COLLECTIONS, name) + } else if (type == FieldType.MAP) { + builder.addStatement("return \$T.unmodifiableMap(\$L)", Types.COLLECTIONS, name) } else { builder.addStatement("return \$L", name) } @@ -561,17 +587,17 @@ class FieldInfo { MethodSpec generateSetterCode() { final ParameterSpec.Builder parameterBuilder = ParameterSpec.builder(typeName, name, Modifier.FINAL) if (!typeName.isPrimitive()) { - parameterBuilder.addAnnotation(AnnotationSpec.builder(nullable ? Types.NULLABLE : Types.NON_NULL).build()) + parameterBuilder.addAnnotation(AnnotationSpec.builder(couldContainsNull() ? Types.NULLABLE : Types.NON_NULL).build()) } 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 (type == FieldType.LIST) { - builder.addStatement("this.\$L = \$T.unmodifiableList(\$L)", name, ClassName.get(Collections.class), name) + if (type == FieldType.LIST) { + builder.addStatement("this.\$L = new \$T<>(\$L)", name, Types.ARRAY_LIST, name) + } else if (type == FieldType.MAP) { + builder.addStatement("this.\$L = new \$T<>(\$L)", name, Types.HASH_MAP, name) } else { builder.addStatement("this.\$L = \$L", name, name) } @@ -580,7 +606,7 @@ class FieldInfo { } void generateValidationCode(MethodSpec.Builder validateMethod) { - if (!nullable) { + if (!couldContainsNull()) { validateMethod.addStatement("validateNotNull(\$L)", name) } if (!type.ableToInnerValidate) { @@ -591,9 +617,17 @@ class FieldInfo { .beginControlFlow("if (\$L instanceof \$T)", name, Types.API_MODEL) .addStatement("((\$T) \$L).validate()", Types.API_MODEL, name) .endControlFlow() + validateMethod + .beginControlFlow("if (\$L instanceof \$T)", name, Types.COLLECTION) + .addStatement("validateCollection((\$T) \$L, CollectionValidationRule.EXCEPTION_IF_ANY_INVALID)", Types.COLLECTION, name) + .endControlFlow() + validateMethod + .beginControlFlow("if (\$L instanceof \$T)", name, Types.MAP) + .addStatement("validateCollection(((\$T) \$L).values(), CollectionValidationRule.EXCEPTION_IF_ANY_INVALID)", Types.MAP, name) + .endControlFlow() return } - if (nullable) { + if (couldContainsNull()) { validateMethod.beginControlFlow("if (\$L != null)", name) } if (type == FieldType.LIST) { @@ -617,7 +651,7 @@ class FieldInfo { } else { throw new Exception("Unexpected able to validate field type '" + type + "' of field " + name) } - if (nullable) { + if (couldContainsNull()) { validateMethod.endControlFlow() } } @@ -650,11 +684,24 @@ class ClassObject extends SchemeObject { static final String PREFIX = "class " + private static ClassObject resolveBaseTypeModel(final String typeString, final Map objects) { + final String baseTypeString = TypeNameUtils.extractBaseTypeString(typeString) + final SchemeObject associatedObject = objects.get(baseTypeString) + if (associatedObject instanceof ClassObject) { + return associatedObject + } + if (associatedObject instanceof ImportObject && associatedObject.relatedModel != null) { + return associatedObject.relatedModel + } + return null + } + final String name final Map fieldsInfo final List fields = new ArrayList<>() - final List typeVariables = new ArrayList<>() + final List typeArguments = new ArrayList<>() TypeName superclass + ClassObject parentModel ClassObject(final String name, final Map fieldsInfo) { this.name = name.trim() @@ -663,20 +710,21 @@ class ClassObject extends SchemeObject { void resolveFieldsInfo(final Map objects) { final Set fieldNames = new HashSet<>() - for (final Map.Entry entry : fieldsInfo.entrySet()) { + for (final Entry entry : fieldsInfo.entrySet()) { if (fieldNames.contains(entry.key)) { throw new Exception("Duplicate field name: " + name) } - if (entry.key.equals("typeVariables")) { + if (entry.key.equals("typeArguments")) { for (String typeVariable : entry.value.replace(" ", "").split(",")) { - typeVariables.add(typeVariable) + typeArguments.add(typeVariable) } continue } if (entry.key.equals("extends")) { superclass = TypeNameUtils.resolveTypeName(entry.value, objects) + parentModel = resolveBaseTypeModel(entry.value, objects) continue } @@ -696,25 +744,84 @@ class ClassObject extends SchemeObject { } } + private MethodSpec.Builder createFullConstructorWithParent(final List parameters, final List childTypeArguments) { + final MethodSpec.Builder result + if (parentModel != null) { + final List resolvedTypeArguments = new ArrayList<>() + if (superclass instanceof ParameterizedTypeName) { + for (final TypeName typeName : superclass.typeArguments) { + final int argIndex = typeArguments.indexOf(typeName.toString()) + if (argIndex >= 0) { + resolvedTypeArguments.add(childTypeArguments.get(argIndex)) + } else { + resolvedTypeArguments.add(typeName.toString()) + } + } + } + result = parentModel.createFullConstructorWithParent(parameters, resolvedTypeArguments) + } else { + result = MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC) + } + for (final FieldInfo field : fields) { + final int argIndex = typeArguments.indexOf(field.typeName.toString()) + if (argIndex >= 0) { + result.addParameter(ParameterSpec.builder(ClassName.bestGuess(childTypeArguments.get(argIndex)), field.name, Modifier.FINAL) + .addAnnotation(field.couldContainsNull() ? Types.NULLABLE : Types.NON_NULL) + .build()) + } else { + result.addParameter(ParameterSpec.builder(field.typeName, field.name, Modifier.FINAL) + .addAnnotation(field.couldContainsNull() ? Types.NULLABLE : Types.NON_NULL) + .build()) + } + parameters.add(field.name) + } + return result + } + @Override 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) + final TypeName[] arguments = new TypeName[typeArguments.size()] + int index = 0 // adds type variables - for (String typeVariable : typeVariables) { + for (final String typeVariable : typeArguments) { classBuilder.addTypeVariable(TypeVariableName.get(typeVariable)) + arguments[index++] = TypeVariableName.get(typeVariable) } + final TypeName thisTypeName = arguments.length > 0 ? ParameterizedTypeName.get(ClassName.bestGuess(name), arguments) : ClassName.bestGuess(name) + // adds default constructor classBuilder.addMethod(MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC).addStatement("super()").build()) // 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 + final MethodSpec.Builder fullConstructorBuilder + if (superclass == null) { + fullConstructorBuilder = MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC).addStatement("super()") + } else if (parentModel != null) { + final List parameters = new ArrayList<>() + final List typeArguments = new ArrayList<>() + if (superclass instanceof ParameterizedTypeName) { + for (final TypeName typeName : superclass.typeArguments) { + typeArguments.add(typeName.toString()) + } + } + fullConstructorBuilder = parentModel.createFullConstructorWithParent(parameters, typeArguments) + fullConstructorBuilder.addStatement("super(\$L)", parameters.join(", ")) + } else { + fullConstructorBuilder = null + } + + // creates copy logic method + final MethodSpec.Builder copyToMethod = MethodSpec.methodBuilder("copyTo").addModifiers(Modifier.PROTECTED) + .addParameter(ParameterSpec.builder(thisTypeName, "destination", Modifier.FINAL).addAnnotation(Types.NON_NULL).build()) + if (parentModel != null) { + copyToMethod.addStatement("super.copyTo(destination)") + } // creates validate() method final MethodSpec.Builder validateMethod = MethodSpec.methodBuilder("validate").addModifiers(Modifier.PUBLIC) @@ -747,9 +854,9 @@ class ClassObject extends SchemeObject { classBuilder.addMethod(field.generateGetterCode()) classBuilder.addMethod(field.generateSetterCode()) field.generateValidationCode(validateMethod) - final String serializeMethodName = (!field.nullable && field.type.serializationMethodName != null + final String serializeMethodName = (!field.couldContainsNull() && field.type.serializationMethodName != null ? field.type.serializationMethodName : "writeObject"); - final String deserializeMethodName = (!field.nullable && field.type.deserializationMethodName != null + final String deserializeMethodName = (!field.couldContainsNull() && field.type.deserializationMethodName != null ? field.type.deserializationMethodName : "readObject"); serializeMethod.addStatement("outputStream.\$L(\$L)", serializeMethodName, field.name) if (deserializeMethodName.equals("readObject")) { @@ -760,15 +867,24 @@ class ClassObject extends SchemeObject { if (fullConstructorBuilder != null) { fullConstructorBuilder.addParameter(ParameterSpec.builder(field.typeName, field.name, Modifier.FINAL) - .addAnnotation(field.nullable ? Types.NULLABLE : Types.NON_NULL) + .addAnnotation(field.couldContainsNull() ? Types.NULLABLE : Types.NON_NULL) .build()) - 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 { + } + if (field.type == FieldType.LIST) { + if (fullConstructorBuilder != null) { + fullConstructorBuilder.addStatement("this.\$L = new \$T<>(\$L)", field.name, Types.ARRAY_LIST, field.name) + } + copyToMethod.addStatement("destination.\$L = new \$T<>(\$L)", field.name, Types.ARRAY_LIST, field.name) + } else if (field.type == FieldType.MAP) { + if (fullConstructorBuilder != null) { + fullConstructorBuilder.addStatement("this.\$L = new \$T<>(\$L)", field.name, Types.HASH_MAP, field.name) + } + copyToMethod.addStatement("destination.\$L = new \$T<>(\$L)", field.name, Types.HASH_MAP, field.name) + } else { + if (fullConstructorBuilder != null) { fullConstructorBuilder.addStatement("this.\$L = \$L", field.name, field.name) } + copyToMethod.addStatement("destination.\$L = \$L", field.name, field.name) } if (first) { @@ -799,6 +915,17 @@ class ClassObject extends SchemeObject { // creates validate() method classBuilder.addMethod(validateMethod.build()) + // creates copyTo() method + classBuilder.addMethod(copyToMethod.build()) + // creates copy() method + classBuilder.addMethod(MethodSpec.methodBuilder("copy").addModifiers(Modifier.PUBLIC) + .returns(thisTypeName) + .addAnnotation(Types.NON_NULL) + .addStatement("final \$T result = new \$T()", thisTypeName, thisTypeName) + .addStatement("this.copyTo(result)") + .addStatement("return result") + .addJavadoc("Beware! It is not copying objects stored in fields.") + .build()) // adds equals() method classBuilder.addMethod(MethodSpec.methodBuilder("equals").addModifiers(Modifier.PUBLIC) .addAnnotation(Override.class) @@ -840,8 +967,7 @@ class FileUtils { } } - static void generateJsonModelsCode(final File generatedModelsDirectory, final String schemeFilePath, - final String modelsPackage, final String projectDir) { + static Map getObjectsMap(final String schemeFilePath, final String projectDir) { if (schemeFilePath == null) { return } @@ -856,26 +982,35 @@ class FileUtils { 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")) + schemeObjects.put("Map", new ImportObject("java.util.Map", ImportObject.Type.EXTERNAL, null)) + schemeObjects.put("List", new ImportObject("java.util.List", ImportObject.Type.EXTERNAL, null)) + schemeObjects.put("DateTime", new ImportObject("org.joda.time.DateTime", ImportObject.Type.EXTERNAL, null)) 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()) { + for (final Entry entry : data.entrySet()) { if (entry.key.equals(ImportObject.GROUP_NAME)) { for (String importString : (Iterable) entry.value) { - final ImportObject importObject = new ImportObject(importString) + final ImportObject importObject = new ImportObject(importString, ImportObject.Type.EXTERNAL, null) + if (schemeObjects.containsKey(importObject.name)) { + throw new Exception("Duplicate import object with name '" + importObject.name + "' in file " + schemeFile.getPath()) + } 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) + if (schemeObjects.containsKey(enumObject.name)) { + throw new Exception("Duplicate enum object with name '" + enumObject.name + "' in file " + schemeFile.getPath()) + } 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) + if (schemeObjects.containsKey(classObject.name)) { + throw new Exception("Duplicate class object with name '" + classObject.name + "' in file " + schemeFile.getPath()) + } schemeObjects.put(classObject.name, classObject) } else { throw new Exception("Unexpected scheme object: " + entry.key) @@ -883,20 +1018,7 @@ class FileUtils { } } - 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()) - } - } + return schemeObjects } } @@ -911,18 +1033,66 @@ android.applicationVariants.all { final List jsonModelsMapping = android.extensions.findByName("jsonModelsMapping") FileUtils.purgeDirectory(generatedModelsDirectory) + final Map> overallObjects = new HashMap<>() for (final String jsonMapping : jsonModelsMapping) { final int indexOfDivider = jsonMapping.indexOf('->') + final String packageName + final Map objects if (indexOfDivider == -1) { - FileUtils.generateJsonModelsCode(generatedModelsDirectory, - jsonMapping.trim(), - android.defaultConfig.applicationId + '.logic.model', - "${project.projectDir}") + packageName = android.defaultConfig.applicationId + '.logic.model' + objects = FileUtils.getObjectsMap(jsonMapping.trim(), "${project.projectDir}") } else { - FileUtils.generateJsonModelsCode(generatedModelsDirectory, - jsonMapping.substring(0, indexOfDivider).trim(), - jsonMapping.substring(indexOfDivider + 2).trim(), - "${project.projectDir}") + packageName = jsonMapping.substring(indexOfDivider + 2).trim() + objects = FileUtils.getObjectsMap(jsonMapping.substring(0, indexOfDivider).trim(), "${project.projectDir}") + } + if (overallObjects.containsKey(packageName)) { + overallObjects.get(packageName).putAll(objects) + } else { + overallObjects.put(packageName, objects) + } + } + + for (final Entry> fileObjects : overallObjects.entrySet()) { + final String packageName = fileObjects.key + final Map schemeObjects = new HashMap<>(fileObjects.value) + for (final Entry> externalObjects : overallObjects.entrySet()) { + if (externalObjects.key == packageName) { + continue + } + for (final SchemeObject externalObject : externalObjects.value.values()) { + if (schemeObjects.containsKey(externalObject.name)) { + if (!(externalObject instanceof ImportObject) || externalObject.type != ImportObject.Type.EXTERNAL) { + throw new Exception("Duplicate model name '" + externalObject.name + "' for package " + packageName) + } + } + if (externalObject instanceof ImportObject) { + schemeObjects.put(externalObject.name, externalObject) + } else if (externalObject instanceof EnumObject) { + schemeObjects.put(externalObject.name, + new ImportObject(externalObjects.key + '.' + externalObject.name, ImportObject.Type.ENUM, null)) + } else if (externalObject instanceof ClassObject) { + schemeObjects.put(externalObject.name, + new ImportObject(externalObjects.key + '.' + externalObject.name, ImportObject.Type.MODEL, externalObject)) + } + } + } + + 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()) + } + } + } + + for (final SchemeObject schemeObject : schemeObjects.values()) { + try { + schemeObject.writeToFile(generatedModelsDirectory, schemeObjects, packageName) + } catch (final Exception exception) { + throw new Exception("Error on generating code for '" + schemeObject.name + "' : " + exception.getMessage()) + } } } }