Merge pull request #10 from TouchInstinct/json_model_features

Json model features
This commit is contained in:
Gavriil 2017-05-12 15:28:19 +03:00 committed by GitHub
commit 60b3a36ed3
1 changed files with 237 additions and 67 deletions

View File

@ -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<String, String> entry : jsonValues) {
for (final Entry<String, String> 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<String, Object> enumValue : values) {
for (final Entry<String, Object> 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<String, *>")
}
} 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<String, SchemeObject> 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<String, String> fieldsInfo
final List<FieldInfo> fields = new ArrayList<>()
final List<String> typeVariables = new ArrayList<>()
final List<String> typeArguments = new ArrayList<>()
TypeName superclass
ClassObject parentModel
ClassObject(final String name, final Map<String, String> fieldsInfo) {
this.name = name.trim()
@ -663,20 +710,21 @@ class ClassObject extends SchemeObject {
void resolveFieldsInfo(final Map<String, SchemeObject> objects) {
final Set<String> 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<String> parameters, final List<String> childTypeArguments) {
final MethodSpec.Builder result
if (parentModel != null) {
final List<String> 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<String, SchemeObject> 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<String> parameters = new ArrayList<>()
final List<String> 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<String, SchemeObject> getObjectsMap(final String schemeFilePath, final String projectDir) {
if (schemeFilePath == null) {
return
}
@ -856,26 +982,35 @@ class FileUtils {
final Yaml yaml = new Yaml()
final Map<String, SchemeObject> 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<String, Object> entry : data.entrySet()) {
for (final Entry<String, Object> 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<String> jsonModelsMapping = android.extensions.findByName("jsonModelsMapping")
FileUtils.purgeDirectory(generatedModelsDirectory)
final Map<String, Map<String, SchemeObject>> overallObjects = new HashMap<>()
for (final String jsonMapping : jsonModelsMapping) {
final int indexOfDivider = jsonMapping.indexOf('->')
final String packageName
final Map<String, SchemeObject> 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<String, Map<String, SchemeObject>> fileObjects : overallObjects.entrySet()) {
final String packageName = fileObjects.key
final Map<String, SchemeObject> schemeObjects = new HashMap<>(fileObjects.value)
for (final Entry<String, Map<String, SchemeObject>> 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())
}
}
}
}