Repository: holunda-io/camunda-bpm-taskpool
Issue: ProjectingCommandAccumulator fails if multiple commands have the sourceReference attribute and sourceReference differs

### Steps to reproduce

* camunda-bpm-taskpool version: 3.3.1
* Camunda BPM version: 7.16.6-ee
* JDK version:  11
* Operating system: Linux
* Complete executable reproducer: will provide a failing test
* Steps: I noticed this moving a process token to a user task with Camunda Cockpit. But it can also be reproduced in a unit test. No idea why in this case, there are multiple commands produced with different source references.

### Expected behaviour

No exception

### Actual behaviour

```
java.lang.IllegalArgumentException: Could not resolve subtype of [simple type, class io.holunda.camunda.taskpool.api.task.SourceReference]: missing type id property '@class' (for POJO property 'sourceReference')
 at [Source: UNKNOWN; byte offset: #UNKNOWN] (through reference chain: io.holunda.polyflow.taskpool.sender.task.accumulator.Model["sourceReference"])

	at com.fasterxml.jackson.databind.ObjectMapper._convert(ObjectMapper.java:4393)
	at com.fasterxml.jackson.databind.ObjectMapper.convertValue(ObjectMapper.java:4324)
	at io.holunda.polyflow.taskpool.sender.task.accumulator.ProjectPropertiesKt$jacksonUnmapper$1.invoke(projectProperties.kt:110)
	at io.holunda.polyflow.taskpool.sender.task.accumulator.ProjectPropertiesKt$jacksonUnmapper$1.invoke(projectProperties.kt:109)
	at io.holunda.polyflow.taskpool.sender.task.accumulator.ProjectPropertiesKt.projectProperties(projectProperties.kt:96)
	at io.holunda.polyflow.taskpool.sender.task.accumulator.ProjectPropertiesKt.projectProperties$default(projectProperties.kt:36)
	at io.holunda.polyflow.taskpool.sender.task.accumulator.PropertiesProjectorTest.should merge two models that have a source reference(ProjectPropertiesTest.kt:144)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:725)
	at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
	at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84)
	at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:214)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:210)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:135)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:66)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:107)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86)
	at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86)
	at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:53)
	at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:57)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)
	at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
	at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:235)
	at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54)
Caused by: com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Could not resolve subtype of [simple type, class io.holunda.camunda.taskpool.api.task.SourceReference]: missing type id property '@class' (for POJO property 'sourceReference')
 at [Source: UNKNOWN; byte offset: #UNKNOWN] (through reference chain: io.holunda.polyflow.taskpool.sender.task.accumulator.Model["sourceReference"])
	at com.fasterxml.jackson.databind.exc.InvalidTypeIdException.from(InvalidTypeIdException.java:43)
	at com.fasterxml.jackson.databind.DeserializationContext.missingTypeIdException(DeserializationContext.java:2083)
	at com.fasterxml.jackson.databind.DeserializationContext.handleMissingTypeId(DeserializationContext.java:1596)
	at com.fasterxml.jackson.databind.jsontype.impl.TypeDeserializerBase._handleMissingTypeId(TypeDeserializerBase.java:307)
	at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer._deserializeTypedUsingDefaultImpl(AsPropertyTypeDeserializer.java:185)
	at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer.deserializeTypedFromObject(AsPropertyTypeDeserializer.java:119)
	at com.fasterxml.jackson.databind.deser.AbstractDeserializer.deserializeWithType(AbstractDeserializer.java:263)
	at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:539)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeWithErrorWrapping(BeanDeserializer.java:563)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:438)
	at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1405)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:351)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:184)
	at com.fasterxml.jackson.databind.ObjectMapper._convert(ObjectMapper.java:4388)
	... 75 more
```



Changed files:
integration/common/taskpool-sender/src/main/kotlin/io/holunda/polyflow/taskpool/sender/task/accumulator/ProjectingCommandAccumulator.kt
integration/common/taskpool-sender/src/main/kotlin/io/holunda/polyflow/taskpool/sender/task/accumulator/projectProperties.kt
integration/common/taskpool-sender/src/test/kotlin/io/holunda/polyflow/taskpool/sender/task/accumulator/ProjectPropertiesTest.kt

diff --git a/integration/common/taskpool-sender/src/main/kotlin/io/holunda/polyflow/taskpool/sender/task/accumulator/ProjectingCommandAccumulator.kt b/integration/common/taskpool-sender/src/main/kotlin/io/holunda/polyflow/taskpool/sender/task/accumulator/ProjectingCommandAccumulator.kt
index 1158747f2..ecabfb410 100644
--- a/integration/common/taskpool-sender/src/main/kotlin/io/holunda/polyflow/taskpool/sender/task/accumulator/ProjectingCommandAccumulator.kt
+++ b/integration/common/taskpool-sender/src/main/kotlin/io/holunda/polyflow/taskpool/sender/task/accumulator/ProjectingCommandAccumulator.kt
@@ -36,59 +36,69 @@ class ProjectingCommandAccumulator(
    * Take the original command and updates the properties based on the properties of details.
    */
   @Suppress("UNCHECKED_CAST")
-  private fun <T : EngineTaskCommand> projectCommandProperties(command: T, details: List<T>): T = projectProperties(
-    original = command,
-    details = details,
-    /**
-     * Configuration to change default behavior of replacing the property.
-     * For delta-commands (add/delete candidate groups/users), the lists has to be adjusted
-     */
-    propertyOperationConfig = mutableMapOf<KClass<out Any>, PropertyOperation>().apply {
-      // a delete command should remove one element from candidate user
-      put(DeleteCandidateUsersCommand::class) { map, key, value ->
-        if (key == DeleteCandidateUsersCommand::candidateUsers.name) {
-          val candidateUsers = (map[key] as Collection<String>).minus(value as Collection<String>)
-          map[key] = candidateUsers
+  private fun <T : EngineTaskCommand> projectCommandProperties(command: T, details: List<T>): T {
+    val mapper = jacksonMapper(objectMapper = objectMapper)
+    return projectProperties(
+      original = command,
+      details = details,
+      /**
+       * Configuration to change default behavior of replacing the property.
+       * For delta-commands (add/delete candidate groups/users), the lists has to be adjusted
+       */
+      propertyOperationConfig = mutableMapOf<KClass<out Any>, PropertyOperation>().apply {
+        // a delete command should remove one element from candidate user
+        put(DeleteCandidateUsersCommand::class) { map, key, value ->
+          if (key == DeleteCandidateUsersCommand::candidateUsers.name) {
+            val candidateUsers = (map[key] as Collection<String>).minus(value as Collection<String>)
+            map[key] = candidateUsers
+          }
         }
-      }
-      // a delete command should remove one element from candidate group
-      put(DeleteCandidateGroupsCommand::class) { map, key, value ->
-        if (key == DeleteCandidateGroupsCommand::candidateGroups.name) {
-          val candidateGroups = (map[key] as Collection<String>).minus(value as Collection<String>)
-          map[key] = candidateGroups
+        // a delete command should remove one element from candidate group
+        put(DeleteCandidateGroupsCommand::class) { map, key, value ->
+          if (key == DeleteCandidateGroupsCommand::candidateGroups.name) {
+            val candidateGroups = (map[key] as Collection<String>).minus(value as Collection<String>)
+            map[key] = candidateGroups
+          }
         }
-      }
-      // add command should add one element to candidate user
-      put(AddCandidateUsersCommand::class) { map, key, value ->
-        if (key == AddCandidateUsersCommand::candidateUsers.name) {
-          val candidateUsers = (map[key] as Collection<String>).plus(value as Collection<String>)
-          map[key] = candidateUsers
+        // add command should add one element to candidate user
+        put(AddCandidateUsersCommand::class) { map, key, value ->
+          if (key == AddCandidateUsersCommand::candidateUsers.name) {
+            val candidateUsers = (map[key] as Collection<String>).plus(value as Collection<String>)
+            map[key] = candidateUsers
+          }
         }
-      }
-      // add command should add one element to candidate group
-      put(AddCandidateGroupsCommand::class) { map, key, value ->
-        if (key == AddCandidateGroupsCommand::candidateGroups.name) {
-          val candidateGroups = (map[key] as Collection<String>).plus(value as Collection<String>)
-          map[key] = candidateGroups
+        // add command should add one element to candidate group
+        put(AddCandidateGroupsCommand::class) { map, key, value ->
+          if (key == AddCandidateGroupsCommand::candidateGroups.name) {
+            val candidateGroups = (map[key] as Collection<String>).plus(value as Collection<String>)
+            map[key] = candidateGroups
+          }
         }
-      }
-    },
-    ignoredProperties = listOf(
-      WithTaskId::id.name,
-      CamundaTaskEventType::eventName.name,
-      EngineTaskCommand::order.name,
-      // no reason to overwrite correlation information or payload ever
-      // the initial command (create, update) already contains the entire information.
-      WithCorrelations::correlations.name,
-      WithPayload::payload.name,
-      // there is no reason to overwrite a business key so far
-      // its initial value is read and send during task creation
-      WithPayload::businessKey.name
-    ),
-    projectionErrorDetector = EngineTaskCommandProjectionErrorDetector,
-    mapper = jacksonMapper(objectMapper = objectMapper),
-    unmapper = jacksonUnmapper(clazz = command::class.java, objectMapper = objectMapper)
-  )
+      },
+      defaultPropertyOperation = { map, key, value ->
+        if (key == TaskIdentity::sourceReference.name) {
+          map[key] = value?.let { mapper.invoke(it) }
+        } else {
+          map[key] = value
+        }
+      },
+      ignoredProperties = listOf(
+        WithTaskId::id.name,
+        CamundaTaskEventType::eventName.name,
+        EngineTaskCommand::order.name,
+        // no reason to overwrite correlation information or payload ever
+        // the initial command (create, update) already contains the entire information.
+        WithCorrelations::correlations.name,
+        WithPayload::payload.name,
+        // there is no reason to overwrite a business key so far
+        // its initial value is read and send during task creation
+        WithPayload::businessKey.name
+      ),
+      projectionErrorDetector = EngineTaskCommandProjectionErrorDetector,
+      mapper = mapper,
+      unmapper = jacksonUnmapper(clazz = command::class.java, objectMapper = objectMapper)
+    )
+  }
 
   /**
    * Handle payload and serializes it using provided object mapper (e.g. to JSON)
diff --git a/integration/common/taskpool-sender/src/main/kotlin/io/holunda/polyflow/taskpool/sender/task/accumulator/projectProperties.kt b/integration/common/taskpool-sender/src/main/kotlin/io/holunda/polyflow/taskpool/sender/task/accumulator/projectProperties.kt
index b9d2bc4e1..404a0cc59 100644
--- a/integration/common/taskpool-sender/src/main/kotlin/io/holunda/polyflow/taskpool/sender/task/accumulator/projectProperties.kt
+++ b/integration/common/taskpool-sender/src/main/kotlin/io/holunda/polyflow/taskpool/sender/task/accumulator/projectProperties.kt
@@ -23,7 +23,7 @@ typealias PropertyOperationConfiguration = Map<KClass<out Any>, PropertyOperatio
 /**
  * Reads a command and returns its properties as a map.
  */
-typealias Mapper<T> = (value: T) -> MutableMap<String, Any?>
+typealias Mapper = (value: Any) -> MutableMap<String, Any?>
 
 /**
  * Reads a map and returns a command.
@@ -37,7 +37,8 @@ fun <T : Any> projectProperties(
   original: T,
   details: List<Any> = emptyList(),
   propertyOperationConfig: PropertyOperationConfiguration = mapOf(),
-  mapper: Mapper<T>,
+  defaultPropertyOperation: PropertyOperation = { map, key, value -> map[key] = value },
+  mapper: Mapper,
   unmapper: Unmapper<T>,
   ignoredProperties: List<String> = emptyList(),
   projectionErrorDetector: ProjectionErrorDetector
@@ -79,13 +80,14 @@ fun <T : Any> projectProperties(
     // - or a property is a collection or a map
     val matchingProperties = potentialMatchingProperties.filter { detailProperty ->
       originalProperties.any {
+        // TODO: Shouldn't we check the name here also?
         it.returnType == detailProperty.returnType && it.get(original) != detailProperty.get(detail)
           || (detailProperty.get(detail) is MutableMap<*, *> || detailProperty.get(detail) is MutableCollection<*>)
       }
     }
 
     // determine property operation
-    val propertyOperation = propertyOperationConfig.getOrDefault(detail.javaClass.kotlin) { map, key, value -> map[key] = value }
+    val propertyOperation = propertyOperationConfig.getOrDefault(detail.javaClass.kotlin, defaultPropertyOperation)
 
     // store values in a map
     matchingProperties.forEach { matchingProperty ->
@@ -99,7 +101,7 @@ fun <T : Any> projectProperties(
 /**
  * Default Jackson Mapper (object to map).
  */
-fun <T> jacksonMapper(objectMapper: ObjectMapper): Mapper<T> = {
+fun jacksonMapper(objectMapper: ObjectMapper): Mapper = {
   objectMapper.convertValue(it, object : TypeReference<MutableMap<String, Any?>>() {})
 }
 
diff --git a/integration/common/taskpool-sender/src/test/kotlin/io/holunda/polyflow/taskpool/sender/task/accumulator/ProjectPropertiesTest.kt b/integration/common/taskpool-sender/src/test/kotlin/io/holunda/polyflow/taskpool/sender/task/accumulator/ProjectPropertiesTest.kt
index 88601c686..77ba7c088 100644
--- a/integration/common/taskpool-sender/src/test/kotlin/io/holunda/polyflow/taskpool/sender/task/accumulator/ProjectPropertiesTest.kt
+++ b/integration/common/taskpool-sender/src/test/kotlin/io/holunda/polyflow/taskpool/sender/task/accumulator/ProjectPropertiesTest.kt
@@ -1,6 +1,10 @@
 package io.holunda.polyflow.taskpool.sender.task.accumulator
 
+import com.fasterxml.jackson.annotation.JsonTypeInfo
 import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
+import io.holunda.camunda.taskpool.api.task.ProcessReference
+import io.holunda.camunda.taskpool.api.task.SourceReference
+import io.holunda.camunda.taskpool.api.task.TaskIdentity
 import io.holunda.polyflow.bus.jackson.configurePolyflowJacksonObjectMapper
 import org.assertj.core.api.Assertions.assertThat
 import org.junit.jupiter.api.Test
@@ -15,7 +19,12 @@ class PropertiesProjectorTest {
   fun `should return the command if details are empty`() {
 
     val model = model("Foo", "My foo model")
-    val result = projectProperties(model, projectionErrorDetector = object:ProjectionErrorDetector{}, mapper = jacksonMapper(objectMapper), unmapper = jacksonUnmapper(model::class.java, objectMapper))
+    val result = projectProperties(
+      model,
+      projectionErrorDetector = object : ProjectionErrorDetector {},
+      mapper = jacksonMapper(objectMapper),
+      unmapper = jacksonUnmapper(model::class.java, objectMapper)
+    )
 
     assertThat(result).isEqualTo(model)
   }
@@ -24,7 +33,13 @@ class PropertiesProjectorTest {
   fun `should replace a detail`() {
 
     val model = model("Foo", "My foo model")
-    val result = projectProperties(model, listOf(named(name = "new name", id = model.id)), projectionErrorDetector = object:ProjectionErrorDetector{}, mapper = jacksonMapper(objectMapper), unmapper = jacksonUnmapper(model::class.java, objectMapper))
+    val result = projectProperties(
+      model,
+      listOf(named(name = "new name", id = model.id)),
+      projectionErrorDetector = object : ProjectionErrorDetector {},
+      mapper = jacksonMapper(objectMapper),
+      unmapper = jacksonUnmapper(model::class.java, objectMapper)
+    )
 
     assertThat(result).isEqualTo(model.copy(name = "new name"))
   }
@@ -33,10 +48,12 @@ class PropertiesProjectorTest {
   fun `should replace a detail with the last value`() {
 
     val model = model("Foo", "My foo model")
-    val result = projectProperties(model, listOf(
-      named(name = "wrong name", id = model.id),
-      named(name = "new name", id = model.id)),
-      projectionErrorDetector = object:ProjectionErrorDetector{},
+    val result = projectProperties(
+      model, listOf(
+        named(name = "wrong name", id = model.id),
+        named(name = "new name", id = model.id)
+      ),
+      projectionErrorDetector = object : ProjectionErrorDetector {},
       mapper = jacksonMapper(objectMapper), unmapper = jacksonUnmapper(model::class.java, objectMapper)
     )
 
@@ -47,10 +64,15 @@ class PropertiesProjectorTest {
   fun `should replace detail map`() {
 
     val model = model("Foo", "My foo model", payload = mutableMapOf("foo" to "bar"))
-    val result = projectProperties(model, listOf(
-      payload(payload = mutableMapOf("zee" to "test"), id = model.id),
-      named(name = "new name", id = model.id)),
-      projectionErrorDetector = object:ProjectionErrorDetector{}, mapper = jacksonMapper(objectMapper), unmapper = jacksonUnmapper(model::class.java, objectMapper)
+    val result = projectProperties(
+      model,
+      listOf(
+        payload(payload = mutableMapOf("zee" to "test"), id = model.id),
+        named(name = "new name", id = model.id)
+      ),
+      projectionErrorDetector = object : ProjectionErrorDetector {},
+      mapper = jacksonMapper(objectMapper),
+      unmapper = jacksonUnmapper(model::class.java, objectMapper)
     )
 
     assertThat(result).isEqualTo(model.copy(name = "new name", enriched = true, payload = mutableMapOf("zee" to "test")))
@@ -60,9 +82,11 @@ class PropertiesProjectorTest {
   fun `should add a detail to a map`() {
 
     val model = model("Foo", "My foo model", payload = mutableMapOf("foo" to "bar"))
-    val result = projectProperties(model, listOf(
-      payload(payload = mutableMapOf("zee" to "test"), id = model.id),
-      named(name = "new name", id = model.id)),
+    val result = projectProperties(model,
+      listOf(
+        payload(payload = mutableMapOf("zee" to "test"), id = model.id),
+        named(name = "new name", id = model.id)
+      ),
 
       mutableMapOf(
         Payload::class to { map, key, value ->
@@ -73,7 +97,9 @@ class PropertiesProjectorTest {
           }
         }
       ),
-      projectionErrorDetector = object:ProjectionErrorDetector{}, mapper = jacksonMapper(objectMapper), unmapper = jacksonUnmapper(model::class.java, objectMapper)
+      projectionErrorDetector = object : ProjectionErrorDetector {},
+      mapper = jacksonMapper(objectMapper),
+      unmapper = jacksonUnmapper(model::class.java, objectMapper)
     )
 
     assertThat(result).isEqualTo(model.copy(name = "new name", enriched = true, payload = mutableMapOf("foo" to "bar", "zee" to "test")))
@@ -84,12 +110,14 @@ class PropertiesProjectorTest {
   fun `should remove a detail from a map`() {
 
     val model = model("Foo", "My foo model", payload = mutableMapOf("foo" to "bar"), users = mutableListOf("kermit", "gonzo"))
-    val result = projectProperties(model, listOf(
+    val result = projectProperties(model,
+      listOf(
 
-      // use payload
-      payload(payload = mutableMapOf("foo" to "bar"), id = model.id, users = mutableListOf("gonzo")),
+        // use payload
+        payload(payload = mutableMapOf("foo" to "bar"), id = model.id, users = mutableListOf("gonzo")),
 
-      named(name = "new name", id = model.id)),
+        named(name = "new name", id = model.id)
+      ),
 
       // to remove elements
       mutableMapOf<KClass<out Any>, PropertyOperation>(
@@ -102,24 +130,64 @@ class PropertiesProjectorTest {
           }
         }
       ),
-      projectionErrorDetector = object:ProjectionErrorDetector{}, mapper = jacksonMapper(objectMapper), unmapper = jacksonUnmapper(model::class.java, objectMapper)
+      projectionErrorDetector = object : ProjectionErrorDetector {},
+      mapper = jacksonMapper(objectMapper),
+      unmapper = jacksonUnmapper(model::class.java, objectMapper)
     )
 
     // map elements should vanish, kermit remains alone in the list
     assertThat(result).isEqualTo(model.copy(name = "new name", enriched = true, payload = mutableMapOf(), users = mutableListOf("kermit")))
   }
 
+  @Test
+  fun `should merge two models that have a source reference`() {
+
+    val fooModel = model(
+      name = "Foo",
+      description = "My foo model",
+      sourceReference = ProcessReference("foo", "bar", "foo", "bar", "foo", "bar")
+    )
+    val barModel = model(
+      id = fooModel.id,
+      name = "Bar",
+      description = "My bar model",
+      sourceReference = ProcessReference("bar", "foo", "bar", "foo", "bar", "foo")
+    )
+    val mapper = jacksonMapper(objectMapper)
+    val result = projectProperties(
+      fooModel,
+      listOf(barModel),
+      projectionErrorDetector = object : ProjectionErrorDetector {},
+      defaultPropertyOperation = { map, key, value ->
+        if (key == TaskIdentity::sourceReference.name) {
+          map[key] = value?.let { mapper.invoke(it) }
+        } else {
+          map[key] = value
+        }
+      },
+      mapper = mapper,
+      unmapper = jacksonUnmapper(fooModel::class.java, objectMapper)
+    )
+
+    assertThat(result).isEqualTo(barModel)
+  }
+    interface SR {
+      val x: String
+    }
 }
 
 internal fun model(
-  name: String, description: String = "",
+  name: String,
+  description: String = "",
   id: String = UUID.randomUUID().toString(),
   payload: MutableMap<String, Any> = mutableMapOf(),
-  users: MutableList<String> = mutableListOf()
+  users: MutableList<String> = mutableListOf(),
+  sourceReference: SourceReference = ProcessReference("foo", "bar", "foo", "bar", "foo", "bar")
 ) = Model(
   id = id,
   name = name,
   description = description,
+  sourceReference = sourceReference,
   payload = payload,
   users = users,
   enriched = payload.isNotEmpty()
@@ -139,6 +207,7 @@ internal data class Model(
   override val id: String,
   val name: String,
   val description: String,
+  val sourceReference: SourceReference,
   override var enriched: Boolean = false,
   override val payload: MutableMap<String, Any> = mutableMapOf(),
   override val users: MutableList<String> = mutableListOf()