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.engine.impl.bpmn.helper;
  
  import java.util.*;
  import org.activiti.bpmn.model.*;
  import org.activiti.bpmn.model.Error;
  import org.activiti.bpmn.model.Process;
  import org.activiti.engine.ActivitiException;
  import org.activiti.engine.delegate.BpmnError;
  import org.activiti.engine.delegate.DelegateExecution;
  import org.activiti.engine.delegate.event.ActivitiEventType;
  import org.activiti.engine.delegate.event.impl.ActivitiEventBuilder;
  import org.activiti.engine.impl.context.Context;
  import org.activiti.engine.impl.persistence.entity.ExecutionEntity;
  import org.activiti.engine.impl.persistence.entity.ExecutionEntityManager;
  import org.activiti.engine.impl.util.CollectionUtil;
  import org.activiti.engine.impl.util.ProcessDefinitionUtil;
  import org.activiti.engine.impl.util.ReflectUtil;
  import org.apache.commons.lang3.StringUtils;
  
  /**
   * This class is responsible for finding and executing error handlers for BPMN Errors.
   *
   * Possible error handlers include Error Intermediate Events and Error Event Sub-Processes.
   *
  
  
   */
  public class ErrorPropagation {
  
      public static void propagateError(BpmnError error, DelegateExecution execution) {
          propagateError(error.getErrorCode(), execution);
      }
  
      public static void propagateError(String errorRef, DelegateExecution execution) {
          boolean isCatchExecutedForProcess = false;
          boolean isCatchExecutedForCallActivity = false;
  
          try {
              Map<String, List<Event>> eventMap = findCatchingEventsForProcess(
                  execution.getProcessDefinitionId(),
                  errorRef
              );
  
              if (!eventMap.isEmpty()) {
                  isCatchExecutedForProcess = executeCatch(eventMap, execution, errorRef);
              }
  
              if (!isCatchExecutedForProcess && isCallActivity(execution)) {
                  isCatchExecutedForCallActivity = findCatchingEventsAndExecuteCatchForCallActivity(errorRef, execution);
              }
          } finally {
              if (!isCatchExecutedForProcess && !isCatchExecutedForCallActivity) {
                  throw new BpmnError(
                      errorRef,
                      "No catching boundary event found for error with errorCode '" +
                      errorRef +
                      "', neither in same process nor in parent process"
                  );
              }
          }
      }
  
      protected static boolean executeCatch(
          Map<String, List<Event>> eventMap,
          DelegateExecution delegateExecution,
          String errorId
      ) {
          Event matchingEvent = null;
          ExecutionEntity currentExecution = (ExecutionEntity) delegateExecution;
          ExecutionEntity parentExecution = null;
  
          if (eventMap.containsKey(currentExecution.getActivityId())) {
              matchingEvent = eventMap.get(currentExecution.getActivityId()).get(0);
  
              // Check for multi instance
              if (currentExecution.getParentId() != null && currentExecution.getParent().isMultiInstanceRoot()) {
                  parentExecution = currentExecution.getParent();
              } else {
                  parentExecution = currentExecution;
              }
          } else {
              parentExecution = currentExecution.getParent();
  
              // Traverse parents until one is found that is a scope and matches the activity the boundary event is defined on
              while (matchingEvent == null && parentExecution != null) {
                  FlowElementsContainer currentContainer = null;
                  if (parentExecution.getCurrentFlowElement() instanceof FlowElementsContainer) {
                      currentContainer = (FlowElementsContainer) parentExecution.getCurrentFlowElement();
                  } else if (parentExecution.getId().equals(parentExecution.getProcessInstanceId())) {
                      currentContainer = ProcessDefinitionUtil.getProcess(parentExecution.getProcessDefinitionId());
                  }
  
                  for (String refId : eventMap.keySet()) {
                      List<Event> events = eventMap.get(refId);
                      if (CollectionUtil.isNotEmpty(events) && events.get(0) instanceof StartEvent) {
                          if (currentContainer.getFlowElement(refId) != null) {
                              matchingEvent = events.get(0);
                          }
                      }
                  }
  
                  if (matchingEvent == null) {
                      if (eventMap.containsKey(parentExecution.getActivityId())) {
                          matchingEvent = eventMap.get(parentExecution.getActivityId()).get(0);
  
                          // Check for multi instance
                          if (
                              parentExecution.getParentId() != null && parentExecution.getParent().isMultiInstanceRoot()
                          ) {
                              parentExecution = parentExecution.getParent();
                          }
                      } else if (StringUtils.isNotEmpty(parentExecution.getParentId())) {
                          parentExecution = parentExecution.getParent();
                      } else {
                          parentExecution = null;
                      }
                  }
              }
          }
  
          if (matchingEvent != null && parentExecution != null) {
              executeEventHandler(matchingEvent, parentExecution, currentExecution, errorId);
              return true;
          } else {
              return false;
          }
      }
  
      private static boolean isCallActivity(DelegateExecution delegateExecution) {
          return !delegateExecution.getProcessInstanceId().equals(delegateExecution.getRootProcessInstanceId());
      }
  
      protected static boolean findCatchingEventsAndExecuteCatchForCallActivity(
          String errorRef,
          DelegateExecution execution
      ) {
          ExecutionEntityManager executionEntityManager = Context.getCommandContext().getExecutionEntityManager();
          ExecutionEntity processInstanceExecution = executionEntityManager.findById(execution.getProcessInstanceId());
  
          Map<String, List<Event>> eventMap = Collections.emptyMap();
          if (processInstanceExecution != null) {
              ExecutionEntity parentExecution = processInstanceExecution.getSuperExecution();
  
              Set<String> toDeleteProcessInstanceIds = new HashSet<>();
              toDeleteProcessInstanceIds.add(execution.getProcessInstanceId());
  
              while (!parentExecution.isRootExecution() && eventMap.isEmpty()) {
                  eventMap = findCatchingEventsForProcess(parentExecution.getProcessDefinitionId(), errorRef);
                  if (!eventMap.isEmpty()) {
                      for (String processInstanceId : toDeleteProcessInstanceIds) {
                          deleteProcessInstanceEntity(errorRef, execution, executionEntityManager, processInstanceId);
                      }
                      return executeCatch(eventMap, parentExecution, errorRef);
                  } else {
                      toDeleteProcessInstanceIds.add(parentExecution.getProcessInstanceId());
                      ExecutionEntity superExecution = parentExecution.getSuperExecution();
                      if (superExecution != null) {
                          parentExecution = superExecution;
                      } else {
                          parentExecution = parentExecution.getProcessInstance();
                      }
                  }
              }
          }
  
          return false;
      }
  
      private static void deleteProcessInstanceEntity(
          String errorRef,
          DelegateExecution execution,
          ExecutionEntityManager executionEntityManager,
          String processInstanceId
      ) {
          ExecutionEntity processInstanceEntity = executionEntityManager.findById(processInstanceId);
  
          executionEntityManager.deleteProcessInstanceExecutionEntity(
              processInstanceEntity.getId(),
              execution.getCurrentFlowElement() != null ? execution.getCurrentFlowElement().getId() : null,
              "ERROR_EVENT " + errorRef,
              false,
              false
          );
          dispatchProcessErroredEvent(processInstanceEntity);
      }
  
      private static void dispatchProcessErroredEvent(ExecutionEntity processInstanceEntity) {
          if (
              Context.getProcessEngineConfiguration() != null &&
              Context.getProcessEngineConfiguration().getEventDispatcher().isEnabled()
          ) {
              Context.getProcessEngineConfiguration()
                  .getEventDispatcher()
                  .dispatchEvent(
                      ActivitiEventBuilder.createEntityEvent(
                          ActivitiEventType.PROCESS_COMPLETED_WITH_ERROR_END_EVENT,
                          processInstanceEntity
                      )
                  );
          }
      }
  
      protected static void executeEventHandler(
          Event event,
          ExecutionEntity parentExecution,
          ExecutionEntity currentExecution,
          String errorId
      ) {
          if (
              Context.getProcessEngineConfiguration() != null &&
              Context.getProcessEngineConfiguration().getEventDispatcher().isEnabled()
          ) {
              BpmnModel bpmnModel = ProcessDefinitionUtil.getBpmnModel(parentExecution.getProcessDefinitionId());
              if (bpmnModel != null) {
                  String errorCode = Optional.ofNullable(bpmnModel.getErrors().get(errorId))
                      .map(Error::getErrorCode)
                      .orElse(errorId);
  
                  Context.getProcessEngineConfiguration()
                      .getEventDispatcher()
                      .dispatchEvent(
                          ActivitiEventBuilder.createErrorEvent(
                              ActivitiEventType.ACTIVITY_ERROR_RECEIVED,
                              event.getId(),
                              errorId,
                              errorCode,
                              parentExecution.getId(),
                              parentExecution.getProcessInstanceId(),
                              parentExecution.getProcessDefinitionId()
                          )
                      );
              }
          }
  
          if (event instanceof StartEvent) {
              ExecutionEntityManager executionEntityManager = Context.getCommandContext().getExecutionEntityManager();
  
              if (!currentExecution.getParentId().equals(parentExecution.getId())) {
                  Context.getAgenda().planDestroyScopeOperation(currentExecution);
              } else {
                  executionEntityManager.deleteExecutionAndRelatedData(currentExecution, null);
              }
  
              ExecutionEntity eventSubProcessExecution = executionEntityManager.createChildExecution(parentExecution);
              eventSubProcessExecution.setCurrentFlowElement(event);
              Context.getAgenda().planContinueProcessOperation(eventSubProcessExecution);
          } else {
              ExecutionEntity boundaryExecution = null;
              List<? extends ExecutionEntity> childExecutions = parentExecution.getExecutions();
              for (ExecutionEntity childExecution : childExecutions) {
                  if (childExecution.getActivityId().equals(event.getId())) {
                      boundaryExecution = childExecution;
                  }
              }
  
              Context.getAgenda().planTriggerExecutionOperation(boundaryExecution);
          }
      }
  
      protected static Map<String, List<Event>> findCatchingEventsForProcess(
          String processDefinitionId,
          String errorRef
      ) {
          Map<String, List<Event>> eventMap = new HashMap<String, List<Event>>();
          Process process = ProcessDefinitionUtil.getProcess(processDefinitionId);
          BpmnModel bpmnModel = ProcessDefinitionUtil.getBpmnModel(processDefinitionId);
  
          String compareErrorCode = retrieveErrorCode(bpmnModel, errorRef);
  
          List<EventSubProcess> subProcesses = process.findFlowElementsOfType(EventSubProcess.class, true);
          for (EventSubProcess eventSubProcess : subProcesses) {
              for (FlowElement flowElement : eventSubProcess.getFlowElements()) {
                  if (flowElement instanceof StartEvent) {
                      StartEvent startEvent = (StartEvent) flowElement;
                      if (
                          CollectionUtil.isNotEmpty(startEvent.getEventDefinitions()) &&
                          startEvent.getEventDefinitions().get(0) instanceof ErrorEventDefinition
                      ) {
                          ErrorEventDefinition errorEventDef = (ErrorEventDefinition) startEvent
                              .getEventDefinitions()
                              .get(0);
                          String eventErrorCode = retrieveErrorCode(bpmnModel, errorEventDef.getErrorRef());
  
                          if (
                              eventErrorCode == null ||
                              compareErrorCode == null ||
                              eventErrorCode.equals(compareErrorCode)
                          ) {
                              List<Event> startEvents = new ArrayList<Event>();
                              startEvents.add(startEvent);
                              eventMap.put(eventSubProcess.getId(), startEvents);
                          }
                      }
                  }
              }
          }
  
          List<BoundaryEvent> boundaryEvents = process.findFlowElementsOfType(BoundaryEvent.class, true);
          for (BoundaryEvent boundaryEvent : boundaryEvents) {
              if (
                  boundaryEvent.getAttachedToRefId() != null &&
                  CollectionUtil.isNotEmpty(boundaryEvent.getEventDefinitions()) &&
                  boundaryEvent.getEventDefinitions().get(0) instanceof ErrorEventDefinition
              ) {
                  ErrorEventDefinition errorEventDef = (ErrorEventDefinition) boundaryEvent.getEventDefinitions().get(0);
                  String eventErrorCode = retrieveErrorCode(bpmnModel, errorEventDef.getErrorRef());
  
                  if (eventErrorCode == null || compareErrorCode == null || eventErrorCode.equals(compareErrorCode)) {
                      List<Event> elementBoundaryEvents = null;
                      if (!eventMap.containsKey(boundaryEvent.getAttachedToRefId())) {
                          elementBoundaryEvents = new ArrayList<Event>();
                          eventMap.put(boundaryEvent.getAttachedToRefId(), elementBoundaryEvents);
                      } else {
                          elementBoundaryEvents = eventMap.get(boundaryEvent.getAttachedToRefId());
                      }
                      elementBoundaryEvents.add(boundaryEvent);
                  }
              }
          }
          return eventMap;
      }
  
      public static boolean mapException(Exception e, ExecutionEntity execution, List<MapExceptionEntry> exceptionMap) {
          String errorCode = findMatchingExceptionMapping(e, exceptionMap);
          if (errorCode != null) {
              propagateError(errorCode, execution);
              return true;
          } else {
              ExecutionEntity callActivityExecution = null;
              ExecutionEntity parentExecution = execution.getParent();
              while (parentExecution != null && callActivityExecution == null) {
                  if (parentExecution.getId().equals(parentExecution.getProcessInstanceId())) {
                      if (parentExecution.getSuperExecution() != null) {
                          callActivityExecution = parentExecution.getSuperExecution();
                      } else {
                          parentExecution = null;
                      }
                  } else {
                      parentExecution = parentExecution.getParent();
                  }
              }
  
              if (callActivityExecution != null) {
                  CallActivity callActivity = (CallActivity) callActivityExecution.getCurrentFlowElement();
                  if (CollectionUtil.isNotEmpty(callActivity.getMapExceptions())) {
                      errorCode = findMatchingExceptionMapping(e, callActivity.getMapExceptions());
                      if (errorCode != null) {
                          propagateError(errorCode, callActivityExecution);
                          return true;
                      }
                  }
              }
  
              return false;
          }
      }
  
      protected static String findMatchingExceptionMapping(Exception e, List<MapExceptionEntry> exceptionMap) {
          String defaultExceptionMapping = null;
  
          for (MapExceptionEntry me : exceptionMap) {
              String exceptionClass = me.getClassName();
              String errorCode = me.getErrorCode();
  
              // save the first mapping with no exception class as default map
              if (
                  StringUtils.isNotEmpty(errorCode) &&
                  StringUtils.isEmpty(exceptionClass) &&
                  defaultExceptionMapping == null
              ) {
                  defaultExceptionMapping = errorCode;
                  continue;
              }
  
              // ignore if error code or class are not defined
              if (StringUtils.isEmpty(errorCode) || StringUtils.isEmpty(exceptionClass)) {
                  continue;
              }
  
              if (e.getClass().getName().equals(exceptionClass)) {
                  return errorCode;
              }
              if (me.isAndChildren()) {
                  Class<?> exceptionClassClass = ReflectUtil.loadClass(exceptionClass);
                  if (exceptionClassClass.isAssignableFrom(e.getClass())) {
                      return errorCode;
                  }
              }
          }
  
          return defaultExceptionMapping;
      }
  
      protected static String retrieveErrorCode(BpmnModel bpmnModel, String errorRef) {
          return Optional.ofNullable(errorRef)
              .map(ref -> {
                  if (bpmnModel.containsErrorRef(errorRef)) {
                      return Optional.ofNullable(bpmnModel.getErrors().get(errorRef))
                          .map(Error::getErrorCode)
                          .orElse(errorRef);
                  }
                  return errorRef;
              })
              .orElse(errorRef);
      }
  }
  