All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.holunda.camunda.taskpool.sender.accumulator.projectProperties.kt Maven / Gradle / Ivy

There is a newer version: 2.1.8
Show newest version
package io.holunda.camunda.taskpool.sender.accumulator

import com.fasterxml.jackson.annotation.JsonTypeInfo
import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import io.holunda.camunda.taskpool.api.task.SourceReference
import org.camunda.bpm.engine.variable.VariableMap
import org.camunda.bpm.engine.variable.impl.VariableMapImpl
import org.slf4j.LoggerFactory
import kotlin.reflect.KClass
import kotlin.reflect.full.memberProperties

/**
 * The property operation defines how a new value can be applied to existing value.
 * values a map of values
 * key the key to access the value
 * value the new value
 */
typealias PropertyOperation = (values: MutableMap, key: String, value: Any?) -> Unit

/**
 * Property operation config.
 * Configures the property operations per class.
 */
typealias PropertyOperationConfiguration = Map, PropertyOperation>

/**
 * Reads a command and returns its properties as a map.
 */
typealias Mapper = (value: T) -> MutableMap

/**
 * Reads a map and returns a command.
 */
typealias Unmapper = (map: MutableMap) -> T

/**
 * Flattens the changes from detail objects into the original by applying property operation configuration.
 */
fun  projectProperties(
  original: T,
  details: List = emptyList(),
  propertyOperationConfig: PropertyOperationConfiguration = mapOf(),
  mapper: Mapper = jacksonMapper(),
  unmapper: Unmapper = jacksonUnmapper(original::class.java),
  ignoredProperties: List = emptyList(),
  projectionErrorDetector: ProjectionErrorDetector
): T {

  val originalProperties = original.javaClass.kotlin.memberProperties
  // read original into a map
  val values: MutableMap = mapper.invoke(original)

  for (detail in details) {

    // the property could be taken in consideration if and only if
    // - a property with the same name exists in the original
    // - it is not ignored
    val potentialMatchingProperties = detail.javaClass.kotlin.memberProperties.filter { detailProperty ->
      // not ignored
      !ignoredProperties.contains(detailProperty.name) &&
        originalProperties.any {
          it.name == detailProperty.name
        }
    }

    if (potentialMatchingProperties.isEmpty()) {
      val errorText = "PROJECTOR-001: No matching attributes of two commands to the same task found. The second command $detail is ignored.";
      if(projectionErrorDetector.shouldReportError(original = original, detail = detail)) {
        LoggerFactory
          .getLogger(ProjectingCommandAccumulator::class.java)
          .error(errorText)
      } else {
        LoggerFactory
          .getLogger(ProjectingCommandAccumulator::class.java)
          .debug(errorText)
      }
    }

    //
    // the property should be taken in consideration if and only if
    // - and either a property with the same return type exists in the original and the value of the property differs from that in original
    // - or a property is a collection or a map
    val matchingProperties = potentialMatchingProperties.filter { detailProperty ->
      originalProperties.any {
        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 }

    // store values in a map
    matchingProperties.forEach { matchingProperty ->
      propertyOperation(values, matchingProperty.name, matchingProperty.get(detail))
    }
  }
  // write back
  return unmapper.invoke(values)
}


fun  jacksonMapper(): Mapper = {
  jacksonObjectMapper()
    .registerModule(variableMapModule)
    .apply {
      addMixIn(SourceReference::class.java, KotlinTypeInfo::class.java)
    }
    .convertValue(it, object : TypeReference>() {})
}

fun  jacksonUnmapper(clazz: Class): Unmapper = {
  jacksonObjectMapper()
    .apply {
      addMixIn(SourceReference::class.java, KotlinTypeInfo::class.java)
    }
    .registerModule(variableMapModule)
    .convertValue(it, clazz)
}

val variableMapModule = SimpleModule()
  .apply {
    addAbstractTypeMapping(VariableMap::class.java, VariableMapImpl::class.java)
  }

@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, property = "@class", include = JsonTypeInfo.As.PROPERTY)
class KotlinTypeInfo

interface ProjectionErrorDetector {
  fun shouldReportError(original: Any, detail: Any): Boolean = true
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy