Skip to content

Commit 8a476aa

Browse files
helfperpjfanning
authored andcommitted
Add merge support for Scala collections (#481)
1 parent 94ef94b commit 8a476aa

File tree

3 files changed

+147
-6
lines changed

3 files changed

+147
-6
lines changed

src/main/scala/com/fasterxml/jackson/module/scala/deser/GenericFactoryDeserializerResolver.scala

+16-2
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@ abstract class GenericFactoryDeserializerResolver[CC[_], CF[X[_]]] extends Deser
5252
override def add(e: A): Boolean = { builder += e; size += 1; true }
5353

5454
// Required by AbstractCollection, but not implemented
55-
override def iterator(): util.Iterator[A] = null
55+
override def iterator(): util.Iterator[A] = None.orNull
56+
57+
def setInitialValue(init: Collection[_]): Unit = init.asInstanceOf[Iterable[A]].foreach(add)
5658
}
5759

5860
private class Instantiator(config: DeserializationConfig, collectionType: JavaType, valueType: JavaType)
@@ -87,9 +89,21 @@ abstract class GenericFactoryDeserializerResolver[CC[_], CF[X[_]]] extends Deser
8789
}
8890
}
8991

92+
override def deserialize(jp: JsonParser, ctxt: DeserializationContext, intoValue: CC[_]): CC[_] = {
93+
val bw = newBuilderWrapper(ctxt)
94+
bw.setInitialValue(intoValue)
95+
containerDeserializer.deserialize(jp, ctxt, bw) match {
96+
case wrapper: BuilderWrapper[_] => wrapper.builder.result()
97+
}
98+
}
99+
90100
override def getEmptyValue(ctxt: DeserializationContext): Object = {
91-
val bw = containerDeserializer.getValueInstantiator.createUsingDefault(ctxt).asInstanceOf[BuilderWrapper[AnyRef]]
101+
val bw = newBuilderWrapper(ctxt)
92102
bw.builder.result().asInstanceOf[Object]
93103
}
104+
105+
private def newBuilderWrapper(ctxt: DeserializationContext): BuilderWrapper[AnyRef] = {
106+
containerDeserializer.getValueInstantiator.createUsingDefault(ctxt).asInstanceOf[BuilderWrapper[AnyRef]]
107+
}
94108
}
95109
}

src/main/scala/com/fasterxml/jackson/module/scala/deser/GenericMapFactoryDeserializerResolver.scala

+28-4
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import com.fasterxml.jackson.databind.deser.{ContextualDeserializer, Deserialize
77
import com.fasterxml.jackson.databind.deser.std.{ContainerDeserializerBase, MapDeserializer, StdValueInstantiator}
88
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer
99

10-
import scala.collection.mutable
10+
import scala.collection.{Map, mutable}
1111
import scala.language.higherKinds
1212

1313
abstract class GenericMapFactoryDeserializerResolver[CC[K, V], CF[X[_, _]]] extends Deserializers.Base {
@@ -44,15 +44,26 @@ abstract class GenericMapFactoryDeserializerResolver[CC[K, V], CF[X[_, _]]] exte
4444
???
4545
}
4646

47-
private class BuilderWrapper[K,V](val builder: Builder[K, V]) extends java.util.AbstractMap[K, V] {
47+
private class BuilderWrapper[K, V >: AnyRef](val builder: Builder[K, V]) extends java.util.AbstractMap[K, V] {
48+
private var baseMap: Map[Any, V] = Map.empty
49+
4850
override def put(k: K, v: V): V = { builder += ((k, v)); v }
4951

52+
// Used by the deserializer when using readerForUpdating
53+
override def get(key: Any): V = baseMap.get(key).orNull
54+
5055
// Isn't used by the deserializer
51-
def entrySet(): java.util.Set[java.util.Map.Entry[K, V]] = throw new UnsupportedOperationException
56+
override def entrySet(): java.util.Set[java.util.Map.Entry[K, V]] = throw new UnsupportedOperationException
57+
58+
def setInitialValue(init: Collection[_, _]): Unit = {
59+
init.asInstanceOf[Map[K, V]].foreach(Function.tupled(put))
60+
baseMap = init.asInstanceOf[Map[Any, V]]
61+
}
5262
}
5363

5464
private class Instantiator(config: DeserializationConfig, mapType: MapLikeType) extends StdValueInstantiator(config, mapType) {
5565
override def canCreateUsingDefault = true
66+
5667
override def createUsingDefault(ctxt: DeserializationContext) =
5768
new BuilderWrapper[AnyRef, AnyRef](builderFor[AnyRef, AnyRef](mapType.getRawClass, mapType.getKeyType, mapType.getContentType))
5869
}
@@ -65,6 +76,7 @@ abstract class GenericMapFactoryDeserializerResolver[CC[K, V], CF[X[_, _]]] exte
6576
}
6677

6778
override def getContentType: JavaType = containerDeserializer.getContentType
79+
6880
override def getContentDeserializer: JsonDeserializer[AnyRef] = containerDeserializer.getContentDeserializer
6981

7082
override def createContextual(ctxt: DeserializationContext, property: BeanProperty): JsonDeserializer[_] = {
@@ -78,9 +90,21 @@ abstract class GenericMapFactoryDeserializerResolver[CC[K, V], CF[X[_, _]]] exte
7890
}
7991
}
8092

93+
override def deserialize(jp: JsonParser, ctxt: DeserializationContext, intoValue: CC[_, _]): CC[_, _] = {
94+
val bw = newBuilderWrapper(ctxt)
95+
bw.setInitialValue(intoValue)
96+
containerDeserializer.deserialize(jp, ctxt, bw) match {
97+
case wrapper: BuilderWrapper[_, _] => wrapper.builder.result()
98+
}
99+
}
100+
81101
override def getEmptyValue(ctxt: DeserializationContext): Object = {
82-
val bw = containerDeserializer.getValueInstantiator.createUsingDefault(ctxt).asInstanceOf[BuilderWrapper[AnyRef, AnyRef]]
102+
val bw = newBuilderWrapper(ctxt)
83103
bw.builder.result().asInstanceOf[Object]
84104
}
105+
106+
private def newBuilderWrapper(ctxt: DeserializationContext): BuilderWrapper[AnyRef, AnyRef] = {
107+
containerDeserializer.getValueInstantiator.createUsingDefault(ctxt).asInstanceOf[BuilderWrapper[AnyRef, AnyRef]]
108+
}
85109
}
86110
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package com.fasterxml.jackson.module.scala.deser
2+
3+
import com.fasterxml.jackson.annotation.JsonMerge
4+
import com.fasterxml.jackson.module.scala.DefaultScalaModule
5+
import org.junit.runner.RunWith
6+
import org.scalatestplus.junit.JUnitRunner
7+
8+
import scala.collection.{Map, mutable}
9+
10+
case class ClassWithLists(field1: List[String], @JsonMerge field2: List[String])
11+
case class ClassWithMaps[T](field1: Map[String, T], @JsonMerge field2: Map[String, T])
12+
case class ClassWithMutableMaps[T](field1: mutable.Map[String, T], @JsonMerge field2: mutable.Map[String, T])
13+
14+
case class Pair(first: String, second: String)
15+
16+
@RunWith(classOf[JUnitRunner])
17+
class MergeTest extends DeserializerTest {
18+
19+
val module: DefaultScalaModule.type = DefaultScalaModule
20+
21+
behavior of "The DefaultScalaModule when reading for updating"
22+
23+
it should "merge both lists" in {
24+
val initial = deserialize[ClassWithLists](classJson(firstListJson))
25+
val result = newBuilder.defaultMergeable(true).build()
26+
.readerForUpdating(initial).readValue[ClassWithLists](classJson(secondListJson))
27+
28+
result shouldBe ClassWithLists(mergedList, mergedList)
29+
}
30+
31+
it should "merge only the annotated list" in {
32+
val initial = deserialize[ClassWithLists](classJson(firstListJson))
33+
val result = newMapper
34+
.readerForUpdating(initial).readValue[ClassWithLists](classJson(secondListJson))
35+
36+
result shouldBe ClassWithLists(secondList, mergedList)
37+
}
38+
39+
it should "merge both string maps" in {
40+
val initial = deserialize[ClassWithMaps[String]](classJson(firstStringMapJson))
41+
val result = newBuilder.defaultMergeable(true).build()
42+
.readerForUpdating(initial).readValue[ClassWithMaps[String]](classJson(secondStringMapJson))
43+
44+
result shouldBe ClassWithMaps(mergedStringMap, mergedStringMap)
45+
}
46+
47+
it should "merge only the annotated string map" in {
48+
val initial = deserialize[ClassWithMaps[String]](classJson(firstStringMapJson))
49+
val result = newBuilder.defaultMergeable(true).build()
50+
.readerForUpdating(initial).readValue[ClassWithMaps[String]](classJson(secondStringMapJson))
51+
52+
result shouldBe ClassWithMaps(secondStringMap, mergedStringMap)
53+
}
54+
55+
it should "merge both pair maps" in {
56+
val initial = deserialize[ClassWithMaps[Pair]](classJson(firstPairMapJson))
57+
val result = newBuilder.defaultMergeable(true).build()
58+
.readerForUpdating(initial).forType(typeReference[ClassWithMaps[Pair]]).readValue[ClassWithMaps[Pair]](classJson(secondPairMapJson))
59+
60+
result shouldBe ClassWithMaps(mergedPairMap, mergedPairMap)
61+
}
62+
63+
it should "merge only the annotated pair map" in {
64+
val initial = deserialize[ClassWithMaps[Pair]](classJson(firstPairMapJson))
65+
val result = newMapper
66+
.readerForUpdating(initial).forType(typeReference[ClassWithMaps[Pair]]).readValue[ClassWithMaps[Pair]](classJson(secondPairMapJson))
67+
68+
result shouldBe ClassWithMaps(secondPairMap, mergedPairMap)
69+
}
70+
71+
it should "merge both mutable maps" in {
72+
val initial = deserialize[ClassWithMutableMaps[String]](classJson(firstStringMapJson))
73+
val result = newBuilder.defaultMergeable(true).build()
74+
.readerForUpdating(initial).readValue[ClassWithMutableMaps[String]](classJson(secondStringMapJson))
75+
76+
result shouldBe ClassWithMutableMaps(mutable.Map() ++ mergedStringMap, mutable.Map() ++ mergedStringMap)
77+
}
78+
79+
it should "merge only the annotated mutable map" in {
80+
val initial = deserialize[ClassWithMutableMaps[String]](classJson(firstStringMapJson))
81+
val result = newMapper
82+
.readerForUpdating(initial).readValue[ClassWithMutableMaps[String]](classJson(secondStringMapJson))
83+
84+
result shouldBe ClassWithMutableMaps(mutable.Map() ++ secondStringMap, mutable.Map() ++ mergedStringMap)
85+
}
86+
87+
def classJson(nestedJson: String) = s"""{"field1":$nestedJson,"field2":$nestedJson}"""
88+
89+
val firstListJson = """["one","two"]"""
90+
val secondListJson = """["three"]"""
91+
val secondList = List("three")
92+
val mergedList = List("one", "two", "three")
93+
94+
val firstStringMapJson = """{"one":"1","two":"2"}"""
95+
val secondStringMapJson = """{"two":"22","three":"33"}"""
96+
val secondStringMap = Map("two" -> "22", "three" -> "33")
97+
val mergedStringMap = Map("one" -> "1", "two" -> "22", "three" -> "33")
98+
99+
val firstPairMapJson = """{"one":{"first":"1"},"two":{"second":"2"},"three":{"first":"3","second":"4"}}"""
100+
val secondPairMapJson = """{"two":{"first":"22"},"three":{"second":"33"}}"""
101+
val secondPairMap = Map("two" -> Pair("22", null), "three" -> Pair(null, "33"))
102+
val mergedPairMap = Map("one" -> Pair("1", null), "two" -> Pair("22", "2"), "three" -> Pair("3", "33"))
103+
}

0 commit comments

Comments
 (0)