Skip to content

Add merge support for Scala collections #481

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ abstract class GenericFactoryDeserializerResolver[CC[_], CF[X[_]]] extends Deser

// Required by AbstractCollection, but not implemented
override def iterator(): util.Iterator[A] = None.orNull

def setInitialValue(init: Collection[_]): Unit = init.asInstanceOf[Iterable[A]].foreach(add)
}

private class Instantiator(config: DeserializationConfig, collectionType: JavaType, valueType: JavaType)
Expand Down Expand Up @@ -82,9 +84,21 @@ abstract class GenericFactoryDeserializerResolver[CC[_], CF[X[_]]] extends Deser
}
}

override def deserialize(jp: JsonParser, ctxt: DeserializationContext, intoValue: CC[_]): CC[_] = {
val bw = newBuilderWrapper(ctxt)
bw.setInitialValue(intoValue)
containerDeserializer.deserialize(jp, ctxt, bw) match {
case wrapper: BuilderWrapper[_] => wrapper.builder.result()
}
}

override def getEmptyValue(ctxt: DeserializationContext): Object = {
val bw = containerDeserializer.getValueInstantiator.createUsingDefault(ctxt).asInstanceOf[BuilderWrapper[AnyRef]]
val bw = newBuilderWrapper(ctxt)
bw.builder.result().asInstanceOf[Object]
}

private def newBuilderWrapper(ctxt: DeserializationContext): BuilderWrapper[AnyRef] = {
containerDeserializer.getValueInstantiator.createUsingDefault(ctxt).asInstanceOf[BuilderWrapper[AnyRef]]
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import com.fasterxml.jackson.databind.deser.{ContextualDeserializer, Deserialize
import com.fasterxml.jackson.databind.deser.std.{ContainerDeserializerBase, MapDeserializer, StdValueInstantiator}
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer

import scala.collection.mutable
import scala.collection.{Map, mutable}
import scala.language.higherKinds

abstract class GenericMapFactoryDeserializerResolver[CC[K, V], CF[X[_, _]]] extends Deserializers.Base {
Expand Down Expand Up @@ -39,15 +39,26 @@ abstract class GenericMapFactoryDeserializerResolver[CC[K, V], CF[X[_, _]]] exte
}
}

private class BuilderWrapper[K,V](val builder: Builder[K, V]) extends java.util.AbstractMap[K, V] {
private class BuilderWrapper[K, V >: AnyRef](val builder: Builder[K, V]) extends java.util.AbstractMap[K, V] {
private var baseMap: Map[Any, V] = Map.empty

override def put(k: K, v: V): V = { builder += ((k, v)); v }

// Used by the deserializer when using readerForUpdating
override def get(key: Any): V = baseMap.get(key).orNull

// Isn't used by the deserializer
def entrySet(): java.util.Set[java.util.Map.Entry[K, V]] = throw new UnsupportedOperationException
override def entrySet(): java.util.Set[java.util.Map.Entry[K, V]] = throw new UnsupportedOperationException

def setInitialValue(init: Collection[_, _]): Unit = {
init.asInstanceOf[Map[K, V]].foreach(Function.tupled(put))
baseMap = init.asInstanceOf[Map[Any, V]]
}
}

private class Instantiator(config: DeserializationConfig, mapType: MapLikeType) extends StdValueInstantiator(config, mapType) {
override def canCreateUsingDefault = true

override def createUsingDefault(ctxt: DeserializationContext) =
new BuilderWrapper[AnyRef, AnyRef](builderFor[AnyRef, AnyRef](mapType.getRawClass, mapType.getKeyType, mapType.getContentType))
}
Expand All @@ -60,6 +71,7 @@ abstract class GenericMapFactoryDeserializerResolver[CC[K, V], CF[X[_, _]]] exte
}

override def getContentType: JavaType = containerDeserializer.getContentType

override def getContentDeserializer: JsonDeserializer[AnyRef] = containerDeserializer.getContentDeserializer

override def createContextual(ctxt: DeserializationContext, property: BeanProperty): JsonDeserializer[_] = {
Expand All @@ -73,9 +85,21 @@ abstract class GenericMapFactoryDeserializerResolver[CC[K, V], CF[X[_, _]]] exte
}
}

override def deserialize(jp: JsonParser, ctxt: DeserializationContext, intoValue: CC[_, _]): CC[_, _] = {
val bw = newBuilderWrapper(ctxt)
bw.setInitialValue(intoValue)
containerDeserializer.deserialize(jp, ctxt, bw) match {
case wrapper: BuilderWrapper[_, _] => wrapper.builder.result()
}
}

override def getEmptyValue(ctxt: DeserializationContext): Object = {
val bw = containerDeserializer.getValueInstantiator.createUsingDefault(ctxt).asInstanceOf[BuilderWrapper[AnyRef, AnyRef]]
val bw = newBuilderWrapper(ctxt)
bw.builder.result().asInstanceOf[Object]
}

private def newBuilderWrapper(ctxt: DeserializationContext): BuilderWrapper[AnyRef, AnyRef] = {
containerDeserializer.getValueInstantiator.createUsingDefault(ctxt).asInstanceOf[BuilderWrapper[AnyRef, AnyRef]]
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ trait DeserializerTest extends JacksonTest {
def deserialize[T: Manifest](value: String) : T =
newMapper.readValue(value, typeReference[T])

private [this] def typeReference[T: Manifest]: TypeReference[T] = new TypeReference[T] {
def typeReference[T: Manifest]: TypeReference[T] = new TypeReference[T] {
override def getType: Type = typeFromManifest(manifest[T])
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package com.fasterxml.jackson.module.scala.deser

import com.fasterxml.jackson.annotation.JsonMerge
import com.fasterxml.jackson.module.scala.DefaultScalaModule
import org.junit.runner.RunWith
import org.scalatestplus.junit.JUnitRunner

import scala.collection.{Map, mutable}

case class ClassWithLists(field1: List[String], @JsonMerge field2: List[String])
case class ClassWithMaps[T](field1: Map[String, T], @JsonMerge field2: Map[String, T])
case class ClassWithMutableMaps[T](field1: mutable.Map[String, T], @JsonMerge field2: mutable.Map[String, T])

case class Pair(first: String, second: String)

@RunWith(classOf[JUnitRunner])
class MergeTest extends DeserializerTest {

val module: DefaultScalaModule.type = DefaultScalaModule

behavior of "The DefaultScalaModule when reading for updating"

it should "merge both lists" in {
val initial = deserialize[ClassWithLists](classJson(firstListJson))
val result = newMapper.setDefaultMergeable(true)
.readerForUpdating(initial).readValue[ClassWithLists](classJson(secondListJson))

result shouldBe ClassWithLists(mergedList, mergedList)
}

it should "merge only the annotated list" in {
val initial = deserialize[ClassWithLists](classJson(firstListJson))
val result = newMapper
.readerForUpdating(initial).readValue[ClassWithLists](classJson(secondListJson))

result shouldBe ClassWithLists(secondList, mergedList)
}

it should "merge both string maps" in {
val initial = deserialize[ClassWithMaps[String]](classJson(firstStringMapJson))
val result = newMapper.setDefaultMergeable(true)
.readerForUpdating(initial).readValue[ClassWithMaps[String]](classJson(secondStringMapJson))

result shouldBe ClassWithMaps(mergedStringMap, mergedStringMap)
}

it should "merge only the annotated string map" in {
val initial = deserialize[ClassWithMaps[String]](classJson(firstStringMapJson))
val result = newMapper
.readerForUpdating(initial).readValue[ClassWithMaps[String]](classJson(secondStringMapJson))

result shouldBe ClassWithMaps(secondStringMap, mergedStringMap)
}

it should "merge both pair maps" in {
val initial = deserialize[ClassWithMaps[Pair]](classJson(firstPairMapJson))
val result = newMapper.setDefaultMergeable(true)
.readerForUpdating(initial).forType(typeReference[ClassWithMaps[Pair]]).readValue[ClassWithMaps[Pair]](classJson(secondPairMapJson))

result shouldBe ClassWithMaps(mergedPairMap, mergedPairMap)
}

it should "merge only the annotated pair map" in {
val initial = deserialize[ClassWithMaps[Pair]](classJson(firstPairMapJson))
val result = newMapper
.readerForUpdating(initial).forType(typeReference[ClassWithMaps[Pair]]).readValue[ClassWithMaps[Pair]](classJson(secondPairMapJson))

result shouldBe ClassWithMaps(secondPairMap, mergedPairMap)
}

it should "merge both mutable maps" in {
val initial = deserialize[ClassWithMutableMaps[String]](classJson(firstStringMapJson))
val result = newMapper.setDefaultMergeable(true)
.readerForUpdating(initial).readValue[ClassWithMutableMaps[String]](classJson(secondStringMapJson))

result shouldBe ClassWithMutableMaps(mutable.Map() ++ mergedStringMap, mutable.Map() ++ mergedStringMap)
}

it should "merge only the annotated mutable map" in {
val initial = deserialize[ClassWithMutableMaps[String]](classJson(firstStringMapJson))
val result = newMapper
.readerForUpdating(initial).readValue[ClassWithMutableMaps[String]](classJson(secondStringMapJson))

result shouldBe ClassWithMutableMaps(mutable.Map() ++ secondStringMap, mutable.Map() ++ mergedStringMap)
}

def classJson(nestedJson: String) = s"""{"field1":$nestedJson,"field2":$nestedJson}"""

val firstListJson = """["one","two"]"""
val secondListJson = """["three"]"""
val secondList = List("three")
val mergedList = List("one", "two", "three")

val firstStringMapJson = """{"one":"1","two":"2"}"""
val secondStringMapJson = """{"two":"22","three":"33"}"""
val secondStringMap = Map("two" -> "22", "three" -> "33")
val mergedStringMap = Map("one" -> "1", "two" -> "22", "three" -> "33")

val firstPairMapJson = """{"one":{"first":"1"},"two":{"second":"2"},"three":{"first":"3","second":"4"}}"""
val secondPairMapJson = """{"two":{"first":"22"},"three":{"second":"33"}}"""
val secondPairMap = Map("two" -> Pair("22", null), "three" -> Pair(null, "33"))
val mergedPairMap = Map("one" -> Pair("1", null), "two" -> Pair("22", "2"), "three" -> Pair("3", "33"))
}