/* * 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 org.yaml.snakeyaml.Yaml import javax.lang.model.element.Modifier import java.util.Map.Entry //TODO: optional in future //TODO: NUMBER/BOOLEAN enums in future //TODO: maybe save md5-hashes to check if files/scheme changed 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 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") static final TypeName OBJECT_OUTPUT_STREAM = ClassName.bestGuess("java.io.ObjectOutputStream") static final TypeName OBJECT_INPUT_STREAM = ClassName.bestGuess("java.io.ObjectInputStream") } class Pair { public final TKey key; public final TValue value; public Pair(final TKey key, final TValue value) { this.key = key this.value = value } } /** * 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 { enum Type { MODEL, ENUM, EXTERNAL } static final String GROUP_NAME = "imports" final String name final String fullName final Type type final ClassObject relatedModel 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 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 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 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, "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), 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 final String serializationMethodName final String deserializationMethodName FieldType(final boolean ableToInnerValidate) { this(null, null, ableToInnerValidate, null, null) } FieldType(final TypeName typeName, final boolean ableToInnerValidate) { this(typeName, typeName, ableToInnerValidate, null, null) } 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 } /** * 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) { 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 } if (object instanceof ClassObject) { return MODEL } return TYPE_ARGUMENT } } } 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 } /** * 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 } /** * 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) } 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) { 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.value } catch (final Exception exception) { throw new Exception("Error resolving type of '" + typeString + "' : " + exception.getMessage()) } } } /** * 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; * - 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. */ 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 optional 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 "optional": optional = 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 = couldContainsNull() ? type.nonPrimitiveTypeName : type.primitiveTypeName } } else if (type != FieldType.TYPE_ARGUMENT) { typeName = TypeNameUtils.resolveTypeName(typeString, objects) } else { // generic typeName = ClassName.bestGuess(typeString) } } boolean couldContainsNull() { return nullable || optional } 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(couldContainsNull() ? Types.NULLABLE : Types.NON_NULL).build()) } if (type == FieldType.LIST) { if (couldContainsNull()) { builder.addStatement("return \$L != null ? \$T.unmodifiableList(\$L) : null", name, Types.COLLECTIONS, name) } else { builder.addStatement("return \$T.unmodifiableList(\$L)", Types.COLLECTIONS, name) } } else if (type == FieldType.MAP) { if (couldContainsNull()) { builder.addStatement("return \$L != null ? \\$T.unmodifiableMap(\\$L) : null", name, Types.COLLECTIONS, name) } else { builder.addStatement("return \$T.unmodifiableMap(\$L)", name, 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(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.LIST) { if (couldContainsNull()) { builder.addStatement("this.\$L = \$L !=null ? new \$T<>(\$L) : null", name, name, Types.ARRAY_LIST, name) } else { builder.addStatement("this.\$L = new \$T<>(\$L)", name, Types.ARRAY_LIST, name) } } else if (type == FieldType.MAP) { if (couldContainsNull()) { builder.addStatement("this.\$L = \$L !=null ? new \$T<>(\$L) : null", name, name, Types.HASH_MAP, name) } else { builder.addStatement("this.\$L = new \$T<>(\$L)", name, name, Types.HASH_MAP, name) } } else { builder.addStatement("this.\$L = \$L", name, name) } return builder.build() } void generateValidationCode(MethodSpec.Builder validateMethod) { if (!couldContainsNull()) { 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() 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 (couldContainsNull()) { 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 (couldContainsNull()) { 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 " 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 typeArguments = new ArrayList<>() TypeName superclass ClassObject parentModel 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 Entry entry : fieldsInfo.entrySet()) { if (fieldNames.contains(entry.key)) { throw new Exception("Duplicate field name: " + name) } if (entry.key.equals("typeArguments")) { for (String typeVariable : entry.value.replace(" ", "").split(",")) { typeArguments.add(typeVariable) } continue } if (entry.key.equals("extends")) { superclass = TypeNameUtils.resolveTypeName(entry.value, objects) parentModel = resolveBaseTypeModel(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()) } } } 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) { final ParameterSpec.Builder paramBuilder = ParameterSpec .builder(ClassName.bestGuess(childTypeArguments.get(argIndex)), field.name, Modifier.FINAL) if (!field.typeName.primitive) { paramBuilder.addAnnotation(field.couldContainsNull() ? Types.NULLABLE : Types.NON_NULL) } result.addParameter(paramBuilder.build()) } else { final ParameterSpec.Builder paramBuilder = ParameterSpec.builder(field.typeName, field.name, Modifier.FINAL) if (!field.typeName.primitive) { paramBuilder.addAnnotation(field.couldContainsNull() ? Types.NULLABLE : Types.NON_NULL) } result.addParameter(paramBuilder.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) .addJavadoc("This class is autogenerated by Touchin tools") .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 (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 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) .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() for (final FieldInfo field : fields) { classBuilder.addField(field.generateFieldCode()) classBuilder.addMethod(field.generateGetterCode()) classBuilder.addMethod(field.generateSetterCode()) field.generateValidationCode(validateMethod) final String serializeMethodName = (!field.couldContainsNull() && field.type.serializationMethodName != null ? field.type.serializationMethodName : "writeObject"); 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")) { deserializeMethod.addStatement("\$L = (\$T) inputStream.\$L()", field.name, field.typeName, deserializeMethodName) } else { deserializeMethod.addStatement("\$L = inputStream.\$L()", field.name, deserializeMethodName) } if (fullConstructorBuilder != null) { final ParameterSpec.Builder paramBuilder = ParameterSpec.builder(field.typeName, field.name, Modifier.FINAL) if (!field.typeName.primitive) { paramBuilder.addAnnotation(field.couldContainsNull() ? Types.NULLABLE : Types.NON_NULL) } fullConstructorBuilder.addParameter(paramBuilder.build()) } 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) { 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(this.\$L, that.\$L)", Types.OBJECT_UTILS, field.name, field.name) } else if (field.type == FieldType.LIST) { equalsStatement.add("\$T.isCollectionsEquals(this.\$L, that.\$L)", Types.OBJECT_UTILS, field.name, field.name) } else { equalsStatement.add("\$T.equals(this.\$L, that.\$L)", Types.OBJECT_UTILS, field.name, field.name) } hashCodeStatement.add("this.\$L", field.name) first = false } equalsStatement.add(";\n") hashCodeStatement.add(");\n") // 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) .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()) // 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()) } 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() } } static Map getObjectsMap(final String schemeFilePath, 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", 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 Entry entry : data.entrySet()) { if (entry.key.equals(ImportObject.GROUP_NAME)) { for (String importString : (Iterable) entry.value) { 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) } } } return schemeObjects } } android.applicationVariants.all { variant -> 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}") << { 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) { packageName = android.defaultConfig.applicationId + '.logic.model' objects = FileUtils.getObjectsMap(jsonMapping.trim(), "${project.projectDir}") } else { 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 Entry> fileObjects : overallObjects.entrySet()) { final String packageName = fileObjects.key final Map schemeObjects = new HashMap<>(fileObjects.value) 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()) } } } } generateJsonModelsTask.description = 'Generates Java classes for JSON models' variant.registerJavaGeneratingTask generateJsonModelsTask, generatedModelsDirectory }