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.ObjectMapper
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.camunda.bpm.engine.ProcessEngine
import org.camunda.bpm.engine.ProcessEngines
import org.camunda.bpm.engine.impl.QueryOperator
import org.camunda.bpm.engine.impl.QueryVariableValue
import org.camunda.bpm.engine.impl.digest._apacheCommonsCodec.Base64
import org.camunda.bpm.engine.variable.VariableMap
import org.camunda.bpm.engine.variable.Variables
import org.camunda.bpm.engine.variable.Variables.untypedNullValue
import org.camunda.bpm.engine.variable.Variables.untypedValue
import org.camunda.bpm.engine.variable.impl.value.ObjectValueImpl
import org.camunda.bpm.engine.variable.type.*
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 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 processEngine: ProcessEngine = ProcessEngines.getDefaultProcessEngine(),
private val objectMapper: ObjectMapper = jacksonObjectMapper(),
private val customValueMapper: List = emptyList()
) {
/**
* Creates a variable value DTO out of variable value.
*/
fun mapValue(variableValue: Any?, isTransient: Boolean = false): VariableValueDto {
return mapValue(
when (variableValue) {
null -> untypedNullValue(isTransient)
else -> untypedValue(variableValue, 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
/*
* preferSerializedValue MUST be set to true, in order to be able to serialize ObjectValues
*/
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)
}
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]
}
}
fun toRestApiTypeName(name: String): String {
return name.substring(0, 1).uppercase(Locale.getDefault()) + name.substring(1)
}
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 {
// Not using VariableValueDto#toMap() since it ignores de-serialization.
val result: VariableMap = Variables.createVariables()
variables.mapValues {
val value = if (deserializeValues) {
restoreObjectJsonIfNeeded(it.value)
} else {
it.value
}.toTypedValue(processEngine, objectMapper)
result[it.key] = value
}
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(processEngine, objectMapper))
} else {
dto.toTypedValue(processEngine, 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(processEngine, objectMapper))
} else {
valueDto.toTypedValue(processEngine, objectMapper)
} as T
}
private fun VariableValueDto.toTypedValue(processEngine: ProcessEngine, objectMapper: ObjectMapper): TypedValue {
val valueTypeResolver = processEngine.processEngineConfiguration.valueTypeResolver
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.decodeBase64(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 valueTypeResolver: ValueTypeResolver = processEngine.processEngineConfiguration.valueTypeResolver
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
}
/**
* Maps untyped value to DTO, guessing the type.
* See {@link VariableValueDto} for more static functions.
*/
private fun fromUntypedValue(value: Any): VariableValueDto =
VariableValueDto().apply {
this.type = getTypeName(value)
this.value = value
}
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
private fun getTypeName(value: Any): String = when (value) {
is Boolean, is Date, is Double, is Integer, is Long, is Short, is String -> value::class.simpleName!!
is ByteArray -> "Bytes"
else -> "Object"
}
/**
* 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 = getTypeName(variableValue.value)
} 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")
}
}
}
}
/**
*
* 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 = Class.forName(value.objectTypeName)
objectMapper.readValue(value.valueSerialized, clazz)
} 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.decodeBase64(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 -> throw IllegalArgumentException()
else -> throw IllegalArgumentException()
}
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