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

io.camunda.tasklist.webapp.service.VariableService Maven / Gradle / Ivy

The newest version!
/*
 * 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.
 * Licensed under the Camunda License 1.0. You may not use this file
 * except in compliance with the Camunda License 1.0.
 */
package io.camunda.tasklist.webapp.service;

import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toList;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.camunda.tasklist.entities.FlowNodeInstanceEntity;
import io.camunda.tasklist.entities.VariableEntity;
import io.camunda.tasklist.entities.listview.ListViewJoinRelation;
import io.camunda.tasklist.entities.listview.VariableListViewEntity;
import io.camunda.tasklist.exceptions.NotFoundException;
import io.camunda.tasklist.property.TasklistProperties;
import io.camunda.tasklist.store.DraftVariableStore;
import io.camunda.tasklist.store.ListViewStore;
import io.camunda.tasklist.store.TaskStore;
import io.camunda.tasklist.store.VariableStore;
import io.camunda.tasklist.store.VariableStore.FlowNodeTree;
import io.camunda.tasklist.store.VariableStore.GetVariablesRequest;
import io.camunda.tasklist.store.VariableStore.VariableMap;
import io.camunda.tasklist.webapp.api.rest.v1.entities.VariableResponse;
import io.camunda.tasklist.webapp.api.rest.v1.entities.VariableSearchResponse;
import io.camunda.tasklist.webapp.es.TaskValidator;
import io.camunda.tasklist.webapp.graphql.entity.VariableDTO;
import io.camunda.tasklist.webapp.graphql.entity.VariableInputDTO;
import io.camunda.tasklist.webapp.rest.exception.InvalidRequestException;
import io.camunda.tasklist.webapp.rest.exception.NotFoundApiException;
import io.camunda.webapps.schema.entities.tasklist.DraftTaskVariableEntity;
import io.camunda.webapps.schema.entities.tasklist.SnapshotTaskVariableEntity;
import io.camunda.webapps.schema.entities.tasklist.TaskEntity;
import io.camunda.webapps.schema.entities.tasklist.TaskState;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import org.apache.commons.collections4.CollectionUtils;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

@Component
public class VariableService {

  private static final Logger LOGGER = LoggerFactory.getLogger(VariableService.class);
  private static final String ABSENT_PARENT_ID = "-1";

  @Autowired private TaskStore taskStore;
  @Autowired private VariableStore variableStore;
  @Autowired private DraftVariableStore draftVariableStore;
  @Autowired private TasklistProperties tasklistProperties;
  @Autowired private TaskValidator taskValidator;
  @Autowired private ListViewStore listViewStore;

  @Autowired
  @Qualifier("tasklistObjectMapper")
  private ObjectMapper objectMapper;

  public void persistDraftTaskVariables(
      final String taskId, final List draftTaskVariables) {
    try {
      final TaskEntity task = taskStore.getTask(taskId);
      taskValidator.validateCanPersistDraftTaskVariables(task);
      validateVariableInputs(draftTaskVariables);

      // drop all current draft variables
      final long deletedDraftVariablesCount = draftVariableStore.deleteAllByTaskId(taskId);
      LOGGER.debug(
          "'{}' draft task variables associated with task id '{}' were deleted",
          deletedDraftVariablesCount,
          task);

      if (CollectionUtils.isEmpty(draftTaskVariables)) {
        return;
      }

      final Map currentOriginalVariables = new HashMap<>();
      getRuntimeVariablesByRequest(GetVariablesRequest.createFrom(task))
          .forEach(originalVar -> currentOriginalVariables.put(originalVar.getName(), originalVar));

      final int variableSizeThreshold = tasklistProperties.getImporter().getVariableSizeThreshold();

      final Map toPersist = new HashMap<>();
      draftTaskVariables.forEach(
          draftVariable -> {
            if (currentOriginalVariables.containsKey(draftVariable.getName())) {
              final VariableEntity variableEntity =
                  currentOriginalVariables.get(draftVariable.getName());
              // Persist new draft variables based on the input if value `name` is the same as
              // original and `value` property is different
              if (!variableEntity.getFullValue().equals(draftVariable.getValue())) {
                toPersist.put(
                    draftVariable.getName(),
                    VariableService.createDraftVariableFrom(
                        // draft variable will have the same Id as original variable
                        variableEntity.getId(),
                        task,
                        draftVariable.getName(),
                        draftVariable.getValue(),
                        variableSizeThreshold));
              }
            } else {
              toPersist.put(
                  draftVariable.getName(),
                  VariableService.createDraftVariableFrom(
                      task,
                      draftVariable.getName(),
                      draftVariable.getValue(),
                      variableSizeThreshold));
            }
          });

      draftVariableStore.createOrUpdate(toPersist.values());
    } catch (final NotFoundException e) {
      throw new NotFoundApiException("Task not found", e);
    }
  }

  private void validateVariableInputs(final Collection variable) {
    variable.stream()
        .map(VariableInputDTO::getValue)
        .forEach(
            value -> {
              try {
                objectMapper.readValue(value, Object.class);
              } catch (final IOException e) {
                throw new InvalidRequestException(e.getMessage(), e);
              }
            });
  }

  public void persistTaskVariables(
      final String taskId,
      final List changedVariables,
      final boolean withDraftVariableValues) {
    // take current runtime variables values and
    final TaskEntity task = taskStore.getTask(taskId);
    final String taskFlowNodeInstanceId = task.getFlowNodeInstanceId();

    final List taskVariables =
        getRuntimeVariablesByRequest(GetVariablesRequest.createFrom(task));

    final Map finalVariablesMap = new HashMap<>();
    taskVariables.forEach(
        variable ->
            finalVariablesMap.put(
                variable.getName(), createSnapshotVariableFrom(taskId, variable)));

    // Snapshot Process Variable at the moment the task was completed as a Task Variable
    final Map finalVariablesListViewMap = new HashMap<>();
    taskVariables.forEach(
        variable ->
            finalVariablesListViewMap.put(
                variable.getName(),
                new VariableListViewEntity(variable)
                    .setScopeKey(taskFlowNodeInstanceId)
                    .setId(taskFlowNodeInstanceId + "-" + variable.getName())
                    .setJoin(
                        new ListViewJoinRelation(
                            "taskVariable", Long.valueOf(taskFlowNodeInstanceId)))));

    if (withDraftVariableValues) {
      // update/append with draft variables
      draftVariableStore
          .getVariablesByTaskIdAndVariableNames(taskId, Collections.emptyList())
          .forEach(
              draftTaskVariable ->
                  finalVariablesMap.put(
                      draftTaskVariable.getName(),
                      createSnapshotVariableFrom(taskId, draftTaskVariable)));
    }

    // update/append with variables passed for task completion
    for (final VariableInputDTO var : changedVariables) {
      finalVariablesMap.put(
          var.getName(),
          createSnapshotVariableFrom(
              task.getTenantId(),
              taskId,
              var.getName(),
              var.getValue(),
              tasklistProperties.getImporter().getVariableSizeThreshold()));

      // Add Variables added to a Task as a Process Variablee
      finalVariablesListViewMap.put(
          var.getName(),
          VariableListViewEntity.createFrom(
              task.getTenantId(),
              task.getFlowNodeInstanceId() + "-" + var.getName(),
              var.getName(),
              var.getValue(),
              task.getFlowNodeInstanceId(),
              tasklistProperties.getImporter().getVariableSizeThreshold(),
              new ListViewJoinRelation("taskVariable", Long.valueOf(taskFlowNodeInstanceId))));
    }
    listViewStore.persistTaskVariables(finalVariablesListViewMap.values());
    variableStore.persistTaskVariables(finalVariablesMap.values());
  }

  /** Deletes all draft variables associated with the task by {@code taskId}. */
  public void deleteDraftTaskVariables(final String taskId) {
    draftVariableStore.deleteAllByTaskId(taskId);
  }

  private List getRuntimeVariablesByRequest(
      final GetVariablesRequest getVariablesRequest) {
    final List requests = Collections.singletonList(getVariablesRequest);
    final Map> runtimeVariablesPerTaskId =
        getRuntimeVariablesPerTaskId(requests);
    if (runtimeVariablesPerTaskId.size() > 0) {
      return runtimeVariablesPerTaskId.values().iterator().next();
    } else {
      return new ArrayList<>();
    }
  }

  private List getRuntimeVariablesDTOPerTaskId(
      final List requests) {
    final Map> variablesByTaskIds =
        getRuntimeVariablesPerTaskId(requests);
    if (variablesByTaskIds.size() > 0) {
      return variablesByTaskIds.values().iterator().next();
    } else {
      return new ArrayList<>();
    }
  }

  private Map> getRuntimeVariablesPerTaskId(
      final List requests) {

    if (requests == null || requests.size() == 0) {
      return new HashMap<>();
    }

    // build flow node trees (for each process instance)
    final Map flowNodeTrees = buildFlowNodeTrees(requests);

    // build local variable map  (for each flow node instance)
    final List flowNodeInstanceIds =
        flowNodeTrees.values().stream()
            .flatMap(f -> f.getFlowNodeInstanceIds().stream())
            .collect(Collectors.toList());
    final Map variableMaps =
        buildVariableMaps(
            flowNodeInstanceIds,
            requests.stream()
                .map(GetVariablesRequest::getVarNames)
                .filter(Objects::nonNull)
                .flatMap(List::stream)
                .distinct()
                .collect(toList()),
            requests
                .get(0)
                .getFieldNames()); // we assume here that all requests has the same list of  fields

    return buildResponse(flowNodeTrees, variableMaps, requests);
  }

  /**
   * Builds lists of variables taking into account nested scopes.
   *
   * @param flowNodeTrees
   * @param variableMaps
   * @param requests
   * @return list of variables per each taskId
   */
  private Map> buildResponse(
      final Map flowNodeTrees,
      final Map variableMaps,
      final List requests) {

    final Map> response = new HashMap<>();

    for (final GetVariablesRequest req : requests) {
      final FlowNodeTree flowNodeTree = flowNodeTrees.get(req.getProcessInstanceId());

      final VariableMap resultingVariableMap = new VariableMap();

      accumulateVariables(
          resultingVariableMap, variableMaps, flowNodeTree, req.getFlowNodeInstanceId());

      response.put(
          req.getTaskId(),
          resultingVariableMap.entrySet().stream()
              .sorted(Entry.comparingByKey())
              .map(e -> e.getValue())
              .collect(Collectors.toList()));
    }
    return response;
  }

  @NotNull
  private Collector, ArrayList, ArrayList>
      getVariableDTOListCollector() {
    return Collector.of(
        ArrayList::new,
        (list, entry) -> list.add(VariableDTO.createFrom(entry.getValue())),
        (list1, list2) -> {
          list1.addAll(list2);
          return list1;
        });
  }

  private void accumulateVariables(
      final VariableMap resultingVariableMap,
      final Map variableMaps,
      final FlowNodeTree flowNodeTree,
      final String flowNodeInstanceId) {
    final VariableMap m = variableMaps.get(flowNodeInstanceId);
    if (m != null) {
      resultingVariableMap.putAll(m);
    }
    final String parentFlowNodeId =
        flowNodeTree != null ? flowNodeTree.getParent(flowNodeInstanceId) : null;
    if (parentFlowNodeId != null && !parentFlowNodeId.equals(ABSENT_PARENT_ID)) {
      accumulateVariables(resultingVariableMap, variableMaps, flowNodeTree, parentFlowNodeId);
    }
  }

  /**
   * Builds variable map for each flow node instance id: "local" variables for each flow node
   * instance.
   *
   * @param flowNodeInstanceIds
   * @return
   */
  private Map buildVariableMaps(
      final List flowNodeInstanceIds,
      final List varNames,
      final Set fieldNames) {
    // get list of all variables
    final List variables =
        variableStore.getVariablesByFlowNodeInstanceIds(flowNodeInstanceIds, varNames, fieldNames);

    return variables.stream()
        .collect(groupingBy(VariableEntity::getScopeFlowNodeId, getVariableMapCollector()));
  }

  @NotNull
  private Collector getVariableMapCollector() {
    return Collector.of(
        VariableMap::new,
        (map, var) -> map.put(var.getName(), var),
        (map1, map2) -> {
          map1.putAll(map2);
          return map1;
        });
  }

  /**
   * Builds flow node tree for each requested process instance id.
   *
   * @param requests
   * @return map of flow node trees per process instance id
   */
  private Map buildFlowNodeTrees(final List requests) {
    final List processInstanceIds =
        requests.stream()
            .map(GetVariablesRequest::getProcessInstanceId)
            .distinct()
            .collect(toList());
    // get all flow node instances for all process instance ids
    final List flowNodeInstances =
        variableStore.getFlowNodeInstances(processInstanceIds);

    final Map flowNodeTrees = new HashMap<>();
    for (final FlowNodeInstanceEntity flowNodeInstance : flowNodeInstances) {
      getFlowNodeTree(flowNodeTrees, flowNodeInstance.getProcessInstanceId())
          .setParent(flowNodeInstance.getId(), flowNodeInstance.getParentFlowNodeId());
    }
    return flowNodeTrees;
  }

  private FlowNodeTree getFlowNodeTree(
      final Map flowNodeTrees, final String processInstanceId) {
    if (flowNodeTrees.get(processInstanceId) == null) {
      flowNodeTrees.put(processInstanceId, new FlowNodeTree());
    }
    return flowNodeTrees.get(processInstanceId);
  }

  public List getVariableSearchResponses(
      final String taskId, final Set variableNames) {

    final TaskEntity task = taskStore.getTask(taskId);
    final List requests =
        Collections.singletonList(
            VariableStore.GetVariablesRequest.createFrom(task)
                .setVarNames(new ArrayList<>(variableNames))
                .setFieldNames(Collections.emptySet()));

    final List vars = new ArrayList<>();
    switch (task.getState()) {
      case CREATED -> {
        final Map nameToOriginalVariables = new HashMap<>();
        getRuntimeVariablesDTOPerTaskId(requests)
            .forEach(
                originalVar -> nameToOriginalVariables.put(originalVar.getName(), originalVar));
        final Map nameToDraftVariable = new HashMap<>();
        draftVariableStore
            .getVariablesByTaskIdAndVariableNames(taskId, new ArrayList<>(variableNames))
            .forEach(draftVar -> nameToDraftVariable.put(draftVar.getName(), draftVar));

        nameToOriginalVariables.forEach(
            (name, originalVar) -> {
              if (nameToDraftVariable.containsKey(name)) {
                vars.add(
                    VariableSearchResponse.createFrom(originalVar, nameToDraftVariable.get(name)));
              } else {
                vars.add(VariableSearchResponse.createFrom(originalVar));
              }
            });

        // creating variable responses for draft variables without original values
        CollectionUtils.removeAll(nameToDraftVariable.keySet(), nameToOriginalVariables.keySet())
            .forEach(
                draftVariableName ->
                    vars.add(
                        VariableSearchResponse.createFrom(
                            nameToDraftVariable.get(draftVariableName))));
      }
      case COMPLETED -> {
        final Map> variablesByTaskIds =
            variableStore.getTaskVariablesPerTaskId(requests);
        if (variablesByTaskIds.size() > 0) {
          vars.addAll(
              variablesByTaskIds.values().iterator().next().stream()
                  .map(VariableSearchResponse::createFrom)
                  .toList());
        }
      }
      default -> {}
    }

    return vars.stream().sorted(Comparator.comparing(VariableSearchResponse::getName)).toList();
  }

  public List getVariables(
      final String taskId, final List variableNames, final Set fieldNames) {
    final TaskEntity task = taskStore.getTask(taskId);
    final List requests =
        Collections.singletonList(
            GetVariablesRequest.createFrom(task)
                .setVarNames(variableNames)
                .setFieldNames(fieldNames));

    final List vars = new ArrayList<>();
    switch (task.getState()) {
      case CREATED ->
          vars.addAll(VariableDTO.createFrom(getRuntimeVariablesDTOPerTaskId(requests)));
      case COMPLETED -> {
        final Map> variablesByTaskIds =
            variableStore.getTaskVariablesPerTaskId(requests);
        if (variablesByTaskIds.size() > 0) {
          vars.addAll(
              variablesByTaskIds.values().iterator().next().stream()
                  .map(VariableDTO::createFrom)
                  .toList());
        }
      }
      default -> {}
    }

    vars.sort(Comparator.comparing(VariableDTO::getName));
    return vars;
  }

  public List> getVariables(final List requests) {
    final Map> variablesPerTaskId = getVariablesPerTaskId(requests);
    final List> result = new ArrayList<>();
    for (final GetVariablesRequest req : requests) {
      result.add(
          variablesPerTaskId.getOrDefault(req.getTaskId(), Collections.emptyList()).stream()
              .sorted(Comparator.comparing(VariableDTO::getName))
              .toList());
    }
    return result;
  }

  public Map> getVariablesPerTaskId(
      final List requests) {
    final Map> result = new HashMap<>();
    final Map> groupByStates =
        requests.stream().collect(groupingBy(GetVariablesRequest::getState));
    if (groupByStates.containsKey(TaskState.CREATED)) {
      result.putAll(
          getRuntimeVariablesPerTaskId(groupByStates.get(TaskState.CREATED)).entrySet().stream()
              .collect(Collectors.toMap(Entry::getKey, e -> VariableDTO.createFrom(e.getValue()))));
    }
    if (groupByStates.containsKey(TaskState.COMPLETED)) {
      result.putAll(
          variableStore
              .getTaskVariablesPerTaskId(groupByStates.get(TaskState.COMPLETED))
              .entrySet()
              .stream()
              .collect(
                  Collectors.toMap(
                      Entry::getKey, e -> VariableDTO.createFromTaskVariables(e.getValue()))));
    }
    return result;
  }

  public VariableDTO getVariable(final String variableId, final Set fieldNames) {
    try {
      // 1st search in runtime variables
      final VariableEntity runtimeVariable =
          variableStore.getRuntimeVariable(variableId, fieldNames);
      return VariableDTO.createFrom(runtimeVariable);
    } catch (final NotFoundException ex) {
      // then in task variables (for completed tasks)
      try {
        // 2nd search in runtime variables
        final SnapshotTaskVariableEntity taskVariable =
            variableStore.getTaskVariable(variableId, fieldNames);
        return VariableDTO.createFrom(taskVariable);
      } catch (final NotFoundException ex2) {
        throw new NotFoundApiException(String.format("Variable with id %s not found.", variableId));
      }
    }
  }

  public VariableResponse getVariableResponse(final String variableId) {
    try {
      // 1st search in runtime variables
      final VariableEntity runtimeVariable =
          variableStore.getRuntimeVariable(variableId, Collections.emptySet());
      final VariableResponse variableResponse = VariableResponse.createFrom(runtimeVariable);
      draftVariableStore.getById(variableId).ifPresent(variableResponse::addDraft);
      return variableResponse;
    } catch (final NotFoundException ex) {
      // 2nd then search in draft task variables
      return draftVariableStore
          .getById(variableId)
          .map(VariableResponse::createFrom)
          .orElseGet(
              () -> {
                try {
                  // 3rd search in task variables (for completed tasks)
                  final SnapshotTaskVariableEntity taskVariable =
                      variableStore.getTaskVariable(variableId, Collections.emptySet());
                  return VariableResponse.createFrom(taskVariable);
                } catch (final NotFoundException ex2) {
                  throw new NotFoundApiException(
                      String.format("Variable with id %s not found.", variableId));
                }
              });
    }
  }

  /**
   * We won't persist Job Workers Variables on List View. (But we are not able to identify in
   * advance if a variable comes from job worker or user task) This method removes variables by
   * flowNodeInstanceId
   *
   * @param flowNodeInstanceId the flow node ID of the task
   */
  public void removeVariableByFlowNodeInstanceId(final String flowNodeInstanceId) {
    listViewStore.removeVariableByFlowNodeInstanceId(flowNodeInstanceId);
  }

  public static String getDraftVariableId(final String idPrefix, final String name) {
    return String.format("%s-%s", idPrefix, name);
  }

  public static String getSnapshotVariableId(final String idPrefix, final String name) {
    return String.format("%s-%s", idPrefix, name);
  }

  private static DraftTaskVariableEntity createDraftVariableFrom(
      final TaskEntity taskEntity,
      final String name,
      final String value,
      final int variableSizeThreshold) {
    return completeVariableSetup(
        new DraftTaskVariableEntity().setId(getDraftVariableId(taskEntity.getId(), name)),
        taskEntity,
        name,
        value,
        variableSizeThreshold);
  }

  private static DraftTaskVariableEntity createDraftVariableFrom(
      final String draftVariableId,
      final TaskEntity taskEntity,
      final String name,
      final String value,
      final int variableSizeThreshold) {
    return completeVariableSetup(
        new DraftTaskVariableEntity().setId(draftVariableId),
        taskEntity,
        name,
        value,
        variableSizeThreshold);
  }

  private static DraftTaskVariableEntity completeVariableSetup(
      final DraftTaskVariableEntity entity,
      final TaskEntity taskEntity,
      final String name,
      final String value,
      final int variableSizeThreshold) {

    entity.setTaskId(taskEntity.getId()).setName(name).setTenantId(taskEntity.getTenantId());
    if (value.length() > variableSizeThreshold) {
      // store preview
      entity.setValue(value.substring(0, variableSizeThreshold));
      entity.setIsPreview(true);
    } else {
      entity.setValue(value);
    }
    entity.setFullValue(value);
    return entity;
  }

  public static SnapshotTaskVariableEntity createSnapshotVariableFrom(
      final String tenantId,
      final String taskId,
      final String name,
      final String value,
      final int variableSizeThreshold) {
    final SnapshotTaskVariableEntity entity =
        new SnapshotTaskVariableEntity()
            .setId(getSnapshotVariableId(taskId, name))
            .setTaskId(taskId)
            .setName(name);
    if (value.length() > variableSizeThreshold) {
      // store preview
      entity.setValue(value.substring(0, variableSizeThreshold));
      entity.setIsPreview(true);
    } else {
      entity.setValue(value);
    }
    entity.setFullValue(value);
    entity.setTenantId(tenantId);
    return entity;
  }

  public static SnapshotTaskVariableEntity createSnapshotVariableFrom(
      final String taskId, final VariableEntity variableEntity) {
    return new SnapshotTaskVariableEntity()
        .setId(getSnapshotVariableId(taskId, variableEntity.getName()))
        .setTaskId(taskId)
        .setName(variableEntity.getName())
        .setValue(variableEntity.getValue())
        .setIsPreview(variableEntity.getIsPreview())
        .setFullValue(variableEntity.getFullValue())
        .setTenantId(variableEntity.getTenantId());
  }

  public static SnapshotTaskVariableEntity createSnapshotVariableFrom(
      final String taskId, final DraftTaskVariableEntity draftTaskVariableEntity) {
    return new SnapshotTaskVariableEntity()
        .setId(getSnapshotVariableId(taskId, draftTaskVariableEntity.getName()))
        .setTaskId(taskId)
        .setName(draftTaskVariableEntity.getName())
        .setValue(draftTaskVariableEntity.getValue())
        .setIsPreview(draftTaskVariableEntity.getIsPreview())
        .setFullValue(draftTaskVariableEntity.getFullValue())
        .setTenantId(draftTaskVariableEntity.getTenantId());
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy