/* * 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 { repositories { jcenter() } dependencies { classpath 'org.yaml:snakeyaml:1.8' classpath 'com.squareup:javapoet:1.8.0' } } import com.squareup.javapoet.* import javafx.util.Pair import org.yaml.snakeyaml.Yaml import javax.lang.model.element.Modifier //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") } /** * 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" final String name final String fullName ImportObject(String value) { fullName = value.trim() name = fullName.substring(fullName.lastIndexOf('.') + 1) } @Override 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("\$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 final Map values = new HashMap<>() EnumObject(final String enumName, final Map jsonValues) { this.name = enumName.trim() if (this.name.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(enumValue)) { throw new Exception("Value '" + enumValue + "' already registered into enum") } final Type type = typeOf(jsonValue) if (this.type == null) { this.type = type } else if (this.type != type) { throw new Exception("Type of value '" + jsonValues + "' conflicts with previous value type: " + this.type) } this.values.put(enumValue, jsonValue) } } @Override 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) .addField(FieldSpec.builder(ClassName.get(String.class), "valueName", Modifier.PRIVATE, Modifier.FINAL) .addAnnotation(Types.NON_NULL) .build()) .addMethod(MethodSpec.constructorBuilder() .addParameter(ClassName.get(String.class), "valueName", Modifier.FINAL) .addStatement("this.valueName = valueName") .build()) .addMethod(MethodSpec.methodBuilder("getValueName") .returns(ClassName.get(String.class)) .addModifiers(Modifier.PUBLIC) .addAnnotation(ClassName.get(Override.class)) .addAnnotation(Types.NON_NULL) .addStatement("return valueName") .build()) .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 (final Map.Entry enumValue : values) { enumBuilder.addEnumConstant(enumValue.key, TypeSpec.anonymousClassBuilder(type.format, enumValue.value).build()) } JavaFile.builder(packageName, enumBuilder.build()) .indent(" ") .build() .writeTo(directory) } } /** * Type of field of class. */ enum FieldType { BOOLEAN(TypeName.BOOLEAN, ClassName.get(Boolean.class), false), INT(TypeName.INT, ClassName.get(Integer.class), false), LONG(TypeName.LONG, ClassName.get(Long.class), false), FLOAT(TypeName.FLOAT, ClassName.get(Float.class), false), DOUBLE(TypeName.DOUBLE, ClassName.get(Double.class), false), STRING(ClassName.get(String.class), false), LIST(ClassName.get(List.class), true), MAP(ClassName.get(Map.class), true), DATE_TIME(ClassName.bestGuess("org.joda.time.DateTime"), false), ENUM(false), MODEL(true), IMPORTED_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) { this(null, null, ableToInnerValidate) } FieldType(final TypeName typeName, final boolean ableToInnerValidate) { this(typeName, typeName, ableToInnerValidate) } FieldType(final TypeName primitiveTypeName, final TypeName nonPrimitiveTypeName, final boolean ableToInnerValidate) { this.primitiveTypeName = primitiveTypeName this.nonPrimitiveTypeName = nonPrimitiveTypeName this.ableToInnerValidate = ableToInnerValidate } /** * 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 default: final SchemeObject object = objects.get(typeString) if (object instanceof ImportObject) { return IMPORTED_CLASS } if (object instanceof EnumObject) { return ENUM } if (object instanceof ClassObject) { return MODEL } return TYPE_ARGUMENT } } } class TypeNameUtils { private static String extractTypeArgumentsString(final String fullTypeString) { final int startOfTypeArguments = fullTypeString.indexOf("<"); return startOfTypeArguments > 0 ? fullTypeString.substring(startOfTypeArguments).replace(" ", "") : null } /** * 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) { 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) { // 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 } // 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(typeArgumentsString, ParameterizedTypeName.get(parentTypeName, (TypeName[]) typeArguments.toArray())) } /** * 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 { private static final String DEFAULT_GETTER_PREFIX = "get" private static upperStartName(final 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) } 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 jsonName boolean nullable boolean missable boolean nonEmptyCollection boolean solidCollection final FieldType type final TypeName typeName 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") } final String flagsString = parameters.get("flags") if (flagsString != null) { 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) } } } if (parameters.get("type") == null) { throw new Exception("Missed type") } 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: " + fieldName + ". Supports only List<*>") } } else if (type == FieldType.MAP) { typeName = TypeNameUtils.resolveTypeName(typeString, objects) if (!typeName.toString().startsWith("java.util.Map") } } else { typeName = nullable ? type.nonPrimitiveTypeName : type.primitiveTypeName } } else if (type != FieldType.TYPE_ARGUMENT) { typeName = TypeNameUtils.resolveTypeName(typeString, objects) } else { // generic typeName = ClassName.bestGuess(typeString) } } FieldSpec generateFieldCode() { return FieldSpec.builder(typeName, name, Modifier.PRIVATE) .addAnnotation(AnnotationSpec.builder(Types.JSON_FIELD) .addMember("name", "\$S", jsonName) .build()) .build() } private String getGetterPrefix() { if (type != FieldType.BOOLEAN) { return DEFAULT_GETTER_PREFIX } if (checkNameStartsWithPrefix(name, "is")) { return "is" } else if (checkNameStartsWithPrefix(name, "has")) { return "has" } else if (checkNameStartsWithPrefix(name, "have")) { return "have" } return DEFAULT_GETTER_PREFIX } 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()))) .addModifiers(Modifier.PUBLIC) .returns(typeName) if (!typeName.isPrimitive()) { builder.addAnnotation(AnnotationSpec.builder(nullable ? Types.NULLABLE : Types.NON_NULL).build()) } 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 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()) } 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) } else { builder.addStatement("this.\$L = \$L", name, name) } return builder.build() } void generateValidationCode(MethodSpec.Builder validateMethod) { if (!nullable) { validateMethod.addStatement("validateNotNull(\$L)", name) } if (!type.ableToInnerValidate) { return } if (type == FieldType.TYPE_ARGUMENT || type == FieldType.IMPORTED_CLASS) { validateMethod .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 (type == FieldType.LIST) { if (nonEmptyCollection) { validateMethod.addStatement("validateCollectionNotEmpty(\$L)", name) } if (solidCollection) { validateMethod.addStatement("validateCollection(\$L, CollectionValidationRule.EXCEPTION_IF_ANY_INVALID)", name) } else if (nonEmptyCollection) { validateMethod.addStatement("validateCollection(\$L, CollectionValidationRule.EXCEPTION_IF_ALL_INVALID)", name) } else { validateMethod.addStatement("validateCollection(\$L, CollectionValidationRule.REMOVE_INVALID_ITEMS)", name) } } else if (type == FieldType.MAP) { if (nonEmptyCollection) { validateMethod.addStatement("validateCollectionNotEmpty(\$L.values())", name) } validateMethod.addStatement("validateCollection(\$L.values(), CollectionValidationRule.EXCEPTION_IF_ANY_INVALID)", name) } else if (type == FieldType.MODEL) { validateMethod.addStatement("\$L.validate()", name) } else { throw new Exception("Unexpected able to validate field type '" + type + "' of field " + name) } if (nullable) { validateMethod.endControlFlow() } } } /** * 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 fieldsInfo final List fields = new ArrayList<>() final List typeVariables = new ArrayList<>() TypeName superclass ClassObject(final String name, final Map fieldsInfo) { this.name = name.trim() this.fieldsInfo = fieldsInfo } 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) } continue } if (entry.key.equals("extends")) { superclass = TypeNameUtils.resolveTypeName(entry.value, objects) continue } fieldNames.add(entry.key) 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(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)) } // add default constructor classBuilder.addMethod(MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC).addStatement("super()").build()) // 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 // 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 final CodeBlock.Builder equalsStatement = CodeBlock.builder() final CodeBlock.Builder hashCodeStatement = CodeBlock.builder() 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 ? 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 { fullConstructorBuilder.addStatement("this.\$L = \$L", field.name, field.name) } } if (first) { if (superclass == null) { hashCodeStatement.add("return \$T.hashCode(", Types.OBJECT_UTILS) equalsStatement.add("return ", Types.OBJECT_UTILS) } else { hashCodeStatement.add("return \$T.hashCode(super.hashCode(), ", Types.OBJECT_UTILS) equalsStatement.add("return super.equals(that) && ", Types.OBJECT_UTILS, field.name, field.name) } } else { 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()) // 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()) } JavaFile.builder(packageName, classBuilder.build()) .indent(" ") .build() .writeTo(directory) } } class FileUtils { static void purgeDirectory(final File directory) { for (File file : directory.listFiles()) { if (file.isDirectory()) { purgeDirectory(file) } file.delete() } } } android.applicationVariants.all { variant -> File generatedModels = new File("${project.buildDir}/generated/source/api/model/${variant.dirName}") String modelsPackage = android.extensions.findByName("apiGeneratorModelsPackage") String schemePath = android.extensions.findByName("apiGeneratorSchemePath") if (modelsPackage == null) { modelsPackage = android.defaultConfig.applicationId + '.logic.api.model' } if (schemePath == null) { return } File schemeFile = new File(schemePath) if (!schemeFile.exists()) { schemeFile = new File("${project.projectDir}", schemePath) } def apiModelsGenerationTask = tasks.create("apiModelsGeneration${variant.name}") << { Yaml yaml = new Yaml() Map schemeObjects = new HashMap<>() schemeObjects.put("Map", new ImportObject("java.util.Map")) schemeObjects.put("List", new ImportObject("java.util.List")) schemeObjects.put("DateTime", new ImportObject("org.joda.time.DateTime")) for (final Object data : yaml.loadAll(new FileReader(schemeFile))) { if (!(data instanceof Map)) { 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) { 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()) } } } apiModelsGenerationTask.description = 'Generates API models' variant.registerJavaGeneratingTask apiModelsGenerationTask, generatedModels }