diff --git a/build.gradle b/build.gradle index aac865a7..4d0643cf 100755 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.7.0' + ext.kotlin_version = '1.7.10' repositories { google() mavenCentral() @@ -19,7 +19,6 @@ buildscript { } } - subprojects { apply plugin: "org.jlleitschuh.gradle.ktlint" @@ -27,8 +26,12 @@ subprojects { mavenCentral() } - // Optionally configure plugin ktlint { + + filter { + exclude { it.file.path.contains("$buildDir/generated/") } + } + disabledRules = [ "import-ordering", "chain-wrapping", diff --git a/couchbase-entity-api/src/main/java/kaufland/com/coachbasebinderapi/PersistenceConfig.kt b/couchbase-entity-api/src/main/java/kaufland/com/coachbasebinderapi/PersistenceConfig.kt index fb16ec4e..8acd131d 100644 --- a/couchbase-entity-api/src/main/java/kaufland/com/coachbasebinderapi/PersistenceConfig.kt +++ b/couchbase-entity-api/src/main/java/kaufland/com/coachbasebinderapi/PersistenceConfig.kt @@ -6,7 +6,7 @@ object PersistenceConfig { private var mConnector: Connector? = null private var mSuspendingConnector: SuspendingConnector? = null - interface Connector { + interface Connector : TypeConversionErrorCallback { val typeConversions: Map, TypeConversion> fun getDocument(id: String, dbName: String, onlyInclude: List? = null): Map? fun getDocuments(ids: List, dbName: String, onlyInclude: List? = null): List?> @@ -19,7 +19,7 @@ object PersistenceConfig { fun upsertDocument(document: MutableMap, id: String?, dbName: String): Map } - interface SuspendingConnector { + interface SuspendingConnector : TypeConversionErrorCallback { val typeConversions: Map, TypeConversion> diff --git a/couchbase-entity-api/src/main/java/kaufland/com/coachbasebinderapi/TypeConversion.java b/couchbase-entity-api/src/main/java/kaufland/com/coachbasebinderapi/TypeConversion.java index 310f73f3..92b312a6 100644 --- a/couchbase-entity-api/src/main/java/kaufland/com/coachbasebinderapi/TypeConversion.java +++ b/couchbase-entity-api/src/main/java/kaufland/com/coachbasebinderapi/TypeConversion.java @@ -1,9 +1,13 @@ package kaufland.com.coachbasebinderapi; +import org.jetbrains.annotations.Nullable; + public interface TypeConversion { - Object write(Object value); + @Nullable + Object write(@Nullable Object value); - Object read(Object value); + @Nullable + Object read(@Nullable Object value); } diff --git a/couchbase-entity-api/src/main/java/kaufland/com/coachbasebinderapi/TypeConversionErrorCallback.kt b/couchbase-entity-api/src/main/java/kaufland/com/coachbasebinderapi/TypeConversionErrorCallback.kt new file mode 100644 index 00000000..360eac5a --- /dev/null +++ b/couchbase-entity-api/src/main/java/kaufland/com/coachbasebinderapi/TypeConversionErrorCallback.kt @@ -0,0 +1,5 @@ +package kaufland.com.coachbasebinderapi + +interface TypeConversionErrorCallback { + fun invokeOnError(errorWrapper: TypeConversionErrorWrapper) +} diff --git a/couchbase-entity-api/src/main/java/kaufland/com/coachbasebinderapi/TypeConversionErrorWrapper.kt b/couchbase-entity-api/src/main/java/kaufland/com/coachbasebinderapi/TypeConversionErrorWrapper.kt new file mode 100644 index 00000000..425317e3 --- /dev/null +++ b/couchbase-entity-api/src/main/java/kaufland/com/coachbasebinderapi/TypeConversionErrorWrapper.kt @@ -0,0 +1,10 @@ +package kaufland.com.coachbasebinderapi + +import kotlin.reflect.KClass + +data class TypeConversionErrorWrapper( + val exception: Exception, + val fieldName: String, + val value: Any?, + val `class`: KClass<*> +) diff --git a/couchbase-entity-connector/build.gradle b/couchbase-entity-connector/build.gradle index 9b0c022b..636617b2 100644 --- a/couchbase-entity-connector/build.gradle +++ b/couchbase-entity-connector/build.gradle @@ -3,11 +3,11 @@ apply plugin: 'com.github.dcendents.android-maven' apply plugin: 'kotlin-android' android { - compileSdkVersion 29 + compileSdkVersion 33 defaultConfig { minSdkVersion 21 - targetSdkVersion 28 + targetSdkVersion 33 versionCode 1 versionName "1.0" @@ -25,7 +25,6 @@ android { sourceCompatibility = 1.8 targetCompatibility = 1.8 } - buildToolsVersion = '29.0.2' } group = 'com.github.SchwarzIT' diff --git a/couchbase-entity-connector/src/main/java/kaufland/com/couchbaseentityconnector/Couchbase2Connector.kt b/couchbase-entity-connector/src/main/java/kaufland/com/couchbaseentityconnector/Couchbase2Connector.kt index e05dc608..86302af7 100644 --- a/couchbase-entity-connector/src/main/java/kaufland/com/couchbaseentityconnector/Couchbase2Connector.kt +++ b/couchbase-entity-connector/src/main/java/kaufland/com/couchbaseentityconnector/Couchbase2Connector.kt @@ -17,11 +17,11 @@ abstract class Couchbase2Connector : PersistenceConfig.Connector { init { mTypeConversions[Int::class] = object : TypeConversion { - override fun write(value: Any): Any { + override fun write(value: Any?): Any? { return value } - override fun read(value: Any): Any { + override fun read(value: Any?): Any? { if (value is Number) { return value.toInt() } @@ -29,7 +29,7 @@ abstract class Couchbase2Connector : PersistenceConfig.Connector { val result = ArrayList() for (itValue in value) { itValue?.let { - result.add(read(itValue)) + read(itValue)?.let { it1 -> result.add(it1) } } } return result @@ -38,11 +38,11 @@ abstract class Couchbase2Connector : PersistenceConfig.Connector { } } mTypeConversions[Double::class] = object : TypeConversion { - override fun write(value: Any): Any { + override fun write(value: Any?): Any? { return value } - override fun read(value: Any): Any { + override fun read(value: Any?): Any? { if (value is Number) { return value.toDouble() } @@ -50,7 +50,7 @@ abstract class Couchbase2Connector : PersistenceConfig.Connector { val result = ArrayList() for (itValue in value) { itValue?.let { - result.add(read(itValue)) + read(itValue)?.let { it1 -> result.add(it1) } } } return result @@ -150,16 +150,16 @@ abstract class Couchbase2Connector : PersistenceConfig.Connector { } @Throws(PersistenceException::class) - override fun upsertDocument(upsert: MutableMap, docId: String?, name: String): Map { - if (upsert["_id"] == null && docId != null) { - upsert["_id"] = docId + override fun upsertDocument(document: MutableMap, id: String?, dbName: String): Map { + if (document["_id"] == null && id != null) { + document["_id"] = id } - val unsavedDoc = MutableDocument(docId, upsert) + val unsavedDoc = MutableDocument(id, document) return try { - upsert["_id"] = unsavedDoc.id + document["_id"] = unsavedDoc.id unsavedDoc.setString("_id", unsavedDoc.id) - getDatabase(name).save(unsavedDoc) - upsert + getDatabase(dbName).save(unsavedDoc) + document } catch (e: CouchbaseLiteException) { throw PersistenceException(e) } diff --git a/couchbase-entity/src/main/java/com/kaufland/generation/model/CblDefaultGeneration.kt b/couchbase-entity/src/main/java/com/kaufland/generation/model/CblDefaultGeneration.kt index 1560c83b..9bf66241 100644 --- a/couchbase-entity/src/main/java/com/kaufland/generation/model/CblDefaultGeneration.kt +++ b/couchbase-entity/src/main/java/com/kaufland/generation/model/CblDefaultGeneration.kt @@ -11,16 +11,29 @@ object CblDefaultGeneration { fun addDefaults(holder: BaseEntityHolder, useNullableMap: Boolean): FunSpec { - val type = if (useNullableMap) TypeUtil.mutableMapStringAnyNullable() else TypeUtil.mutableMapStringAny() - val typeConversionReturnType = if (useNullableMap) TypeUtil.anyNullable() else TypeUtil.any() + val type = + if (useNullableMap) TypeUtil.mutableMapStringAnyNullable() else TypeUtil.mutableMapStringAny() + val typeConversionReturnType = + if (useNullableMap) TypeUtil.anyNullable() else TypeUtil.any() - val builder = FunSpec.builder("addDefaults").addModifiers(KModifier.PRIVATE).addParameter("map", type) + val builder = + FunSpec.builder("addDefaults").addModifiers(KModifier.PRIVATE).addParameter("map", type) for (fieldHolder in holder.fields.values) { if (fieldHolder.isDefault) { builder.beginControlFlow("if(map[%N] == null)", fieldHolder.constantName) - builder.addStatement("map.put(%N, " + fieldHolder.ensureType(typeConversionReturnType, ConversionUtil.convertStringToDesiredFormat(fieldHolder.typeMirror, fieldHolder.defaultValue)) + "!!)", fieldHolder.constantName) + builder.addStatement( + "map.put(%N, " + fieldHolder.ensureType( + typeConversionReturnType, + ConversionUtil.convertStringToDesiredFormat( + fieldHolder.typeMirror, + fieldHolder.defaultValue + ) + ", %N", + fieldHolder.constantName + ) + "!!)", + fieldHolder.constantName + ) builder.endControlFlow() } } diff --git a/couchbase-entity/src/main/java/com/kaufland/generation/model/CommonInterfaceGeneration.kt b/couchbase-entity/src/main/java/com/kaufland/generation/model/CommonInterfaceGeneration.kt index 3cc8b8a5..81286727 100644 --- a/couchbase-entity/src/main/java/com/kaufland/generation/model/CommonInterfaceGeneration.kt +++ b/couchbase-entity/src/main/java/com/kaufland/generation/model/CommonInterfaceGeneration.kt @@ -9,11 +9,11 @@ class CommonInterfaceGeneration { fun generateModel(holder: BaseEntityHolder): FileSpec { - var interfaceSpec = TypeSpec.interfaceBuilder(holder.interfaceSimpleName) + val interfaceSpec = TypeSpec.interfaceBuilder(holder.interfaceSimpleName) interfaceSpec.addSuperinterface(TypeUtil.mapSupport()) holder.basedOn.forEach { interfaceSpec.addSuperinterface(it.interfaceTypeName) } - var companionSpec = TypeSpec.companionObjectBuilder() + val companionSpec = TypeSpec.companionObjectBuilder() for (fieldHolder in holder.allFields) { val isBaseField = holder.basedOn.any { diff --git a/couchbase-entity/src/main/java/com/kaufland/generation/model/EnsureTypesGeneration.kt b/couchbase-entity/src/main/java/com/kaufland/generation/model/EnsureTypesGeneration.kt index 9129f93c..b2f3a8e6 100644 --- a/couchbase-entity/src/main/java/com/kaufland/generation/model/EnsureTypesGeneration.kt +++ b/couchbase-entity/src/main/java/com/kaufland/generation/model/EnsureTypesGeneration.kt @@ -8,15 +8,26 @@ object EnsureTypesGeneration { fun ensureTypes(holder: BaseEntityHolder, useNullableMap: Boolean): FunSpec { - val explicitType = if (useNullableMap) TypeUtil.hashMapStringAnyNullable() else TypeUtil.hashMapStringAny() + val explicitType = + if (useNullableMap) TypeUtil.hashMapStringAnyNullable() else TypeUtil.hashMapStringAny() val type = if (useNullableMap) TypeUtil.mapStringAnyNullable() else TypeUtil.mapStringAny() - val typeConversionReturnType = if (useNullableMap) TypeUtil.anyNullable() else TypeUtil.any() + val typeConversionReturnType = + if (useNullableMap) TypeUtil.anyNullable() else TypeUtil.any() val ensureTypes = FunSpec.builder("ensureTypes").addParameter("doc", type).returns(type) ensureTypes.addStatement("val result = %T()", explicitType) ensureTypes.addStatement("result.putAll(doc)") for (field in holder.fields.values) { - ensureTypes.beginControlFlow("${field.ensureType(typeConversionReturnType,"doc[%N]", field.constantName)}?.let") + ensureTypes.beginControlFlow( + "${ + field.ensureType( + typeConversionReturnType, + "doc[%N], %N", + field.constantName, + field.constantName + ) + }?.let" + ) ensureTypes.addStatement("result[%N] = it", field.constantName) ensureTypes.endControlFlow() } diff --git a/couchbase-entity/src/main/java/com/kaufland/generation/model/EntityGeneration.kt b/couchbase-entity/src/main/java/com/kaufland/generation/model/EntityGeneration.kt index 9a2a89c7..ef0b1f4d 100644 --- a/couchbase-entity/src/main/java/com/kaufland/generation/model/EntityGeneration.kt +++ b/couchbase-entity/src/main/java/com/kaufland/generation/model/EntityGeneration.kt @@ -5,7 +5,12 @@ import com.kaufland.model.entity.EntityHolder import com.kaufland.model.id.DocIdHolder import com.kaufland.util.TypeUtil import com.kaufland.util.TypeUtil.string -import com.squareup.kotlinpoet.* +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.PropertySpec +import com.squareup.kotlinpoet.TypeSpec import com.squareup.kotlinpoet.jvm.throws import kaufland.com.coachbasebinderapi.Entity import kaufland.com.coachbasebinderapi.PersistenceConfig diff --git a/couchbase-entity/src/main/java/com/kaufland/generation/model/TypeConversionMethodsGeneration.kt b/couchbase-entity/src/main/java/com/kaufland/generation/model/TypeConversionMethodsGeneration.kt index e8d68f1c..ca9d5b14 100644 --- a/couchbase-entity/src/main/java/com/kaufland/generation/model/TypeConversionMethodsGeneration.kt +++ b/couchbase-entity/src/main/java/com/kaufland/generation/model/TypeConversionMethodsGeneration.kt @@ -3,42 +3,65 @@ package com.kaufland.generation.model import com.kaufland.util.TypeUtil import com.squareup.kotlinpoet.CodeBlock import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.KModifier import com.squareup.kotlinpoet.TypeVariableName import kaufland.com.coachbasebinderapi.PersistenceConfig +import kaufland.com.coachbasebinderapi.TypeConversionErrorWrapper -class TypeConversionMethodsGeneration(val useSuspend: Boolean) { +class TypeConversionMethodsGeneration(private val useSuspend: Boolean) { fun generate(): Collection { return listOf( - FunSpec.builder(READ_METHOD_NAME) - .addAnnotation(JvmStatic::class) + FunSpec.builder(READ_METHOD_NAME).addAnnotation(JvmStatic::class) .addParameter("value", TypeUtil.any().copy(nullable = true)) + .addParameter("fieldName", TypeUtil.string()) .addParameter("clazz", TypeUtil.classStar()) - .addTypeVariable(TypeVariableName.invoke("T")) - .returns(TypeVariableName.invoke("T?")) - .addCode(CodeBlock.builder().addStatement("val conversion = %T.${getTypeConversionMethod(useSuspend)}.get(clazz)", PersistenceConfig::class).beginControlFlow("if(conversion == null)").addStatement("return value as T?").endControlFlow().addStatement("return conversion?.read(value) as T?").build()) - .build(), + .addTypeVariable(TypeVariableName.invoke("reified T")) + /** has to be inlined because the type inference would otherwise happen in the properties and cause there an classcastexception */ + .addModifiers(KModifier.INLINE).returns(TypeVariableName.invoke("T?")) + /** use empty space otherwise its recognized as a single-expression*/ + .addCode(CodeBlock.of(" return ")).addCode( + CodeBlock.builder().beginControlFlow("try").addStatement( + "val conversion = %T.${getTypeConversionMethod(useSuspend)}[clazz] ?:\n return value as T?", + PersistenceConfig::class + ).addStatement("return conversion.read(value) as T?").endControlFlow() + .beginControlFlow("catch(ex: %T)", java.lang.Exception::class).addStatement( + "%T.${getConnector(useSuspend)}.invokeOnError(${TypeConversionErrorWrapper::class.qualifiedName}(ex, fieldName, value, clazz))", + PersistenceConfig::class + ).addStatement("null").endControlFlow().build() + ).build(), - FunSpec.builder(WRITE_METHOD_NAME) - .addAnnotation(JvmStatic::class) + FunSpec.builder(WRITE_METHOD_NAME).addAnnotation(JvmStatic::class) .addParameter("value", TypeUtil.any().copy(nullable = true)) + .addParameter("fieldName", TypeUtil.string()) .addParameter("clazz", TypeUtil.classStar()) - .addTypeVariable(TypeVariableName.invoke("T")) - .returns(TypeVariableName.invoke("T?")) - .addCode(CodeBlock.builder().addStatement("val conversion = %T.${getTypeConversionMethod(useSuspend)}.get(clazz)", PersistenceConfig::class).beginControlFlow("if(conversion == null)").addStatement("return value as T?").endControlFlow().addStatement("return conversion.write(value) as T?").build()) - .build() - + .addTypeVariable(TypeVariableName.invoke("reified T")) + /** has to be inlined because the type inference would otherwise happen in the properties and cause there an classcastexception */ + .addModifiers(KModifier.INLINE).returns(TypeVariableName.invoke("T?")) + /** use empty space otherwise its recognized as a single-expression*/ + .addCode(CodeBlock.of(" return ")).addCode( + CodeBlock.builder().beginControlFlow("try").addStatement( + "val conversion = %T.${getTypeConversionMethod(useSuspend)}[clazz] ?:\n return value as T?", + PersistenceConfig::class + ).addStatement("return conversion.write(value) as T?").endControlFlow() + .beginControlFlow("catch(ex: %T)", java.lang.Exception::class).addStatement( + "%T.${getConnector(useSuspend)}.invokeOnError(${TypeConversionErrorWrapper::class.qualifiedName}(ex, fieldName, value, clazz))", + PersistenceConfig::class + ).addStatement("null").endControlFlow().build() + ).build() ) } companion object { - - val READ_METHOD_NAME = "read" - - val WRITE_METHOD_NAME = "write" + const val READ_METHOD_NAME = "read" + const val WRITE_METHOD_NAME = "write" private fun getTypeConversionMethod(useSuspend: Boolean): String { - return "${if (useSuspend) "suspendingConnector" else "connector"}.typeConversions" + return "${getConnector(useSuspend)}.typeConversions" + } + + private fun getConnector(useSuspend: Boolean): String { + return if (useSuspend) "suspendingConnector" else "connector" } } } diff --git a/couchbase-entity/src/main/java/com/kaufland/model/EntityFactory.kt b/couchbase-entity/src/main/java/com/kaufland/model/EntityFactory.kt index 6b2ba431..267d487b 100644 --- a/couchbase-entity/src/main/java/com/kaufland/model/EntityFactory.kt +++ b/couchbase-entity/src/main/java/com/kaufland/model/EntityFactory.kt @@ -26,10 +26,7 @@ object EntityFactory { return create( sourceModel, EntityHolder( - annotation.database, - annotation.modifierOpen, - annotation.type, - sourceModel + annotation.database, annotation.modifierOpen, annotation.type, sourceModel ), allWrappers, allBaseModels @@ -41,10 +38,7 @@ object EntityFactory { allWrappers: List ): BaseModelHolder { return create( - sourceModel, - BaseModelHolder(sourceModel), - allWrappers, - emptyMap() + sourceModel, BaseModelHolder(sourceModel), allWrappers, emptyMap() ) as BaseModelHolder } @@ -72,8 +66,7 @@ object EntityFactory { content.reducesModels = createReduceModels(sourceModel, content, allWrappers, allBaseModels) content.abstractParts = sourceModel.abstractParts content.comment = sourceModel.commentAnnotation?.comment ?: arrayOf() - content.deprecated = - sourceModel.deprecatedAnnotation?.let { DeprecatedModel(it) } + content.deprecated = sourceModel.deprecatedAnnotation?.let { DeprecatedModel(it) } addBasedOn(sourceModel, allBaseModels, content) @@ -90,26 +83,20 @@ object EntityFactory { if (it.generateAccessor != null) { content.generateAccessors.add( CblGenerateAccessorHolder( - content.sourceClazzTypeName, - it, - null + content.sourceClazzTypeName, it, null ) ) } } - sourceModel.relevantStaticFields.forEach { if (it.generateAccessor != null) { content.generateAccessors.add( CblGenerateAccessorHolder( - content.sourceClazzTypeName, - null, - it + content.sourceClazzTypeName, null, it ) ) } } - content.docId = docId?.let { DocIdHolder(it, docIdSegments) } ?: content.docId?.apply { customSegmentSource.addAll(docIdSegments) recompile() @@ -124,9 +111,8 @@ object EntityFactory { allWrappers: List, allBaseModels: Map ): List { - - return sourceModel.reduceAnnotations?.let { - return it.map { + sourceModel.reduceAnnotations.let { reduce -> + return reduce.map { ReducedModelHolder( it.namePrefix, it.include.asList(), @@ -137,7 +123,7 @@ object EntityFactory { content ) } - } ?: emptyList() + } } fun addBasedOn( @@ -145,8 +131,7 @@ object EntityFactory { allBaseModels: Map, content: BaseEntityHolder ) { - val basedOnValue = sourceModel.basedOnAnnotation - ?.let { FieldExtractionUtil.typeMirror(it) } + val basedOnValue = sourceModel.basedOnAnnotation?.let { FieldExtractionUtil.typeMirror(it) } basedOnValue?.forEach { type -> allBaseModels[type.toString()]?.let { @@ -190,7 +175,7 @@ object EntityFactory { } private fun parseQueries(sourceModel: ISourceModel, content: BaseEntityHolder) { - val queries = sourceModel.queryAnnotations ?: return + val queries = sourceModel.queryAnnotations for (cblQuery in queries) { content.queries.add(CblQueryHolder(cblQuery)) diff --git a/couchbase-entity/src/main/java/com/kaufland/model/accessor/CblGenerateAccessorHolder.kt b/couchbase-entity/src/main/java/com/kaufland/model/accessor/CblGenerateAccessorHolder.kt index 9371b70c..c27c9bde 100644 --- a/couchbase-entity/src/main/java/com/kaufland/model/accessor/CblGenerateAccessorHolder.kt +++ b/couchbase-entity/src/main/java/com/kaufland/model/accessor/CblGenerateAccessorHolder.kt @@ -1,12 +1,12 @@ package com.kaufland.model.accessor -import com.kaufland.javaToKotlinType import com.kaufland.model.source.SourceMemberField import com.kaufland.model.source.SourceMemberFunction -import com.squareup.kotlinpoet.* -import javax.lang.model.element.VariableElement -import javax.lang.model.type.TypeMirror -import kotlin.coroutines.Continuation +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.PropertySpec +import com.squareup.kotlinpoet.TypeName class CblGenerateAccessorHolder( private val sourceClassTypeName: TypeName, @@ -41,14 +41,6 @@ class CblGenerateAccessorHolder( return null } - private fun isSuspendFunction(varElement: VariableElement): Boolean { - return varElement.asType().toString().contains(Continuation::class.qualifiedName.toString()) - } - - private fun evaluateTypeName(typeMirror: TypeMirror, nullable: Boolean): TypeName { - return typeMirror.asTypeName().javaToKotlinType().copy(nullable = nullable) - } - fun accessorPropertySpec(): PropertySpec? { if (memberProperty != null) { diff --git a/couchbase-entity/src/main/java/com/kaufland/model/entity/BaseEntityHolder.kt b/couchbase-entity/src/main/java/com/kaufland/model/entity/BaseEntityHolder.kt index c7f50bb2..cf3089a1 100644 --- a/couchbase-entity/src/main/java/com/kaufland/model/entity/BaseEntityHolder.kt +++ b/couchbase-entity/src/main/java/com/kaufland/model/entity/BaseEntityHolder.kt @@ -12,8 +12,6 @@ import com.kaufland.model.source.ISourceModel import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.TypeName -import java.util.ArrayList - abstract class BaseEntityHolder(val sourceElement: ISourceModel) : IClassModel by sourceElement { val fields: MutableMap = mutableMapOf() diff --git a/couchbase-entity/src/main/java/com/kaufland/model/field/CblBaseFieldHolder.kt b/couchbase-entity/src/main/java/com/kaufland/model/field/CblBaseFieldHolder.kt index 06e24804..7bb70414 100644 --- a/couchbase-entity/src/main/java/com/kaufland/model/field/CblBaseFieldHolder.kt +++ b/couchbase-entity/src/main/java/com/kaufland/model/field/CblBaseFieldHolder.kt @@ -39,14 +39,27 @@ abstract class CblBaseFieldHolder(val dbField: String, private val mField: Field abstract val fieldType: TypeName fun accessorSuffix(): String { - return WordUtils.uncapitalize(WordUtils.capitalize(dbField.replace("_".toRegex(), " ")).replace(" ".toRegex(), "")) + return WordUtils.uncapitalize( + WordUtils.capitalize(dbField.replace("_".toRegex(), " ")).replace(" ".toRegex(), "") + ) } abstract fun interfaceProperty(isOverride: Boolean = false): PropertySpec - abstract fun property(dbName: String?, possibleOverrides: Set, useMDocChanges: Boolean, deprecated: DeprecatedModel?): PropertySpec - - abstract fun builderSetter(dbName: String?, packageName: String, entitySimpleName: String, useMDocChanges: Boolean, deprecated: DeprecatedModel?): FunSpec? + abstract fun property( + dbName: String?, + possibleOverrides: Set, + useMDocChanges: Boolean, + deprecated: DeprecatedModel? + ): PropertySpec + + abstract fun builderSetter( + dbName: String?, + packageName: String, + entitySimpleName: String, + useMDocChanges: Boolean, + deprecated: DeprecatedModel? + ): FunSpec? abstract fun createFieldConstant(): List } diff --git a/couchbase-entity/src/main/java/com/kaufland/model/field/CblConstantHolder.kt b/couchbase-entity/src/main/java/com/kaufland/model/field/CblConstantHolder.kt index 2fc1458f..3aaed9ad 100644 --- a/couchbase-entity/src/main/java/com/kaufland/model/field/CblConstantHolder.kt +++ b/couchbase-entity/src/main/java/com/kaufland/model/field/CblConstantHolder.kt @@ -32,7 +32,7 @@ class CblConstantHolder(field: Field) : CblBaseFieldHolder(field.name, field) { override fun property(dbName: String?, possibleOverrides: Set, useMDocChanges: Boolean, deprecated: DeprecatedModel?): PropertySpec { val builder = PropertySpec.builder(accessorSuffix(), fieldType, KModifier.PUBLIC, KModifier.OVERRIDE) - .getter(FunSpec.getterBuilder().addStatement("return " + TypeConversionMethodsGeneration.READ_METHOD_NAME + "(mDoc.get(%N), %T::class)!!", constantName, fieldType).build()) + .getter(FunSpec.getterBuilder().addStatement("return " + TypeConversionMethodsGeneration.READ_METHOD_NAME + "(mDoc.get(%N),%N, %T::class)!!", constantName, constantName, fieldType).build()) if (comment.isNotEmpty()) { builder.addKdoc(KDocGeneration.generate(comment)) diff --git a/couchbase-entity/src/main/java/com/kaufland/model/field/CblFieldHolder.kt b/couchbase-entity/src/main/java/com/kaufland/model/field/CblFieldHolder.kt index 10a35b49..4e7d41e9 100644 --- a/couchbase-entity/src/main/java/com/kaufland/model/field/CblFieldHolder.kt +++ b/couchbase-entity/src/main/java/com/kaufland/model/field/CblFieldHolder.kt @@ -4,11 +4,17 @@ import com.kaufland.generation.model.KDocGeneration import com.kaufland.generation.model.TypeConversionMethodsGeneration import com.kaufland.model.deprecated.DeprecatedModel import com.kaufland.util.TypeUtil -import com.squareup.kotlinpoet.* +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.PropertySpec +import com.squareup.kotlinpoet.TypeName import kaufland.com.coachbasebinderapi.Field import org.apache.commons.lang3.StringUtils -class CblFieldHolder(field: Field, allWrappers: List) : CblBaseFieldHolder(field.name, field) { +class CblFieldHolder(field: Field, allWrappers: List) : + CblBaseFieldHolder(field.name, field) { private var subEntityPackage: String? = null @@ -20,13 +26,14 @@ class CblFieldHolder(field: Field, allWrappers: List) : CblBaseFieldHold override var isIterable: Boolean = false - val subEntityTypeName: TypeName + private val subEntityTypeName: TypeName get() = ClassName(subEntityPackage!!, subEntitySimpleName!!) - val isTypeOfSubEntity: Boolean + private val isTypeOfSubEntity: Boolean get() = !StringUtils.isBlank(subEntitySimpleName) - override val fieldType: TypeName = TypeUtil.parseMetaType(typeMirror, isIterable, subEntitySimpleName) + override val fieldType: TypeName = + TypeUtil.parseMetaType(typeMirror, isIterable, subEntitySimpleName) init { if (allWrappers.contains(typeMirror.toString())) { @@ -41,15 +48,25 @@ class CblFieldHolder(field: Field, allWrappers: List) : CblBaseFieldHold } override fun interfaceProperty(isOverride: Boolean): PropertySpec { - val returnType = TypeUtil.parseMetaType(typeMirror, isIterable, subEntitySimpleName).copy(nullable = true) + val returnType = TypeUtil.parseMetaType(typeMirror, isIterable, subEntitySimpleName) + .copy(nullable = true) val modifiers = listOfNotNull(KModifier.PUBLIC, KModifier.OVERRIDE.takeIf { isOverride }) - return PropertySpec.builder(accessorSuffix(), returnType.copy(true), modifiers).mutable(true).build() + return PropertySpec.builder(accessorSuffix(), returnType.copy(true), modifiers) + .mutable(true).build() } - override fun property(dbName: String?, possibleOverrides: Set, useMDocChanges: Boolean, deprecated: DeprecatedModel?): PropertySpec { - val returnType = TypeUtil.parseMetaType(typeMirror, isIterable, subEntitySimpleName).copy(nullable = true) + override fun property( + dbName: String?, + possibleOverrides: Set, + useMDocChanges: Boolean, + deprecated: DeprecatedModel? + ): PropertySpec { + val returnType = TypeUtil.parseMetaType(typeMirror, isIterable, subEntitySimpleName) + .copy(nullable = true) - val propertyBuilder = PropertySpec.builder(accessorSuffix(), returnType.copy(true), KModifier.PUBLIC, KModifier.OVERRIDE).mutable(true) + val propertyBuilder = PropertySpec.builder( + accessorSuffix(), returnType.copy(true), KModifier.PUBLIC, KModifier.OVERRIDE + ).mutable(true) val getter = FunSpec.getterBuilder() val setter = FunSpec.setterBuilder().addParameter("value", String::class) @@ -59,41 +76,69 @@ class CblFieldHolder(field: Field, allWrappers: List) : CblBaseFieldHold val docName = if (useMDocChanges) "mDocChanges" else "mDoc" if (isTypeOfSubEntity) { - val castType = if (isSubEntityIsTypeParam) TypeUtil.listWithMutableMapStringAnyNullable() else TypeUtil.mutableMapStringAnyNullable() + val castType = + if (isSubEntityIsTypeParam) TypeUtil.listWithMutableMapStringAnyNullable() else TypeUtil.mutableMapStringAnyNullable() if (useMDocChanges) { getter.addCode( - CodeBlock.builder().beginControlFlow("if(mDocChanges.containsKey(%N))", constantName) - .addStatement("return·%T.fromMap(mDocChanges.get(%N) as %T)", subEntityTypeName, constantName, castType) - .endControlFlow().build() + CodeBlock.builder() + .beginControlFlow("if(mDocChanges.containsKey(%N))", constantName) + .addStatement( + "return·%T.fromMap(mDocChanges.get(%N) as? %T)", + subEntityTypeName, + constantName, + castType + ).endControlFlow().build() ) } getter.addCode( - CodeBlock.builder() - .beginControlFlow("if(mDoc.containsKey(%N))", constantName) - .addStatement("return·%T.fromMap(mDoc.get(%N) as %T)", subEntityTypeName, constantName, castType) - .endControlFlow().build() + CodeBlock.builder().beginControlFlow("if(mDoc.containsKey(%N))", constantName) + /** In case the key for the subentity is set but the value is null it would result in a classcastexception since null can't be cast to any type*/ + .addStatement( + "return·%T.fromMap(mDoc.get(%N) as? %T)", + subEntityTypeName, + constantName, + castType + ).endControlFlow().build() ) getter.addStatement("return null") - setter.addStatement("%N.put(%N, %T.toMap(value))", docName, constantName, subEntityTypeName) + setter.addStatement( + "%N.put(%N, %T.toMap(value))", docName, constantName, subEntityTypeName + ) } else { val forTypeConversion = evaluateClazzForTypeConversion() if (useMDocChanges) { getter.addCode( - CodeBlock.builder().beginControlFlow("if(mDocChanges.containsKey(%N))", constantName) - .addStatement("return " + TypeConversionMethodsGeneration.READ_METHOD_NAME + "(mDocChanges.get(%N), %T::class)", constantName, forTypeConversion) - .endControlFlow().build() + CodeBlock.builder() + .beginControlFlow("if(mDocChanges.containsKey(%N))", constantName) + .addStatement( + "return " + TypeConversionMethodsGeneration.READ_METHOD_NAME + "(mDocChanges.get(%N), %N, %T::class)", + constantName, constantName, + forTypeConversion + ).endControlFlow().build() ) } - getter.addCode(CodeBlock.builder().beginControlFlow("if(mDoc.containsKey(%N))", constantName).addStatement("return " + TypeConversionMethodsGeneration.READ_METHOD_NAME + "(mDoc.get(%N), %T::class)", constantName, forTypeConversion).endControlFlow().build()) + getter.addCode( + CodeBlock.builder().beginControlFlow("if(mDoc.containsKey(%N))", constantName) + .addStatement( + "return " + TypeConversionMethodsGeneration.READ_METHOD_NAME + "(mDoc.get(%N), %N, %T::class)", + constantName, constantName, + forTypeConversion + ).endControlFlow().build() + ) getter.addStatement("return null") - setter.addStatement("%N.put(%N, " + TypeConversionMethodsGeneration.WRITE_METHOD_NAME + "(value, %T::class))", docName, constantName, forTypeConversion) + setter.addStatement( + "%N.put(%N, " + TypeConversionMethodsGeneration.WRITE_METHOD_NAME + "(value, %N, %T::class))", + docName, + constantName, constantName, + forTypeConversion + ) } if (comment.isNotEmpty()) { @@ -105,21 +150,35 @@ class CblFieldHolder(field: Field, allWrappers: List) : CblBaseFieldHold fun ensureType(resultType: TypeName, format: String, vararg args: Any?): CodeBlock { val forTypeConversion = evaluateClazzForTypeConversion() - return CodeBlock.of("${TypeConversionMethodsGeneration.WRITE_METHOD_NAME}<%T>($format, %T::class)", resultType, *args, forTypeConversion) + return CodeBlock.of( + "${TypeConversionMethodsGeneration.WRITE_METHOD_NAME}<%T>($format, %T::class)", + resultType, + *args, + forTypeConversion + ) } - override fun builderSetter(dbName: String?, packageName: String, entitySimpleName: String, useMDocChanges: Boolean, deprecated: DeprecatedModel?): FunSpec { + override fun builderSetter( + dbName: String?, + packageName: String, + entitySimpleName: String, + useMDocChanges: Boolean, + deprecated: DeprecatedModel? + ): FunSpec { val fieldType = TypeUtil.parseMetaType(typeMirror, isIterable, subEntitySimpleName) - val builder = FunSpec.builder("set" + accessorSuffix().capitalize()) - .addModifiers(KModifier.PUBLIC) - .addParameter("value", fieldType) - .returns(ClassName(packageName, "$entitySimpleName.Builder")) + val builder = + FunSpec.builder("set" + accessorSuffix().capitalize()).addModifiers(KModifier.PUBLIC) + .addParameter("value", fieldType) + .returns(ClassName(packageName, "$entitySimpleName.Builder")) if (this.comment.isNotEmpty()) { builder.addKdoc(KDocGeneration.generate(comment)) } - if (deprecated?.evaluateFieldDeprecationLevel(dbField) == DeprecationLevel.ERROR && deprecated.addDeprecated(dbField, builder)) { + if (deprecated?.evaluateFieldDeprecationLevel(dbField) == DeprecationLevel.ERROR && deprecated.addDeprecated( + dbField, builder + ) + ) { builder.addStatement("throw %T()", UnsupportedOperationException::class) } else { builder.addStatement("obj.${accessorSuffix()} = value") @@ -130,7 +189,9 @@ class CblFieldHolder(field: Field, allWrappers: List) : CblBaseFieldHold } override fun createFieldConstant(): List { - val fieldAccessorConstant = PropertySpec.builder(constantName, String::class, KModifier.FINAL, KModifier.PUBLIC).initializer("%S", dbField).addAnnotation(JvmField::class).build() + val fieldAccessorConstant = + PropertySpec.builder(constantName, String::class, KModifier.FINAL, KModifier.PUBLIC) + .initializer("%S", dbField).addAnnotation(JvmField::class).build() return listOf(fieldAccessorConstant) } diff --git a/couchbase-entity/src/main/java/com/kaufland/model/query/CblQueryHolder.kt b/couchbase-entity/src/main/java/com/kaufland/model/query/CblQueryHolder.kt index 2f1bf00f..65af71b8 100644 --- a/couchbase-entity/src/main/java/com/kaufland/model/query/CblQueryHolder.kt +++ b/couchbase-entity/src/main/java/com/kaufland/model/query/CblQueryHolder.kt @@ -5,7 +5,8 @@ import com.kaufland.generation.model.TypeConversionMethodsGeneration import com.kaufland.model.entity.BaseEntityHolder import com.kaufland.model.field.CblFieldHolder import com.kaufland.util.TypeUtil -import com.squareup.kotlinpoet.* +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.KModifier import com.squareup.kotlinpoet.jvm.throws import kaufland.com.coachbasebinderapi.PersistenceConfig import kaufland.com.coachbasebinderapi.PersistenceException @@ -26,7 +27,11 @@ class CblQueryHolder(private val mQuery: Query) { .addModifiers(KModifier.PUBLIC) .addAnnotation(JvmStatic::class) .throws(PersistenceException::class) - .addStatement("val queryParams = mutableMapOf<%T, %T>()", TypeUtil.string(), TypeUtil.any()) + .addStatement( + "val queryParams = mutableMapOf<%T, %T>()", + TypeUtil.string(), + TypeUtil.any() + ) .returns(TypeUtil.list(entityHolder.entityTypeName)) if (useSuspend) { @@ -40,27 +45,44 @@ class CblQueryHolder(private val mQuery: Query) { builder.addQueryParamComparisonStatement(this, dbField) } entityHolder.fieldConstants[it]?.apply { - builder.addStatement("queryParams[%N] = %N", constantName, constantValueAccessorName) + builder.addStatement( + "queryParams[%N] = %N", + constantName, + constantValueAccessorName + ) } } - builder.beginControlFlow("return %T.${queryDocumentMethod(useSuspend)}(%S, queryParams, null, %N).map", PersistenceConfig::class, dbName, CblReduceGeneration.PROPERTY_ONLY_INCLUDES) + builder.beginControlFlow( + "return %T.${queryDocumentMethod(useSuspend)}(%S, queryParams, null, %N).map", + PersistenceConfig::class, + dbName, + CblReduceGeneration.PROPERTY_ONLY_INCLUDES + ) builder.addStatement("%T(it)", entityHolder.entityTypeName) builder.endControlFlow() return builder.build() } - private fun FunSpec.Builder.addQueryParamComparisonStatement(fieldHolder: CblFieldHolder, value: String) { + private fun FunSpec.Builder.addQueryParamComparisonStatement( + fieldHolder: CblFieldHolder, + value: String + ) { val classForTypeConversion = fieldHolder.evaluateClazzForTypeConversion() addStatement( - "queryParams[%N] = ${TypeConversionMethodsGeneration.WRITE_METHOD_NAME}(%N, %T::class) ?:\nthrow PersistenceException(\"Invalid·type-conversion:·value·must·not·be·null\")", + "queryParams[%N] = ${TypeConversionMethodsGeneration.WRITE_METHOD_NAME}(%N, %N, %T::class) ?:\nthrow PersistenceException(\"Invalid·type-conversion:·value·must·not·be·null\")", fieldHolder.constantName, value, + fieldHolder.constantName, classForTypeConversion ) } - private val queryFunName: String = "findBy${fields.joinToString(separator = "And") { WordUtils.capitalize(it.replace("_", " ")).replace(" ", "") }}" + private val queryFunName: String = "findBy${ + fields.joinToString(separator = "And") { + WordUtils.capitalize(it.replace("_", " ")).replace(" ", "") + } + }" companion object { diff --git a/couchbase-entity/src/test/java/com/kaufland/testdata/TestDataHelper.kt b/couchbase-entity/src/test/java/com/kaufland/testdata/TestDataHelper.kt index 5a4ad615..04311427 100644 --- a/couchbase-entity/src/test/java/com/kaufland/testdata/TestDataHelper.kt +++ b/couchbase-entity/src/test/java/com/kaufland/testdata/TestDataHelper.kt @@ -9,7 +9,7 @@ object TestDataHelper { fun clazzAsJavaFileObjects(clazz: String): SourceFile { val className = "$clazz.kt" - val file = javaClass.classLoader.getResource(className).file - return SourceFile.kotlin(className, "$PACKAGE_DECLARE${File(file).readText()}") + val file = javaClass.classLoader.getResource(className)?.file + return SourceFile.kotlin(className, "$PACKAGE_DECLARE${file?.let { File(it).readText() }}") } } diff --git a/couchbase-entity/src/test/resources/EntityWithDeprecatedFields.kt b/couchbase-entity/src/test/resources/EntityWithDeprecatedFields.kt index 4f580898..d45b2a87 100644 --- a/couchbase-entity/src/test/resources/EntityWithDeprecatedFields.kt +++ b/couchbase-entity/src/test/resources/EntityWithDeprecatedFields.kt @@ -8,9 +8,9 @@ import java.lang.String @Entity(database = "mydb_db") @Fields( -Field(name = "name", type = String::class), - Field(name = "type", type = String::class, defaultValue = "entityWithQueries", readonly = true), -Field(name = "identifiers", type = String::class, list = true) + Field(name = "name", type = String::class), + Field(name = "type", type = String::class, defaultValue = "entityWithQueries", readonly = true), + Field(name = "identifiers", type = String::class, list = true) ) @Deprecated(fields = [DeprecatedField("name", replacedBy = "identifiers")]) open class EntityWithDeprecatedFields \ No newline at end of file diff --git a/couchbase-entity/src/test/resources/EntityWithQueries.kt b/couchbase-entity/src/test/resources/EntityWithQueries.kt index eeeff1db..7de22851 100644 --- a/couchbase-entity/src/test/resources/EntityWithQueries.kt +++ b/couchbase-entity/src/test/resources/EntityWithQueries.kt @@ -3,16 +3,15 @@ import kaufland.com.coachbasebinderapi.Field import kaufland.com.coachbasebinderapi.Fields import kaufland.com.coachbasebinderapi.query.Queries import kaufland.com.coachbasebinderapi.query.Query -import java.lang.String @Entity(database = "mydb_db") @Fields( -Field(name = "name", type = String::class), - Field(name = "type", type = String::class, defaultValue = "entityWithQueries", readonly = true), -Field(name = "identifiers", type = String::class, list = true) + Field(name = "name", type = String::class), + Field(name = "type", type = String::class, defaultValue = "entityWithQueries", readonly = true), + Field(name = "identifiers", type = String::class, list = true) ) @Queries( - Query(fields = ["type"]) + Query(fields = ["type"]) ) open class EntityWithQueries \ No newline at end of file diff --git a/demo/build.gradle b/demo/build.gradle index 5fb8f300..b3b0f930 100644 --- a/demo/build.gradle +++ b/demo/build.gradle @@ -1,6 +1,6 @@ buildscript { - ext.kotlin_version = '1.7.0' + ext.kotlin_version = '1.7.10' repositories { google() mavenCentral() @@ -9,7 +9,7 @@ buildscript { classpath 'com.android.tools.build:gradle:4.2.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - //TODO enable to localy test plugin + //TODO enable to locally test plugin // classpath "com.fasterxml.jackson.module:jackson-module-kotlin:2.11.2" // classpath fileTree(dir:'../couchbase-entity-versioning-plugin/build/libs', include: ['*.jar']) // classpath fileTree(dir:'../couchbase-entity-api/build/libs', include: ['*.jar']) @@ -32,20 +32,16 @@ apply plugin: 'kotlin-kapt' //} android { + defaultConfig { applicationId "kaufland.com.demo" buildToolsVersion = "30.0.2" - compileSdkVersion = 30 - minSdkVersion = 24 - targetSdkVersion = 28 + compileSdkVersion 33 + minSdkVersion 24 + targetSdkVersion 33 versionCode 1 versionName "1.0" testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' - javaCompileOptions { - annotationProcessorOptions { - includeCompileClasspath = true - } - } } buildTypes { release { @@ -91,11 +87,8 @@ kapt { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - androidTestImplementation('androidx.test.espresso:espresso-core:3.1.0', { - exclude group: 'com.android.support', module: 'support-annotations' - }) - implementation 'androidx.appcompat:appcompat:1.0.0' - implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.appcompat:appcompat:1.5.1' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation project(path: ':couchbase-entity-api') implementation project(path: ':couchbase-entity-connector') kapt project(path: ':couchbase-entity') @@ -104,5 +97,13 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" // Test dependencies - testImplementation 'junit:junit:4.12' + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.mockito:mockito-core:3.10.0' + implementation group: 'ch.qos.logback', name: 'logback-core', version: '1.4.4' + implementation group: 'org.slf4j', name: 'slf4j-api', version: '2.0.1' + testImplementation 'ch.qos.logback:logback-classic:1.4.4' + kapt 'org.apache.logging.log4j:log4j-api:2.19.0' + androidTestImplementation('androidx.test.espresso:espresso-core:3.1.0', { + exclude group: 'com.android.support', module: 'support-annotations' + }) } diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index 905080b3..643c9448 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ + package="kaufland.com.demo"> - + - + - + diff --git a/demo/src/main/java/kaufland/com/demo/Application.kt b/demo/src/main/java/kaufland/com/demo/Application.kt index 0c81017f..25d1dfe5 100755 --- a/demo/src/main/java/kaufland/com/demo/Application.kt +++ b/demo/src/main/java/kaufland/com/demo/Application.kt @@ -8,6 +8,7 @@ import com.couchbase.lite.DatabaseConfiguration import kaufland.com.coachbasebinderapi.PersistenceConfig import kaufland.com.coachbasebinderapi.PersistenceException import kaufland.com.coachbasebinderapi.TypeConversion +import kaufland.com.coachbasebinderapi.TypeConversionErrorWrapper import kaufland.com.couchbaseentityconnector.Couchbase2Connector import kaufland.com.demo.customtypes.GenerateClassName import kaufland.com.demo.customtypes.GenerateClassNameConversion @@ -47,10 +48,20 @@ class Application : android.app.Application() { override val typeConversions: Map, TypeConversion> get() { - val mutableMapOf = mutableMapOf, TypeConversion>(GenerateClassName::class to GenerateClassNameConversion()) + val mutableMapOf = + mutableMapOf, TypeConversion>(GenerateClassName::class to GenerateClassNameConversion()) mutableMapOf.putAll(super.typeConversions) return mutableMapOf } + + override fun invokeOnError(errorWrapper: TypeConversionErrorWrapper) { + if (errorWrapper.exception is java.lang.ClassCastException) { + Log.e( + TAG, + "Data type manipulated: Tried to cast ${errorWrapper.value} into ${errorWrapper.`class`}" + ) + } else throw errorWrapper.exception + } }) createMockArticle() } @@ -67,22 +78,58 @@ class Application : android.app.Application() { private fun createMockArticle() { try { - ProductEntity.create().builder().setName("Beer").setComments(listOf(UserCommentWrapper.create().builder().setComment("very awesome").exit(), UserCommentWrapper.create().builder().setComment("tasty").exit())).setImage(Blob("image/jpeg", resources.openRawResource(R.raw.ic_kaufland_placeholder))).exit().save() - ProductEntity.create().builder().setName("Beer (no alcohol)").setComments(listOf(UserCommentWrapper.create().builder().setComment("very bad").exit(), UserCommentWrapper.create().builder().setComment("not tasty").setAge(99).exit())).setImage(Blob("image/jpeg", resources.openRawResource(R.raw.ic_kaufland_placeholder))).exit().save() - ProductEntity.create().builder().setName("Wodka").setComments(listOf(UserCommentWrapper.create().builder().setComment("feeling like touch the sky").exit())).setImage(Blob("image/jpeg", resources.openRawResource(R.raw.ic_kaufland_placeholder))).exit().save() - ProductEntity.create().builder().setName("Gin").setComments(listOf(UserCommentWrapper.create().builder().setComment("hipster drink but great").exit(), UserCommentWrapper.create().builder().setComment("tasty!!!").exit())).setImage(Blob("image/jpeg", resources.openRawResource(R.raw.ic_kaufland_placeholder))).exit().save() - ProductEntity.create().builder().setName("Apple").setComments(listOf(UserCommentWrapper.create().builder().setComment("mhmhmh tasty!").exit(), UserCommentWrapper.create().builder().setComment("dont like it").exit())).setImage(Blob("image/jpeg", resources.openRawResource(R.raw.ic_kaufland_placeholder))).exit().save() - ProductEntity.create().builder().setName("Tomatoes").setComments(listOf(UserCommentWrapper.create().builder().setComment("don't like there color").exit(), UserCommentWrapper.create().builder().setComment("worst experience ever!!").exit())).setImage(Blob("image/jpeg", resources.openRawResource(R.raw.ic_kaufland_placeholder))).exit().save() + ProductEntity.create().builder().setName("Beer").setComments( + listOf( + UserCommentWrapper.create().builder().setComment("very awesome").exit(), + UserCommentWrapper.create().builder().setComment("tasty").exit() + ) + ).setImage(Blob("image/jpeg", resources.openRawResource(R.raw.ic_kaufland_placeholder))) + .exit().save() + ProductEntity.create().builder().setName("Beer (no alcohol)").setComments( + listOf( + UserCommentWrapper.create().builder().setComment("very bad").exit(), + UserCommentWrapper.create().builder().setComment("not tasty").setAge(99).exit() + ) + ).setImage(Blob("image/jpeg", resources.openRawResource(R.raw.ic_kaufland_placeholder))) + .exit().save() + ProductEntity.create().builder().setName("Wodka").setComments( + listOf( + UserCommentWrapper.create().builder().setComment("feeling like touch the sky") + .exit() + ) + ).setImage(Blob("image/jpeg", resources.openRawResource(R.raw.ic_kaufland_placeholder))) + .exit().save() + ProductEntity.create().builder().setName("Gin").setComments( + listOf( + UserCommentWrapper.create().builder().setComment("hipster drink but great") + .exit(), + UserCommentWrapper.create().builder().setComment("tasty!!!").exit() + ) + ).setImage(Blob("image/jpeg", resources.openRawResource(R.raw.ic_kaufland_placeholder))) + .exit().save() + ProductEntity.create().builder().setName("Apple").setComments( + listOf( + UserCommentWrapper.create().builder().setComment("mhmhmh tasty!").exit(), + UserCommentWrapper.create().builder().setComment("dont like it").exit() + ) + ).setImage(Blob("image/jpeg", resources.openRawResource(R.raw.ic_kaufland_placeholder))) + .exit().save() + ProductEntity.create().builder().setName("Tomatoes").setComments( + listOf( + UserCommentWrapper.create().builder().setComment("don't like there color") + .exit(), + UserCommentWrapper.create().builder().setComment("worst experience ever!!") + .exit() + ) + ).setImage(Blob("image/jpeg", resources.openRawResource(R.raw.ic_kaufland_placeholder))) + .exit().save() } catch (e: PersistenceException) { e.printStackTrace() } } companion object { - private val TAG = Application::class.java.name - - @JvmField - val DB = "mydb_db" + const val DB = "mydb_db" } } diff --git a/demo/src/main/java/kaufland/com/demo/entity/Product.kt b/demo/src/main/java/kaufland/com/demo/entity/Product.kt index 6bb2c35a..e53ab4f2 100644 --- a/demo/src/main/java/kaufland/com/demo/entity/Product.kt +++ b/demo/src/main/java/kaufland/com/demo/entity/Product.kt @@ -1,7 +1,15 @@ package kaufland.com.demo.entity import com.couchbase.lite.Blob -import kaufland.com.coachbasebinderapi.* +import kaufland.com.coachbasebinderapi.Comment +import kaufland.com.coachbasebinderapi.DocId +import kaufland.com.coachbasebinderapi.DocIdSegment +import kaufland.com.coachbasebinderapi.Entity +import kaufland.com.coachbasebinderapi.Field +import kaufland.com.coachbasebinderapi.Fields +import kaufland.com.coachbasebinderapi.MapWrapper +import kaufland.com.coachbasebinderapi.Reduce +import kaufland.com.coachbasebinderapi.Reduces import kaufland.com.coachbasebinderapi.query.Queries import kaufland.com.coachbasebinderapi.query.Query @@ -9,16 +17,30 @@ import kaufland.com.coachbasebinderapi.query.Query @MapWrapper @Comment(["Hey, I just met you and this is crazy", "But here's my documentation, so read it maybe"]) @Fields( - Field(name = "type", type = String::class, defaultValue = "product", readonly = true, comment = ["Document type"]), - Field(name = "name", type = String::class, comment = ["contains the product name.", "and other infos"]), - Field(name = "comments", type = UserComment::class, list = true, comment = ["I'm also comfortable with pseudo %2D placeholders"]), + Field( + name = "type", + type = String::class, + defaultValue = "product", + readonly = true, + comment = ["Document type"] + ), + Field( + name = "name", + type = String::class, + comment = ["contains the product name.", "and other infos"] + ), + Field( + name = "comments", + type = UserComment::class, + list = true, + comment = ["I'm also comfortable with pseudo %2D placeholders"] + ), Field(name = "image", type = Blob::class), Field(name = "identifiers", type = String::class, list = true), - Field(name = "category", type = ProductCategory::class) + Field(name = "category", type = ProductCategory::class), ) @Queries( - Query(fields = ["type"]), - Query(fields = ["type", "category"]) + Query(fields = ["type"]), Query(fields = ["type", "category"]) ) @Reduces( Reduce(namePrefix = "Light", include = ["name", "type", "category", "image"]), diff --git a/demo/src/main/java/kaufland/com/demo/entity/UserComment.kt b/demo/src/main/java/kaufland/com/demo/entity/UserComment.kt index 598b4428..f7565827 100644 --- a/demo/src/main/java/kaufland/com/demo/entity/UserComment.kt +++ b/demo/src/main/java/kaufland/com/demo/entity/UserComment.kt @@ -8,6 +8,6 @@ import kaufland.com.coachbasebinderapi.Field @Fields( Field(name = "comment", type = String::class), Field(name = "user", type = String::class, defaultValue = "anonymous"), - Field("age", type = Integer::class, defaultValue = "0") + Field("age", type = Int::class, defaultValue = "0") ) open class UserComment diff --git a/demo/src/test/java/kaufland/com/demo/TypeConversionTest.kt b/demo/src/test/java/kaufland/com/demo/TypeConversionTest.kt index c0557d66..e3c6327c 100644 --- a/demo/src/test/java/kaufland/com/demo/TypeConversionTest.kt +++ b/demo/src/test/java/kaufland/com/demo/TypeConversionTest.kt @@ -2,6 +2,7 @@ package kaufland.com.demo import kaufland.com.coachbasebinderapi.PersistenceConfig import kaufland.com.coachbasebinderapi.TypeConversion +import kaufland.com.coachbasebinderapi.TypeConversionErrorWrapper import kaufland.com.demo.customtypes.GenerateClassName import kaufland.com.demo.customtypes.GenerateClassNameConversion import kaufland.com.demo.entity.TestClassEntity @@ -22,20 +23,30 @@ class TypeConversionTest { PersistenceConfig.configure(object : PersistenceConfig.Connector { override val typeConversions: Map, TypeConversion> - get() { - val mutableMapOf = mutableMapOf, TypeConversion>(GenerateClassName::class to GenerateClassNameConversion()) - return mutableMapOf - } + get() = mutableMapOf, TypeConversion>(GenerateClassName::class to GenerateClassNameConversion()) - override fun getDocument(id: String, dbName: String, onlyInclude: List?): Map { + override fun getDocument( + id: String, + dbName: String, + onlyInclude: List? + ): Map { return emptyMap() } - override fun getDocuments(ids: List, dbName: String, onlyInclude: List?): List?> { + override fun getDocuments( + ids: List, + dbName: String, + onlyInclude: List? + ): List?> { TODO("Not yet implemented") } - override fun queryDoc(dbName: String, queryParams: Map, limit: Int?, onlyInclude: List?): List> { + override fun queryDoc( + dbName: String, + queryParams: Map, + limit: Int?, + onlyInclude: List? + ): List> { throw Exception("Should not be called") } @@ -43,9 +54,17 @@ class TypeConversionTest { throw Exception("should not called") } - override fun upsertDocument(document: MutableMap, id: String?, dbName: String): Map { + override fun upsertDocument( + document: MutableMap, + id: String?, + dbName: String + ): Map { throw Exception("should not called") } + + override fun invokeOnError(errorWrapper: TypeConversionErrorWrapper) { + throw errorWrapper.exception + } }) } @@ -53,14 +72,16 @@ class TypeConversionTest { @Throws(Exception::class) fun testCustomTypeConversion() { - val test = mapOf(TestClassEntity.CLAZZ_NAME to TypeConversionTest::class.simpleName!!) - - val testWithPreFill = TestClassEntity(test) - - Assert.assertEquals(TypeConversionTest::class.simpleName!!, testWithPreFill.toMap()[TestClassEntity.CLAZZ_NAME]) - + val test = + mapOf(TestClassEntity.CLAZZ_NAME to TypeConversionTest::class.simpleName!!) + Assert.assertEquals( + TypeConversionTest::class.simpleName!!, + TestClassEntity(test).toMap()[TestClassEntity.CLAZZ_NAME] + ) val testClassEntity1 = TestClassEntity.create() - - Assert.assertEquals(TestClassEntity::class.simpleName!!, testClassEntity1.toMap()[TestClassEntity.CLAZZ_NAME]) + Assert.assertEquals( + TestClassEntity::class.simpleName!!, + testClassEntity1.toMap()[TestClassEntity.CLAZZ_NAME] + ) } } diff --git a/demo/src/test/java/kaufland/com/demo/UnitTestConnector.kt b/demo/src/test/java/kaufland/com/demo/UnitTestConnector.kt index e4e7a870..bd406fd7 100644 --- a/demo/src/test/java/kaufland/com/demo/UnitTestConnector.kt +++ b/demo/src/test/java/kaufland/com/demo/UnitTestConnector.kt @@ -2,6 +2,7 @@ package kaufland.com.demo import kaufland.com.coachbasebinderapi.PersistenceConfig import kaufland.com.coachbasebinderapi.TypeConversion +import kaufland.com.coachbasebinderapi.TypeConversionErrorWrapper import kotlin.reflect.KClass open class UnitTestConnector( @@ -11,15 +12,28 @@ open class UnitTestConnector( // Do Nothing } - override fun getDocument(id: String, dbName: String, onlyInclude: List?): Map? { + override fun getDocument( + id: String, + dbName: String, + onlyInclude: List? + ): Map? { return null } - override fun getDocuments(ids: List, dbName: String, onlyInclude: List?): List?> { + override fun getDocuments( + ids: List, + dbName: String, + onlyInclude: List? + ): List?> { return emptyList() } - override fun queryDoc(dbName: String, queryParams: Map, limit: Int?, onlyInclude: List?): List> { + override fun queryDoc( + dbName: String, + queryParams: Map, + limit: Int?, + onlyInclude: List? + ): List> { return emptyList() } @@ -30,4 +44,8 @@ open class UnitTestConnector( ): Map { return document } + + override fun invokeOnError(errorWrapper: TypeConversionErrorWrapper) { + throw errorWrapper.exception + } } diff --git a/demo/src/test/java/kaufland/com/demo/entity/ProductEntityTest.kt b/demo/src/test/java/kaufland/com/demo/entity/ProductEntityTest.kt index dfa5f888..03f425d2 100644 --- a/demo/src/test/java/kaufland/com/demo/entity/ProductEntityTest.kt +++ b/demo/src/test/java/kaufland/com/demo/entity/ProductEntityTest.kt @@ -2,21 +2,62 @@ package kaufland.com.demo.entity import kaufland.com.coachbasebinderapi.PersistenceConfig import kaufland.com.coachbasebinderapi.TypeConversion +import kaufland.com.coachbasebinderapi.TypeConversionErrorWrapper import kaufland.com.demo.UnitTestConnector import kaufland.com.demo.entity.ProductCategory.AMAZING_PRODUCT -import org.junit.Assert.* +import kaufland.com.demo.logger.TestAppender +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull import org.junit.BeforeClass import org.junit.Test +import org.mockito.internal.matchers.Null +import org.slf4j.LoggerFactory import kotlin.reflect.KClass -private val typeConversions: Map, TypeConversion> = mapOf( - ProductCategory::class to ProductCategoryTypeConversion -) +private val typeConversions: Map, TypeConversion> = + mapOf(ProductCategory::class to ProductCategoryTypeConversion) +private val logger = + LoggerFactory.getLogger(ProductEntityTestConnector::class.java) as ch.qos.logback.classic.Logger + +private val dataTypeErrorMsg: (String?, String?, String?) -> String + get() = { fieldName, value, `class` -> + "Field $fieldName manipulated: Tried to cast $value into $`class`" + } object ProductEntityTestConnector : UnitTestConnector(typeConversions) { - override fun queryDoc(dbName: String, queryParams: Map, limit: Int?, onlyInclude: List?): List> { + init { + TestAppender().run { + name = this::class.java.simpleName + logger.addAppender(this) + start() + } + } + + override fun queryDoc( + dbName: String, + queryParams: Map, + limit: Int?, + onlyInclude: List? + ): List> { return listOf(queryParams) } + + override fun invokeOnError(errorWrapper: TypeConversionErrorWrapper) { + if (errorWrapper.exception is ClassCastException) { + + logger.error( + dataTypeErrorMsg.invoke( + errorWrapper.fieldName, + if (errorWrapper.value != null) { + errorWrapper.value?.javaClass?.kotlin?.simpleName ?: "" + } else { + Null.NULL.toString().lowercase() + }, + errorWrapper.`class`.simpleName + ) + ) + } + } } class ProductEntityTest { @@ -32,12 +73,30 @@ class ProductEntityTest { @Test fun `findByTypeAndCategory should use the expected query-params`() { val category = AMAZING_PRODUCT - val result = ProductEntity.findByTypeAndCategory(category) - assertEquals(result.size, 1) val queryParams = result.first() assertEquals(AMAZING_PRODUCT, queryParams.category) assertEquals(ProductEntity.DOC_TYPE, queryParams.type) } + + /** + * Can happen if combined db data is changed wilfully. + */ + @Test + fun `data type changed at runtime test suppress exception`() { + ProductEntity.write(1, EXAMPLE_TYPE, String::class) + assertEquals( + (logger.getAppender(TestAppender::class.java.simpleName) as TestAppender).lastLoggedEvent?.message, + dataTypeErrorMsg.invoke(EXAMPLE_TYPE, 1::class.simpleName, String::class.simpleName) + ) + } + + @Test + fun `data type consistent`() { + ProductEntity.write(1, EXAMPLE_TYPE, Int::class) + assertNull((logger.getAppender(TestAppender::class.java.simpleName) as TestAppender).lastLoggedEvent?.message) + } } + +private const val EXAMPLE_TYPE = "EXAMPLE_TYPE" diff --git a/demo/src/test/java/kaufland/com/demo/logger/TestAppender.kt b/demo/src/test/java/kaufland/com/demo/logger/TestAppender.kt new file mode 100644 index 00000000..5713b56c --- /dev/null +++ b/demo/src/test/java/kaufland/com/demo/logger/TestAppender.kt @@ -0,0 +1,15 @@ +package kaufland.com.demo.logger + +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.AppenderBase + +class TestAppender : AppenderBase() { + private val events: MutableList = mutableListOf() + + val lastLoggedEvent + get() = events.lastOrNull() + + override fun append(eventObject: ILoggingEvent) { + events.add(eventObject) + } +} diff --git a/demo/src/test/java/kaufland/com/demo/mapper/DummyMapperSourceTest.kt b/demo/src/test/java/kaufland/com/demo/mapper/DummyMapperSourceTest.kt index f7a03ea6..6c15acd9 100644 --- a/demo/src/test/java/kaufland/com/demo/mapper/DummyMapperSourceTest.kt +++ b/demo/src/test/java/kaufland/com/demo/mapper/DummyMapperSourceTest.kt @@ -2,8 +2,9 @@ package kaufland.com.demo.mapper import kaufland.com.coachbasebinderapi.PersistenceConfig import kaufland.com.coachbasebinderapi.TypeConversion +import kaufland.com.coachbasebinderapi.TypeConversionErrorWrapper import kaufland.com.demo.entity.ProductEntity -import org.junit.Assert.* +import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test import java.math.BigDecimal @@ -36,6 +37,10 @@ class DummyMapperSourceTest { override fun upsertDocument(document: MutableMap, id: String?, dbName: String): Map { TODO("Not yet implemented") } + + override fun invokeOnError(errorWrapper: TypeConversionErrorWrapper) { + TODO("Not yet implemented") + } }) } diff --git a/gradle.properties b/gradle.properties index 5465fec0..d77281d1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,3 @@ android.enableJetifier=true -android.useAndroidX=true \ No newline at end of file +android.useAndroidX=true +org.gradle.jvmargs=-Dkotlin.daemon.jvm.options=--illegal-access=permit \ No newline at end of file