content: /*
   * Copyright 2010-2025 Hyland Software, Inc. and its affiliates.
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * 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.
   */
  package org.activiti.spring.process;
  
  import static java.util.Collections.emptyMap;
  
  import com.fasterxml.jackson.databind.node.ObjectNode;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.Map;
  import java.util.Optional;
  import java.util.Set;
  import org.activiti.bpmn.model.FlowElement;
  import org.activiti.engine.ActivitiException;
  import org.activiti.engine.impl.bpmn.behavior.MappingExecutionContext;
  import org.activiti.engine.impl.bpmn.behavior.VariablesCalculator;
  import org.activiti.engine.impl.interceptor.CommandContext;
  import org.activiti.engine.impl.persistence.entity.ExecutionEntity;
  import org.activiti.engine.impl.util.ProcessDefinitionUtil;
  import org.activiti.engine.impl.util.ProcessInstanceHelper;
  import org.activiti.engine.repository.ProcessDefinition;
  import org.activiti.runtime.api.impl.ExpressionResolver;
  import org.activiti.runtime.api.impl.SimpleMapExpressionEvaluator;
  import org.activiti.spring.process.model.Extension;
  import org.activiti.spring.process.model.VariableDefinition;
  import org.activiti.spring.process.variable.VariableParsingService;
  import org.activiti.spring.process.variable.VariableValidationService;
  
  public class ProcessVariablesInitiator extends ProcessInstanceHelper {
  
      private ProcessExtensionService processExtensionService;
  
      private final VariableParsingService variableParsingService;
  
      private final VariableValidationService variableValidationService;
  
      private VariablesCalculator variablesCalculator;
  
      private ExpressionResolver expressionResolver;
  
      public ProcessVariablesInitiator(
          ProcessExtensionService processExtensionService,
          VariableParsingService variableParsingService,
          VariableValidationService variableValidationService,
          VariablesCalculator variablesCalculator,
          ExpressionResolver expressionResolver
      ) {
          this.processExtensionService = processExtensionService;
          this.variableParsingService = variableParsingService;
          this.variableValidationService = variableValidationService;
          this.variablesCalculator = variablesCalculator;
          this.expressionResolver = expressionResolver;
      }
  
      public Map<String, Object> calculateVariablesFromExtensionFile(
          ProcessDefinition processDefinition,
          Map<String, Object> variables
      ) {
          Map<String, Object> processedVariables = new HashMap<>();
          if (processExtensionService.hasExtensionsFor(processDefinition)) {
              Extension processExtension = processExtensionService.getExtensionsFor(processDefinition);
  
              Map<String, VariableDefinition> variableDefinitionMap = processExtension.getProperties();
              processedVariables = processVariables(variables, variableDefinitionMap);
  
              Set<String> missingRequiredVars = checkRequiredVariables(processedVariables, variableDefinitionMap);
              if (!missingRequiredVars.isEmpty()) {
                  throw new ActivitiException(
                      "Can't start process '" +
                      processDefinition.getKey() +
                      "' without required variables - " +
                      String.join(", ", missingRequiredVars)
                  );
              }
              Set<String> varsWithMismatchedTypes = validateVariablesAgainstDefinitions(
                  processedVariables,
                  variableDefinitionMap
              );
              if (!varsWithMismatchedTypes.isEmpty()) {
                  throw new ActivitiException(
                      "Can't start process '" +
                      processDefinition.getKey() +
                      "' as variables fail type validation - " +
                      String.join(", ", varsWithMismatchedTypes)
                  );
              }
          }
  
          return processedVariables;
      }
  
      public Map<String, Object> calculateOutputVariables(
          Map<String, Object> variables,
          ProcessDefinition processDefinition,
          FlowElement initialFlowElement
      ) {
          Map<String, Object> processVariables = variables;
  
          if (processExtensionService.hasExtensionsFor(processDefinition)) {
              Map<String, Object> calculateOutPutVariables = variablesCalculator.calculateOutPutVariables(
                  MappingExecutionContext.buildMappingExecutionContext(
                      processDefinition.getId(),
                      initialFlowElement.getId()
                  ),
                  variables
              );
              if (!calculateOutPutVariables.isEmpty()) {
                  processVariables = calculateOutPutVariables;
              }
  
              processVariables = calculateVariablesFromExtensionFile(processDefinition, processVariables);
          }
          return processVariables;
      }
  
      private Map<String, Object> processVariables(
          Map<String, Object> variables,
          Map<String, VariableDefinition> variableDefinitionMap
      ) {
          Map<String, Object> newVarsMap = new HashMap<>(Optional.ofNullable(variables).orElse(emptyMap()));
          Map<String, Object> varsWithExpressions = new HashMap<>();
          variableDefinitionMap.forEach((k, v) -> {
              if (!newVarsMap.containsKey(v.getName()) && v.getValue() != null) {
                  Object value = createDefaultVariableValue(v);
                  if (expressionResolver.containsExpression(value) || value instanceof ObjectNode) {
                      varsWithExpressions.put(v.getName(), value);
                  } else {
                      newVarsMap.put(v.getName(), value);
                  }
              }
          });
  
          if (!varsWithExpressions.isEmpty()) {
              newVarsMap.putAll(
                  expressionResolver.resolveExpressionsMap(
                      new SimpleMapExpressionEvaluator(newVarsMap),
                      varsWithExpressions
                  )
              );
          }
  
          return newVarsMap;
      }
  
      private Object createDefaultVariableValue(VariableDefinition variableDefinition) {
          // take a default from the variable definition in the proc extensions
          return variableParsingService.parse(variableDefinition);
      }
  
      private Set<String> checkRequiredVariables(
          Map<String, Object> variables,
          Map<String, VariableDefinition> variableDefinitionMap
      ) {
          Set<String> missingRequiredVars = new HashSet<>();
          variableDefinitionMap.forEach((k, v) -> {
              if (!variables.containsKey(v.getName()) && v.isRequired()) {
                  missingRequiredVars.add(v.getName());
              }
          });
          return missingRequiredVars;
      }
  
      private Set<String> validateVariablesAgainstDefinitions(
          Map<String, Object> variables,
          Map<String, VariableDefinition> variableDefinitionMap
      ) {
          Set<String> mismatchedVars = new HashSet<>();
          variableDefinitionMap.forEach((k, v) -> {
              //if we have definition for this variable then validate it
              if (variables.containsKey(v.getName())) {
                  if (!variableValidationService.validate(variables.get(v.getName()), v)) {
                      mismatchedVars.add(v.getName());
                  }
              }
          });
          return mismatchedVars;
      }
  
      public void startProcessInstance(
          ExecutionEntity processInstance,
          CommandContext commandContext,
          Map<String, Object> variables,
          FlowElement initialFlowElement,
          Map<String, Object> transientVariables
      ) {
          ProcessDefinition processDefinition = ProcessDefinitionUtil.getProcessDefinition(
              processInstance.getProcessDefinitionId()
          );
          Map<String, Object> calculatedVariables = calculateOutputVariables(
              variables,
              processDefinition,
              initialFlowElement
          );
          super.startProcessInstance(
              processInstance,
              commandContext,
              calculatedVariables,
              initialFlowElement,
              transientVariables
          );
      }
  }
  