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

org.camunda.community.rest.variables.ValueMapper.kt Maven / Gradle / Ivy

/*-
 * #%L
 * camunda-platform-7-rest-client-spring-boot
 * %%
 * Copyright (C) 2019 Camunda Services GmbH
 * %%
 * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH
 *  under one or more contributor license agreements. See the NOTICE file
 *  distributed with this work for additional information regarding copyright
 *  ownership. Camunda licenses this file to you under the Apache License,
 *  Version 2.0; 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.
 * #L%
 */

package org.camunda.community.rest.variables

import com.fasterxml.jackson.core.JsonProcessingException
import com.fasterxml.jackson.databind.JavaType
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.type.TypeFactory
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.camunda.bpm.engine.variable.VariableMap
import org.camunda.bpm.engine.variable.Variables
import org.camunda.bpm.engine.variable.Variables.untypedValue
import org.camunda.bpm.engine.variable.impl.value.ObjectValueImpl
import org.camunda.bpm.engine.variable.type.FileValueType
import org.camunda.bpm.engine.variable.type.PrimitiveValueType
import org.camunda.bpm.engine.variable.type.SerializableValueType
import org.camunda.bpm.engine.variable.type.ValueType
import org.camunda.bpm.engine.variable.type.ValueTypeResolver
import org.camunda.bpm.engine.variable.value.FileValue
import org.camunda.bpm.engine.variable.value.SerializableValue
import org.camunda.bpm.engine.variable.value.TypedValue
import org.camunda.community.rest.client.model.VariableInstanceDto
import org.camunda.community.rest.client.model.VariableQueryParameterDto
import org.camunda.community.rest.client.model.VariableValueDto
import org.camunda.community.rest.impl.query.QueryOperator
import org.camunda.community.rest.impl.query.QueryVariableValue
import java.io.ObjectInputStream
import java.util.*

interface CustomValueMapper {

  fun mapValue(variableValue: Any): TypedValue

  fun canHandle(variableValue: Any): Boolean

  fun serializeValue(variableValue: SerializableValue): SerializableValue

  fun deserializeValue(variableValue: SerializableValue): TypedValue

}

/**
 * Class responsible for mapping variables from and to DTO representations.
 */
class ValueMapper(
  private val objectMapper: ObjectMapper = jacksonObjectMapper(),
  private val valueTypeResolver: ValueTypeResolver,
  private val customValueMapper: List = emptyList()
) {
  /**
   * Creates a variable value DTO out of variable value.
   */
  fun mapValue(variableValue: Any?, isTransient: Boolean = false): VariableValueDto {
    return mapValue(convertToTypedValue(variableValue, isTransient))
  }

  private fun convertToTypedValue(variableValue: Any?, isTransient: Boolean) =
    variableValue.resolveValueType().createValue(variableValue, mapOf(ValueType.VALUE_INFO_TRANSIENT to isTransient))

  /**
   * Create a variable value DTO out of typed variable value.
   */
  private fun mapValue(variableValue: TypedValue): VariableValueDto {
    val variable = customValueMapper.firstOrNull { it.canHandle(variableValue.value) }?.mapValue(variableValue.value) ?: variableValue
    if (variable is SerializableValue) {
      serializeValue(variable)
    }
    return variable.toDto()
  }

  private fun TypedValue.toDto() = VariableValueDto().apply {
    [email protected]?.let {
      type = toRestApiTypeName(it.name)
      valueInfo = it.getValueInfo(this@toDto)
    } ?: let {
      type = toRestApiTypeName([email protected]().name)
    }

    value = when (this@toDto) {
      is SerializableValue -> [email protected]
      is FileValue -> null //do not set the value for FileValues since we don't want to send megabytes over the network without explicit request
      else -> [email protected]
    }
  }

  private fun toRestApiTypeName(name: String): String {
    return name.substring(0, 1).uppercase(Locale.getDefault()) + name.substring(1)
  }

  private fun fromRestApiTypeName(name: String): String {
    return name.substring(0, 1).lowercase(Locale.getDefault()) + name.substring(1)
  }

  /**
   * Converts variable map to its REST representation.
   */
  fun mapValues(variables: MutableMap): Map {
    return if (variables is VariableMap) {
      variables.map { it.key to mapValue(variables.getValueTyped(it.key)) }.toMap()
    } else {
      variables.mapValues {
        mapValue(it.value)
      }
    }
  }

  /**
   * Convert variable REST implementation to variable map.
   */
  fun mapDtos(variables: Map, deserializeValues: Boolean = true): VariableMap {
    val result: VariableMap = Variables.createVariables()
    variables.mapValues {
      result[it.key] = mapDto(it.value, deserializeValues)
    }
    return result
  }

  /**
   * Maps DTO to its value.
   */
  @Suppress("UNCHECKED_CAST")
  fun  mapDto(dto: VariableValueDto, deserializeValues: Boolean = true): T? {
    return if (deserializeValues) {
      deserializeObjectValue(restoreObjectJsonIfNeeded(dto).toTypedValue(objectMapper))
    } else {
      dto.toTypedValue(objectMapper)
    } as T
  }

  /**
   * Maps DTO to its value.
   */
  @Suppress("UNCHECKED_CAST")
  fun  mapDto(dto: VariableInstanceDto, deserializeValues: Boolean = true): T? {
    val valueDto = VariableValueDto().type(dto.type).value(dto.value).valueInfo(dto.valueInfo)
    return if (deserializeValues) {
      deserializeObjectValue(restoreObjectJsonIfNeeded(valueDto).toTypedValue(objectMapper))
    } else {
      valueDto.toTypedValue(objectMapper)
    } as T
  }

  private fun VariableValueDto.toTypedValue(objectMapper: ObjectMapper): TypedValue {
    return if (type == null) {
      if (valueInfo != null && valueInfo["transient"] is Boolean) untypedValue(value, valueInfo["transient"] as Boolean) else untypedValue(
        value
      )
    } else {
      when (val valueType = valueTypeResolver.typeForName(fromRestApiTypeName(type))) {
        is PrimitiveValueType -> {
          val javaType = valueType.javaType
          var mappedValue: Any? = null
          if (value != null) {
            mappedValue = if (javaType.isAssignableFrom(value.javaClass)) {
              value
            } else {
              objectMapper.readValue("\"" + value + "\"", javaType)
            }
          }
          valueType.createValue(mappedValue, valueInfo)
        }

        is SerializableValueType -> {
          if (value != null && value !is String) {
            throw IllegalArgumentException("Must provide 'null' or String value for value of SerializableValue type '$type'.")
          } else {
            valueType.createValueFromSerialized(value as String, valueInfo)
          }
        }

        is FileValueType -> {
          if (value is String) {
            value = Base64.getDecoder().decode(value as String)
          }
          valueType.createValue(value, valueInfo)
        }

        else -> if (valueType == null) throw IllegalArgumentException("Unsupported value type '$type'") else valueType.createValue(
          value,
          valueInfo
        )
      }
    }

  }

  /**
   * In case of object values, Jackson serializes any JSON to a map of String -> Object.
   * We want to make use of type information provided by and therefor restore the original JSON.
   */
  private fun restoreObjectJsonIfNeeded(dto: VariableValueDto): VariableValueDto {
    val valueType: ValueType? = valueTypeResolver.typeForName(fromRestApiTypeName(dto.type))

    if (valueType is SerializableValueType) {
      if (dto.value != null && dto.value !is String && dto.value is Map<*, *>) {
        // recover json in order to avoid "Must provide 'null' or String value for value of SerializableValue type '$type'." exception
        val attributes: Map<*, *> = dto.value as Map<*, *>
        dto.value = objectMapper.writeValueAsString(attributes)
      }
    }

    return dto
  }

  /**
   * Serialize value, if not already serialized.
   * @param variableValue value to modify.
   */
  private fun serializeValue(variableValue: SerializableValue) {
    if (variableValue.valueSerialized == null) {
      customValueMapper.firstOrNull { it.canHandle(variableValue) }?.serializeValue(variableValue) ?: run {
        if (variableValue.serializationDataFormat == Variables.SerializationDataFormats.JSON.getName()
          // try it for application/json or unspecified
          || variableValue.serializationDataFormat == null
        ) {
          if (variableValue is ObjectValueImpl) {
            try {
              val serializedValue = objectMapper.writeValueAsString(variableValue.value)
              variableValue.setSerializedValue(serializedValue)
              // fix format if missing
              if (variableValue.serializationDataFormat == null) {
                variableValue.serializationDataFormat = Variables.SerializationDataFormats.JSON.getName()
              }
              // this allows to detect native types hidden in objectValue
              variableValue.objectTypeName = constructType(variableValue.value).toCanonical()
            } catch (e: JsonProcessingException) {
              throw IllegalArgumentException("Object value could not be serialized into '${variableValue.serializationDataFormat}'", e)
            }
          } else {
            throw UnsupportedOperationException("Serialization not supported for $variableValue")
          }
        } else {
          throw IllegalArgumentException("Object value could not be serialized into '${variableValue.serializationDataFormat}' and no serialized value has been provided for $variableValue")
        }
      }
    }
  }

  private fun constructType(value: Any): JavaType =
    if (value is Collection<*> && value.javaClass.typeParameters.size == 1 && value.isNotEmpty()) {
      TypeFactory.defaultInstance().constructCollectionType(value.javaClass, value.first()!!.javaClass)
    } else if (value is Array<*> && value.javaClass.typeParameters.size == 1 && value.isNotEmpty()) {
      TypeFactory.defaultInstance().constructArrayType(value.first()!!.javaClass)
    } else {
      TypeFactory.defaultInstance().constructType(value.javaClass)
    }

  /**
   *
   * Takes existing TypedValue and tries to create one with deserialized value.
   */
  private fun deserializeObjectValue(value: TypedValue): TypedValue {
    return if (value is SerializableValue && !value.isDeserialized) {
      return customValueMapper.firstOrNull { it.canHandle(value) }?.deserializeValue(value)
        ?: if (value.serializationDataFormat == Variables.SerializationDataFormats.JSON.getName()
          || value.serializationDataFormat == null
        ) {
          return when (value) {
            is ObjectValueImpl -> {
              val deserializedValue: Any = try {
                val clazz = TypeFactory.defaultInstance().constructFromCanonical(value.objectTypeName)
                objectMapper.readValue(value.valueSerialized, clazz) as Any
              } catch (e: Exception) {
                throw IllegalStateException("Error deserializing value $value", e)
              }
              ObjectValueImpl(deserializedValue, value.valueSerialized, value.serializationDataFormat, value.objectTypeName, true)
            }

            else -> throw IllegalStateException("Could not deserialize value $value")
          }
        } else if (value.serializationDataFormat == Variables.SerializationDataFormats.JAVA.getName()) {
          if (value is ObjectValueImpl) {
            val deserializedValue: Any = try {
              ObjectInputStream(Base64.getDecoder().decode(value.valueSerialized).inputStream()).use { it.readObject() }
            } catch (e: Exception) {
              throw IllegalStateException("Error deserializing value $value", e)
            }
            return ObjectValueImpl(deserializedValue, value.valueSerialized, value.serializationDataFormat, value.objectTypeName, true)
          } else {
            throw IllegalStateException("Could not deserialize value $value")
          }
        } else {
          throw IllegalStateException("Could not deserialize value $value, ${value.serializationDataFormat} is not supported.")
        }
    } else {
      value
    }
  }
}

fun QueryOperator.toRestOperator() = when (this) {
  QueryOperator.EQUALS -> VariableQueryParameterDto.OperatorEnum.EQ
  QueryOperator.GREATER_THAN -> VariableQueryParameterDto.OperatorEnum.GT
  QueryOperator.GREATER_THAN_OR_EQUAL -> VariableQueryParameterDto.OperatorEnum.GTEQ
  QueryOperator.LESS_THAN -> VariableQueryParameterDto.OperatorEnum.LT
  QueryOperator.LESS_THAN_OR_EQUAL -> VariableQueryParameterDto.OperatorEnum.LTEQ
  QueryOperator.LIKE -> VariableQueryParameterDto.OperatorEnum.LIKE
  QueryOperator.NOT_EQUALS -> VariableQueryParameterDto.OperatorEnum.NEQ
  QueryOperator.NOT_LIKE -> VariableQueryParameterDto.OperatorEnum.NOT_LIKE
}

fun List.toDto() = if (this.isEmpty()) null else this.map { it.toDto() }

fun QueryVariableValue.toDto(): VariableQueryParameterDto = VariableQueryParameterDto()
  .name(this.name)
  .value(this.value)
  .operator(this.operator.toRestOperator())





© 2015 - 2025 Weber Informatics LLC | Privacy Policy