Skip to content

Commit

Permalink
fixes #182 Native Custom codec is not used for update
Browse files Browse the repository at this point in the history
  • Loading branch information
zigzago committed Mar 1, 2020
1 parent 55b21c9 commit bc7cecd
Show file tree
Hide file tree
Showing 9 changed files with 183 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import org.litote.kmongo.JacksonMappingCategory
import org.litote.kmongo.KMongoRootTest
import org.litote.kmongo.NativeMappingCategory
import org.litote.kmongo.SerializationMappingCategory
import org.litote.kmongo.model.Friend
import java.lang.reflect.ParameterizedType
import kotlin.reflect.KClass

/**
Expand All @@ -43,7 +43,7 @@ open class KMongoCoroutineBaseTest<T : Any> : KMongoRootTest() {
inline fun <reified T : Any> getCollection(): MongoCollection<T> = rule.getCollection<T>()

@Suppress("UNCHECKED_CAST")
open fun getDefaultCollectionClass(): KClass<T>
= Friend::class as KClass<T>
open fun getDefaultCollectionClass(): KClass<T> =
((this::class.java.genericSuperclass as ParameterizedType).actualTypeArguments[0] as Class<T>).kotlin

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright (C) 2016/2020 Litote
*
* 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.
*/

package org.litote.kmongo.coroutine.issues

import kotlinx.coroutines.runBlocking
import org.bson.BsonReader
import org.bson.BsonWriter
import org.bson.codecs.Codec
import org.bson.codecs.DecoderContext
import org.bson.codecs.EncoderContext
import org.bson.codecs.pojo.annotations.BsonId
import org.junit.Test
import org.junit.experimental.categories.Category
import org.litote.kmongo.Id
import org.litote.kmongo.JacksonMappingCategory
import org.litote.kmongo.NativeMappingCategory
import org.litote.kmongo.coroutine.KMongoCoroutineBaseTest
import org.litote.kmongo.coroutine.findOneById
import org.litote.kmongo.coroutine.insertOne
import org.litote.kmongo.coroutine.updateOne
import org.litote.kmongo.newId
import org.litote.kmongo.util.ObjectMappingConfiguration
import java.util.Currency

object CurrencyCodec : Codec<Currency> {

override fun getEncoderClass(): Class<Currency> =
Currency::class.java

override fun encode(writer: BsonWriter, value: Currency, encoderContext: EncoderContext) {
writer.writeString(value.currencyCode)
}

override fun decode(reader: BsonReader, decoderContext: DecoderContext): Currency =
Currency.getInstance(reader.readString())
}

data class Account(
@BsonId
val id: Id<Account> = newId(),
val currency: Currency
)

/**
*
*/
@Category(JacksonMappingCategory::class, NativeMappingCategory::class)
class Issue182CustomCodec : KMongoCoroutineBaseTest<Account>() {

@Test
fun testCustomCodec() = runBlocking {
ObjectMappingConfiguration.addCustomCodec(CurrencyCodec)

val account = Account(currency = Currency.getInstance("USD"))

col.insertOne(account)

assert(col.findOneById(account.id)?.currency == Currency.getInstance("USD"))

//check this does not fail
col.updateOne(
account.copy(
currency = Currency.getInstance("EUR")
)
)
Unit
}
}
5 changes: 5 additions & 0 deletions kmongo-kdoc/docs/object-mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ whatever is the timezone of the saved date, you will get an UTC date when loadin

So, in this case, the loaded date will not usually be equals to the saved date - the Instant part of the date of course is the same. If you need to check equality, use a method like ```OffsetDateTime#withOffsetSameInstant``` on the loaded date.

## Register a custom mongo Codec

Use [ObjectMappingConfiguration.addCustomCodec](https://litote.org/kmongo/dokka/kmongo/org.litote.kmongo.util/-object-mapping-configuration/index.html) function in order to register a custom codec.
You have also the option to use [custom Jackson modules](https://litote.org/kmongo/object-mapping/#register-a-custom-jackson-module) or custom [kotlinx.serialization modules](https://litote.org/kmongo/object-mapping/#additional-modules-and-serializers).

## How to choose the mapping engine

### The Jackson choice
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

package org.litote.kmongo.pojo

import com.mongodb.MongoClientSettings
import com.mongodb.MongoClientSettings.getDefaultCodecRegistry
import org.bson.BsonDocument
import org.bson.BsonDocumentWriter
import org.bson.codecs.EncoderContext
Expand All @@ -33,6 +33,7 @@ import org.litote.kmongo.service.ClassMappingType
import org.litote.kmongo.service.ClassMappingTypeService
import org.litote.kmongo.util.ObjectMappingConfiguration
import java.io.StringWriter
import kotlin.LazyThreadSafetyMode.PUBLICATION
import kotlin.reflect.KClass
import kotlin.reflect.KProperty
import kotlin.reflect.KProperty1
Expand All @@ -45,15 +46,30 @@ import kotlin.reflect.jvm.javaGetter
*/
internal class PojoClassMappingTypeService : ClassMappingTypeService {

private val internalCodecRegistry: CodecRegistry by lazy(PUBLICATION) {
ClassMappingType.codecRegistry(
getDefaultCodecRegistry(),
codecRegistry
)
}

private val internalNullCodecRegistry: CodecRegistry by lazy(PUBLICATION) {
ClassMappingType.codecRegistry(
getDefaultCodecRegistry(),
codecRegistryWithNullSerialization
)
}

override fun priority(): Int {
return 0
}

override fun filterIdToBson(obj: Any, filterNullProperties: Boolean): BsonDocument {
val bsonDocument = BsonDocument()
val bsonWriter = BsonDocumentWriter(bsonDocument)
(if (filterNullProperties) codecRegistry else codecRegistryWithNullSerialization)
.get(obj.javaClass)
val codec = if (filterNullProperties) internalCodecRegistry else internalNullCodecRegistry

codec.get(obj.javaClass)
?.encode(
bsonWriter,
obj,
Expand Down Expand Up @@ -82,7 +98,7 @@ internal class PojoClassMappingTypeService : ClassMappingTypeService {
//create a fake document to bypass bson writer built-in checks
jsonWriter.writeStartDocument()
jsonWriter.writeName("tmp")
ClassMappingType.codecRegistry(MongoClientSettings.getDefaultCodecRegistry())
internalCodecRegistry
.get(obj.javaClass)
?.encode(
jsonWriter,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,12 @@
*/
package org.litote.kmongo.serialization

import com.mongodb.MongoClientSettings
import com.mongodb.MongoClientSettings.getDefaultCodecRegistry
import kotlinx.serialization.ImplicitReflectionSerializer
import kotlinx.serialization.SerialName
import org.bson.BsonDocument
import org.bson.BsonDocumentWriter
import org.bson.codecs.EncoderContext
import org.bson.codecs.configuration.CodecRegistries
import org.bson.codecs.configuration.CodecRegistry
import org.bson.json.JsonMode
import org.bson.json.JsonWriter
Expand All @@ -41,15 +40,18 @@ class SerializationClassMappingTypeService : ClassMappingTypeService {

override fun priority(): Int = 200

private val coreCodecRegistry: CodecRegistry by lazy(PUBLICATION) {
SerializationCodecRegistry(configuration)
}
private val codecRegistry: CodecRegistry by lazy(PUBLICATION) {
CodecRegistries.fromRegistries(
MongoClientSettings.getDefaultCodecRegistry(),
codecRegistry(
getDefaultCodecRegistry(),
SerializationCodecRegistry(configuration)
)
}
private val codecRegistryWithNonEncodeNull: CodecRegistry by lazy(PUBLICATION) {
CodecRegistries.fromRegistries(
MongoClientSettings.getDefaultCodecRegistry(),
codecRegistry(
getDefaultCodecRegistry(),
SerializationCodecRegistry(configuration.copy(nonEncodeNull = true))
)
}
Expand Down Expand Up @@ -85,7 +87,7 @@ class SerializationClassMappingTypeService : ClassMappingTypeService {
//create a fake document to bypass bson writer built-in checks
jsonWriter.writeStartDocument()
jsonWriter.writeName("tmp")
coreCodecRegistry().get(obj.javaClass).encode(jsonWriter, obj, EncoderContext.builder().build())
codecRegistryWithNonEncodeNull.get(obj.javaClass).encode(jsonWriter, obj, EncoderContext.builder().build())
jsonWriter.writeEndDocument()
writer.toString().run {
substring("{ \"tmp\":".length, length - "}".length).trim()
Expand All @@ -99,7 +101,7 @@ class SerializationClassMappingTypeService : ClassMappingTypeService {
override fun <T, R> getIdValue(idProperty: KProperty1<T, R>, instance: T): R? =
idController.getIdValue(idProperty, instance)

override fun coreCodecRegistry(): CodecRegistry = codecRegistry
override fun coreCodecRegistry(): CodecRegistry = coreCodecRegistry

override fun <T> calculatePath(property: KProperty<T>): String {
return property.findAnnotation<SerialName>()?.value ?: property.name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package org.litote.kmongo.serialization

import com.mongodb.MongoClientSettings.getDefaultCodecRegistry
import org.bson.BsonDocument
import org.bson.BsonDocumentReader
import org.bson.BsonDocumentWriter
Expand All @@ -34,7 +35,8 @@ class SerializationClassMappingTypeServiceTest {
fun `encode and decode document`() {
val doc = Document()
doc["a"] = "b"
val codec = SerializationClassMappingTypeService().coreCodecRegistry().get(Document::class.java)
val codec =
SerializationClassMappingTypeService().codecRegistry(getDefaultCodecRegistry()).get(Document::class.java)
val document = BsonDocument()
val writer = BsonDocumentWriter(document)
codec.encode(writer, doc, EncoderContext.builder().build())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@
package org.litote.kmongo.service

import org.bson.BsonDocument
import org.bson.codecs.Codec
import org.bson.codecs.configuration.CodecConfigurationException
import org.bson.codecs.configuration.CodecProvider
import org.bson.codecs.configuration.CodecRegistries
import org.bson.codecs.configuration.CodecRegistry
import org.litote.kmongo.util.ObjectMappingConfiguration
import org.litote.kmongo.util.ObjectMappingConfiguration.customCodecProviders
import java.util.concurrent.ConcurrentHashMap
import kotlin.reflect.KClass
import kotlin.reflect.KProperty
Expand All @@ -38,6 +41,19 @@ private val kPropertyPathClass =
null
}

internal object CustomCodecProvider : CodecProvider {

private val customCodecMap = ConcurrentHashMap<Class<*>, Codec<*>>()

fun <T> addCustomCodec(codec: Codec<T>) {
customCodecMap[codec.encoderClass] = codec
}

@Suppress("UNCHECKED_CAST")
override fun <T : Any?> get(clazz: Class<T>, registry: CodecRegistry): Codec<T>? =
customCodecMap[clazz] as? Codec<T>
}

/**
* Provides an object mapping utility using [java.util.ServiceLoader].
*/
Expand All @@ -56,19 +72,39 @@ interface ClassMappingTypeService {

fun <T, R> getIdValue(idProperty: KProperty1<T, R>, instance: T): R?

fun codecRegistry(codecRegistry: CodecRegistry): CodecRegistry {
/**
* Returns a codec registry built with [baseCodecRegistry], [customCodecProviders] and [coreCodeRegistry]
*/
fun codecRegistry(
baseCodecRegistry: CodecRegistry,
coreCodeRegistry: CodecRegistry = coreCodecRegistry()
): CodecRegistry {
lateinit var codec: CodecRegistry
codec = CodecRegistries.fromRegistries(
codecRegistry,
CodecRegistries.fromCodecs(
ObjectMappingConfiguration.customCodecProviders.map { it.codec { codec } }
),
coreCodecRegistry()
codec = CodecRegistries.fromProviders(
providerFromRegistry(baseCodecRegistry),
providerFromRegistry(CodecRegistries.fromCodecs(
customCodecProviders.map { it.codec { codec } }
)),
CustomCodecProvider,
providerFromRegistry(coreCodeRegistry)
)

return codec
}

private fun providerFromRegistry(innerRegistry: CodecRegistry): CodecProvider {
return if (innerRegistry is CodecProvider) {
innerRegistry
} else {
object : CodecProvider {
override fun <T> get(clazz: Class<T>, outerRregistry: CodecRegistry): Codec<T>? =
try {
innerRegistry[clazz]
} catch (e: CodecConfigurationException) {
null
}
}
}
}

fun coreCodecRegistry(): CodecRegistry

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ import org.bson.codecs.Codec
import org.bson.codecs.configuration.CodecRegistry

/**
* Used by [KMongoCodecBase].
* Used by [ObjectMappingConfiguration].
*/
@Deprecated("use add CustomCodec()")
interface KMongoCodecProvider<T> {

fun codec(codecRegistryProvider: () -> (CodecRegistry)): Codec<T>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@

package org.litote.kmongo.util

import org.bson.codecs.Codec
import org.litote.kmongo.service.CustomCodecProvider
import java.util.concurrent.CopyOnWriteArrayList

/**
* Default object mapping configuration options.
* Set values before KMongo initialization.
Expand All @@ -32,5 +36,13 @@ object ObjectMappingConfiguration {
/**
* Additional generated codecs registered before KMongo main codec.
*/
val customCodecProviders: MutableList<KMongoCodecProvider<*>> = mutableListOf()
@Deprecated("use add CustomCodec()")
val customCodecProviders: MutableList<KMongoCodecProvider<*>> = CopyOnWriteArrayList()

/**
* Adds a custom codec.
*/
fun <T> addCustomCodec(codec: Codec<T>) {
CustomCodecProvider.addCustomCodec(codec)
}
}

0 comments on commit bc7cecd

Please sign in to comment.