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.history;
  
  import java.util.Date;
  import java.util.List;
  import org.activiti.bpmn.model.FlowElement;
  import org.activiti.bpmn.model.FlowNode;
  import org.activiti.bpmn.model.SequenceFlow;
  import org.activiti.engine.delegate.event.ActivitiEventDispatcher;
  import org.activiti.engine.delegate.event.ActivitiEventType;
  import org.activiti.engine.delegate.event.impl.ActivitiEventBuilder;
  import org.activiti.engine.impl.cfg.IdGenerator;
  import org.activiti.engine.impl.cfg.ProcessEngineConfigurationImpl;
  import org.activiti.engine.impl.identity.Authentication;
  import org.activiti.engine.impl.persistence.AbstractManager;
  import org.activiti.engine.impl.persistence.cache.EntityCache;
  import org.activiti.engine.impl.persistence.entity.CommentEntity;
  import org.activiti.engine.impl.persistence.entity.ExecutionEntity;
  import org.activiti.engine.impl.persistence.entity.HistoricActivityInstanceEntity;
  import org.activiti.engine.impl.persistence.entity.HistoricDetailVariableInstanceUpdateEntity;
  import org.activiti.engine.impl.persistence.entity.HistoricIdentityLinkEntity;
  import org.activiti.engine.impl.persistence.entity.HistoricProcessInstanceEntity;
  import org.activiti.engine.impl.persistence.entity.HistoricTaskInstanceEntity;
  import org.activiti.engine.impl.persistence.entity.HistoricVariableInstanceEntity;
  import org.activiti.engine.impl.persistence.entity.IdentityLinkEntity;
  import org.activiti.engine.impl.persistence.entity.TaskEntity;
  import org.activiti.engine.impl.persistence.entity.VariableInstanceEntity;
  import org.activiti.engine.task.Event;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  /**
   * Manager class that centralises recording of all history-related operations that are originated from inside the engine.
   *
   */
  public class DefaultHistoryManager extends AbstractManager implements HistoryManager {
  
      private static Logger log = LoggerFactory.getLogger(DefaultHistoryManager.class.getName());
  
      private HistoryLevel historyLevel;
  
      public DefaultHistoryManager(ProcessEngineConfigurationImpl processEngineConfiguration, HistoryLevel historyLevel) {
          super(processEngineConfiguration);
          this.historyLevel = historyLevel;
      }
  
      /*
       * (non-Javadoc)
       *
       * @see org.activiti.engine.impl.history.HistoryManagerInterface# isHistoryLevelAtLeast(org.activiti.engine.impl.history.HistoryLevel)
       */
      @Override
      public boolean isHistoryLevelAtLeast(HistoryLevel level) {
          if (log.isDebugEnabled()) {
              log.debug("Current history level: {}, level required: {}", historyLevel, level);
          }
          // Comparing enums actually compares the location of values declared in
          // the enum
          return historyLevel.isAtLeast(level);
      }
  
      /*
       * (non-Javadoc)
       *
       * @see org.activiti.engine.impl.history.HistoryManagerInterface#isHistoryEnabled ()
       */
      @Override
      public boolean isHistoryEnabled() {
          if (log.isDebugEnabled()) {
              log.debug("Current history level: {}", historyLevel);
          }
          return !historyLevel.equals(HistoryLevel.NONE);
      }
  
      // Process related history
  
      /*
       * (non-Javadoc)
       *
       * @see org.activiti.engine.impl.history.HistoryManagerInterface# recordProcessInstanceEnd(java.lang.String, java.lang.String, java.lang.String)
       */
      @Override
      public void recordProcessInstanceEnd(String processInstanceId, String deleteReason, String activityId) {
          if (isHistoryLevelAtLeast(HistoryLevel.ACTIVITY)) {
              HistoricProcessInstanceEntity historicProcessInstance = getHistoricProcessInstanceEntityManager().findById(
                  processInstanceId
              );
  
              if (historicProcessInstance != null) {
                  historicProcessInstance.markEnded(deleteReason);
                  historicProcessInstance.setEndActivityId(activityId);
  
                  // Fire event
                  ActivitiEventDispatcher activitiEventDispatcher = getEventDispatcher();
                  if (activitiEventDispatcher != null && activitiEventDispatcher.isEnabled()) {
                      activitiEventDispatcher.dispatchEvent(
                          ActivitiEventBuilder.createEntityEvent(
                              ActivitiEventType.HISTORIC_PROCESS_INSTANCE_ENDED,
                              historicProcessInstance
                          )
                      );
                  }
              }
          }
      }
  
      @Override
      public void recordProcessInstanceNameChange(String processInstanceId, String newName) {
          if (isHistoryLevelAtLeast(HistoryLevel.ACTIVITY)) {
              HistoricProcessInstanceEntity historicProcessInstance = getHistoricProcessInstanceEntityManager().findById(
                  processInstanceId
              );
  
              if (historicProcessInstance != null) {
                  historicProcessInstance.setName(newName);
              }
          }
      }
  
      /*
       * (non-Javadoc)
       *
       * @see org.activiti.engine.impl.history.HistoryManagerInterface# recordProcessInstanceStart (org.activiti.engine.impl.persistence.entity.ExecutionEntity)
       */
      @Override
      public void recordProcessInstanceStart(ExecutionEntity processInstance, FlowElement startElement) {
          if (isHistoryLevelAtLeast(HistoryLevel.ACTIVITY)) {
              HistoricProcessInstanceEntity historicProcessInstance = getHistoricProcessInstanceEntityManager().create(
                  processInstance
              );
              historicProcessInstance.setStartActivityId(startElement.getId());
  
              // Insert historic process-instance
              getHistoricProcessInstanceEntityManager().insert(historicProcessInstance, false);
  
              // Fire event
              ActivitiEventDispatcher activitiEventDispatcher = getEventDispatcher();
              if (activitiEventDispatcher != null && activitiEventDispatcher.isEnabled()) {
                  activitiEventDispatcher.dispatchEvent(
                      ActivitiEventBuilder.createEntityEvent(
                          ActivitiEventType.HISTORIC_PROCESS_INSTANCE_CREATED,
                          historicProcessInstance
                      )
                  );
              }
          }
      }
  
      /*
       * (non-Javadoc)
       *
       * @see org.activiti.engine.impl.history.HistoryManagerInterface# recordSubProcessInstanceStart (org.activiti.engine.impl.persistence.entity.ExecutionEntity,
       * org.activiti.engine.impl.persistence.entity.ExecutionEntity)
       */
      @Override
      public void recordSubProcessInstanceStart(
          ExecutionEntity parentExecution,
          ExecutionEntity subProcessInstance,
          FlowElement initialElement
      ) {
          if (isHistoryLevelAtLeast(HistoryLevel.ACTIVITY)) {
              HistoricProcessInstanceEntity historicProcessInstance = getHistoricProcessInstanceEntityManager().create(
                  subProcessInstance
              );
  
              // Fix for ACT-1728: startActivityId not initialized with subprocess instance
              if (historicProcessInstance.getStartActivityId() == null) {
                  historicProcessInstance.setStartActivityId(initialElement.getId());
              }
              getHistoricProcessInstanceEntityManager().insert(historicProcessInstance, false);
  
              // Fire event
              ActivitiEventDispatcher activitiEventDispatcher = getEventDispatcher();
              if (activitiEventDispatcher != null && activitiEventDispatcher.isEnabled()) {
                  activitiEventDispatcher.dispatchEvent(
                      ActivitiEventBuilder.createEntityEvent(
                          ActivitiEventType.HISTORIC_PROCESS_INSTANCE_CREATED,
                          historicProcessInstance
                      )
                  );
              }
  
              HistoricActivityInstanceEntity activitiyInstance = findActivityInstance(parentExecution, false, true);
              if (activitiyInstance != null) {
                  activitiyInstance.setCalledProcessInstanceId(subProcessInstance.getProcessInstanceId());
              }
          }
      }
  
      // Activity related history
  
      /*
       * (non-Javadoc)
       *
       * @see org.activiti.engine.impl.history.HistoryManagerInterface#recordActivityStart (org.activiti.engine.impl.persistence.entity.ExecutionEntity)
       */
      @Override
      public void recordActivityStart(ExecutionEntity executionEntity) {
          if (isHistoryLevelAtLeast(HistoryLevel.ACTIVITY)) {
              if (executionEntity.getActivityId() != null && executionEntity.getCurrentFlowElement() != null) {
                  HistoricActivityInstanceEntity historicActivityInstanceEntity = null;
  
                  // Historic activity instance could have been created (but only in cache, never persisted)
                  // for example when submitting form properties
                  HistoricActivityInstanceEntity historicActivityInstanceEntityFromCache =
                      getHistoricActivityInstanceFromCache(
                          executionEntity.getId(),
                          executionEntity.getActivityId(),
                          true
                      );
                  if (historicActivityInstanceEntityFromCache != null) {
                      historicActivityInstanceEntity = historicActivityInstanceEntityFromCache;
                  } else {
                      historicActivityInstanceEntity = createHistoricActivityInstanceEntity(executionEntity);
                  }
  
                  // Fire event
                  ActivitiEventDispatcher activitiEventDispatcher = getEventDispatcher();
                  if (activitiEventDispatcher != null && activitiEventDispatcher.isEnabled()) {
                      activitiEventDispatcher.dispatchEvent(
                          ActivitiEventBuilder.createEntityEvent(
                              ActivitiEventType.HISTORIC_ACTIVITY_INSTANCE_CREATED,
                              historicActivityInstanceEntity
                          )
                      );
                  }
              }
          }
      }
  
      /*
       * (non-Javadoc)
       *
       * @see org.activiti.engine.impl.history.HistoryManagerInterface#recordActivityEnd (org.activiti.engine.impl.persistence.entity.ExecutionEntity)
       */
      @Override
      public void recordActivityEnd(ExecutionEntity executionEntity, String deleteReason) {
          if (isHistoryLevelAtLeast(HistoryLevel.ACTIVITY)) {
              HistoricActivityInstanceEntity historicActivityInstance = findActivityInstance(
                  executionEntity,
                  false,
                  true
              );
              if (historicActivityInstance != null) {
                  historicActivityInstance.markEnded(deleteReason);
  
                  // Fire event
                  ActivitiEventDispatcher activitiEventDispatcher = getEventDispatcher();
                  if (activitiEventDispatcher != null && activitiEventDispatcher.isEnabled()) {
                      activitiEventDispatcher.dispatchEvent(
                          ActivitiEventBuilder.createEntityEvent(
                              ActivitiEventType.HISTORIC_ACTIVITY_INSTANCE_ENDED,
                              historicActivityInstance
                          )
                      );
                  }
              }
          }
      }
  
      @Override
      public HistoricActivityInstanceEntity findActivityInstance(
          ExecutionEntity execution,
          boolean createOnNotFound,
          boolean endTimeMustBeNull
      ) {
          String activityId = null;
          if (execution.getCurrentFlowElement() instanceof FlowNode) {
              activityId = execution.getCurrentFlowElement().getId();
          } else if (
              execution.getCurrentFlowElement() instanceof SequenceFlow && execution.getCurrentActivitiListener() == null
          ) {
              // while executing sequence flow listeners, we don't want historic activities
              activityId = ((SequenceFlow) (execution.getCurrentFlowElement())).getSourceFlowElement().getId();
          }
  
          if (activityId != null) {
              return findActivityInstance(execution, activityId, createOnNotFound, endTimeMustBeNull);
          }
  
          return null;
      }
  
      public HistoricActivityInstanceEntity findActivityInstance(
          ExecutionEntity execution,
          String activityId,
          boolean createOnNotFound,
          boolean endTimeMustBeNull
      ) {
          // No use looking for the HistoricActivityInstance when no activityId is provided.
          if (activityId == null) {
              return null;
          }
  
          String executionId = execution.getId();
  
          // Check the cache
          HistoricActivityInstanceEntity historicActivityInstanceEntityFromCache = getHistoricActivityInstanceFromCache(
              executionId,
              activityId,
              endTimeMustBeNull
          );
          if (historicActivityInstanceEntityFromCache != null) {
              return historicActivityInstanceEntityFromCache;
          }
  
          // If the execution was freshly created, there is no need to check the database,
          // there can never be an entry for a historic activity instance with this execution id.
          if (!execution.isInserted() && !execution.isProcessInstanceType()) {
              // Check the database
              List<HistoricActivityInstanceEntity> historicActivityInstances =
                  getHistoricActivityInstanceEntityManager().findUnfinishedHistoricActivityInstancesByExecutionAndActivityId(
                      executionId,
                      activityId
                  );
  
              if (historicActivityInstances.size() > 0) {
                  return historicActivityInstances.get(0);
              }
          }
  
          if (execution.getParentId() != null) {
              HistoricActivityInstanceEntity historicActivityInstanceFromParent = findActivityInstance(
                  (ExecutionEntity) execution.getParent(),
                  activityId,
                  false,
                  endTimeMustBeNull
              ); // always false for create, we only check if it can be found
              if (historicActivityInstanceFromParent != null) {
                  return historicActivityInstanceFromParent;
              }
          }
  
          if (
              createOnNotFound &&
              activityId != null &&
              ((execution.getCurrentFlowElement() != null && execution.getCurrentFlowElement() instanceof FlowNode) ||
                  execution.getCurrentFlowElement() == null)
          ) {
              return createHistoricActivityInstanceEntity(execution);
          }
  
          return null;
      }
  
      protected HistoricActivityInstanceEntity getHistoricActivityInstanceFromCache(
          String executionId,
          String activityId,
          boolean endTimeMustBeNull
      ) {
          List<HistoricActivityInstanceEntity> cachedHistoricActivityInstances = getEntityCache().findInCache(
              HistoricActivityInstanceEntity.class
          );
          for (HistoricActivityInstanceEntity cachedHistoricActivityInstance : cachedHistoricActivityInstances) {
              if (
                  activityId != null &&
                  activityId.equals(cachedHistoricActivityInstance.getActivityId()) &&
                  (!endTimeMustBeNull || cachedHistoricActivityInstance.getEndTime() == null)
              ) {
                  if (executionId.equals(cachedHistoricActivityInstance.getExecutionId())) {
                      return cachedHistoricActivityInstance;
                  }
              }
          }
  
          return null;
      }
  
      protected HistoricActivityInstanceEntity createHistoricActivityInstanceEntity(ExecutionEntity execution) {
          IdGenerator idGenerator = getProcessEngineConfiguration().getIdGenerator();
  
          String processDefinitionId = execution.getProcessDefinitionId();
          String processInstanceId = execution.getProcessInstanceId();
  
          HistoricActivityInstanceEntity historicActivityInstance = getHistoricActivityInstanceEntityManager().create();
          historicActivityInstance.setId(idGenerator.getNextId());
          historicActivityInstance.setProcessDefinitionId(processDefinitionId);
          historicActivityInstance.setProcessInstanceId(processInstanceId);
          historicActivityInstance.setExecutionId(execution.getId());
          historicActivityInstance.setActivityId(execution.getActivityId());
          if (execution.getCurrentFlowElement() != null) {
              historicActivityInstance.setActivityName(execution.getCurrentFlowElement().getName());
              historicActivityInstance.setActivityType(parseActivityType(execution.getCurrentFlowElement()));
          }
          Date now = getClock().getCurrentTime();
          historicActivityInstance.setStartTime(now);
  
          // Inherit tenant id (if applicable)
          if (execution.getTenantId() != null) {
              historicActivityInstance.setTenantId(execution.getTenantId());
          }
  
          getHistoricActivityInstanceEntityManager().insert(historicActivityInstance);
          return historicActivityInstance;
      }
  
      /*
       * (non-Javadoc)
       *
       * @see org.activiti.engine.impl.history.HistoryManagerInterface# recordProcessDefinitionChange(java.lang.String, java.lang.String)
       */
      @Override
      public void recordProcessDefinitionChange(String processInstanceId, String processDefinitionId) {
          if (isHistoryLevelAtLeast(HistoryLevel.ACTIVITY)) {
              HistoricProcessInstanceEntity historicProcessInstance = getHistoricProcessInstanceEntityManager().findById(
                  processInstanceId
              );
              if (historicProcessInstance != null) {
                  historicProcessInstance.setProcessDefinitionId(processDefinitionId);
              }
          }
      }
  
      // Task related history
  
      /*
       * (non-Javadoc)
       *
       * @see org.activiti.engine.impl.history.HistoryManagerInterface#recordTaskCreated (org.activiti.engine.impl.persistence.entity.TaskEntity,
       * org.activiti.engine.impl.persistence.entity.ExecutionEntity)
       */
      @Override
      public void recordTaskCreated(TaskEntity task, ExecutionEntity execution) {
          if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
              HistoricTaskInstanceEntity historicTaskInstance = getHistoricTaskInstanceEntityManager().create(
                  task,
                  execution
              );
              getHistoricTaskInstanceEntityManager().insert(historicTaskInstance, false);
          }
  
          recordTaskId(task);
      }
  
      /*
       * (non-Javadoc)
       *
       * @see org.activiti.engine.impl.history.HistoryManagerInterface#recordTaskAssignment (org.activiti.engine.impl.persistence.entity.TaskEntity)
       */
      @Override
      public void recordTaskAssignment(TaskEntity task) {
          ExecutionEntity executionEntity = task.getExecution();
          if (isHistoryLevelAtLeast(HistoryLevel.ACTIVITY)) {
              if (executionEntity != null) {
                  HistoricActivityInstanceEntity historicActivityInstance = findActivityInstance(
                      executionEntity,
                      false,
                      true
                  );
                  if (historicActivityInstance != null) {
                      historicActivityInstance.setAssignee(task.getAssignee());
                  }
              }
          }
      }
  
      /*
       * (non-Javadoc)
       *
       * @see org.activiti.engine.impl.history.HistoryManagerInterface#recordTaskClaim (org.activiti.engine.impl.persistence.entity.TaskEntity)
       */
  
      @Override
      public void recordTaskClaim(TaskEntity task) {
          if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
              HistoricTaskInstanceEntity historicTaskInstance = getHistoricTaskInstanceEntityManager().findById(
                  task.getId()
              );
              if (historicTaskInstance != null) {
                  historicTaskInstance.setClaimTime(task.getClaimTime());
              }
          }
      }
  
      /*
       * (non-Javadoc)
       *
       * @see org.activiti.engine.impl.history.HistoryManagerInterface#recordTaskId (org.activiti.engine.impl.persistence.entity.TaskEntity)
       */
      @Override
      public void recordTaskId(TaskEntity task) {
          if (isHistoryLevelAtLeast(HistoryLevel.ACTIVITY)) {
              ExecutionEntity execution = task.getExecution();
              if (execution != null) {
                  HistoricActivityInstanceEntity historicActivityInstance = findActivityInstance(execution, false, true);
                  if (historicActivityInstance != null) {
                      historicActivityInstance.setTaskId(task.getId());
                  }
              }
          }
      }
  
      /*
       * (non-Javadoc)
       *
       * @see org.activiti.engine.impl.history.HistoryManagerInterface#recordTaskEnd (java.lang.String, java.lang.String)
       */
      @Override
      public void recordTaskEnd(String taskId, String deleteReason) {
          if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
              HistoricTaskInstanceEntity historicTaskInstance = getHistoricTaskInstanceEntityManager().findById(taskId);
              if (historicTaskInstance != null) {
                  historicTaskInstance.markEnded(deleteReason);
              }
          }
      }
  
      /*
       * (non-Javadoc)
       *
       * @see org.activiti.engine.impl.history.HistoryManagerInterface# recordTaskAssigneeChange(java.lang.String, java.lang.String)
       */
      @Override
      public void recordTaskAssigneeChange(String taskId, String assignee) {
          if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
              HistoricTaskInstanceEntity historicTaskInstance = getHistoricTaskInstanceEntityManager().findById(taskId);
              if (historicTaskInstance != null) {
                  historicTaskInstance.setAssignee(assignee);
              }
          }
      }
  
      /*
       * (non-Javadoc)
       *
       * @see org.activiti.engine.impl.history.HistoryManagerInterface# recordTaskOwnerChange(java.lang.String, java.lang.String)
       */
      @Override
      public void recordTaskOwnerChange(String taskId, String owner) {
          if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
              HistoricTaskInstanceEntity historicTaskInstance = getHistoricTaskInstanceEntityManager().findById(taskId);
              if (historicTaskInstance != null) {
                  historicTaskInstance.setOwner(owner);
              }
          }
      }
  
      /*
       * (non-Javadoc)
       *
       * @see org.activiti.engine.impl.history.HistoryManagerInterface#recordTaskNameChange (java.lang.String, java.lang.String)
       */
      @Override
      public void recordTaskNameChange(String taskId, String taskName) {
          if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
              HistoricTaskInstanceEntity historicTaskInstance = getHistoricTaskInstanceEntityManager().findById(taskId);
              if (historicTaskInstance != null) {
                  historicTaskInstance.setName(taskName);
              }
          }
      }
  
      /*
       * (non-Javadoc)
       *
       * @see org.activiti.engine.impl.history.HistoryManagerInterface# recordTaskDescriptionChange(java.lang.String, java.lang.String)
       */
      @Override
      public void recordTaskDescriptionChange(String taskId, String description) {
          if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
              HistoricTaskInstanceEntity historicTaskInstance = getHistoricTaskInstanceEntityManager().findById(taskId);
              if (historicTaskInstance != null) {
                  historicTaskInstance.setDescription(description);
              }
          }
      }
  
      /*
       * (non-Javadoc)
       *
       * @see org.activiti.engine.impl.history.HistoryManagerInterface# recordTaskDueDateChange(java.lang.String, java.util.Date)
       */
      @Override
      public void recordTaskDueDateChange(String taskId, Date dueDate) {
          if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
              HistoricTaskInstanceEntity historicTaskInstance = getHistoricTaskInstanceEntityManager().findById(taskId);
              if (historicTaskInstance != null) {
                  historicTaskInstance.setDueDate(dueDate);
              }
          }
      }
  
      /*
       * (non-Javadoc)
       *
       * @see org.activiti.engine.impl.history.HistoryManagerInterface# recordTaskPriorityChange(java.lang.String, int)
       */
      @Override
      public void recordTaskPriorityChange(String taskId, int priority) {
          if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
              HistoricTaskInstanceEntity historicTaskInstance = getHistoricTaskInstanceEntityManager().findById(taskId);
              if (historicTaskInstance != null) {
                  historicTaskInstance.setPriority(priority);
              }
          }
      }
  
      /*
       * (non-Javadoc)
       *
       * @see org.activiti.engine.impl.history.HistoryManagerInterface# recordTaskCategoryChange(java.lang.String, java.lang.String)
       */
      @Override
      public void recordTaskCategoryChange(String taskId, String category) {
          if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
              HistoricTaskInstanceEntity historicTaskInstance = getHistoricTaskInstanceEntityManager().findById(taskId);
              if (historicTaskInstance != null) {
                  historicTaskInstance.setCategory(category);
              }
          }
      }
  
      @Override
      public void recordTaskFormKeyChange(String taskId, String formKey) {
          if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
              HistoricTaskInstanceEntity historicTaskInstance = getHistoricTaskInstanceEntityManager().findById(taskId);
              if (historicTaskInstance != null) {
                  historicTaskInstance.setFormKey(formKey);
              }
          }
      }
  
      /*
       * (non-Javadoc)
       *
       * @see org.activiti.engine.impl.history.HistoryManagerInterface# recordTaskParentTaskIdChange(java.lang.String, java.lang.String)
       */
      @Override
      public void recordTaskParentTaskIdChange(String taskId, String parentTaskId) {
          if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
              HistoricTaskInstanceEntity historicTaskInstance = getHistoricTaskInstanceEntityManager().findById(taskId);
              if (historicTaskInstance != null) {
                  historicTaskInstance.setParentTaskId(parentTaskId);
              }
          }
      }
  
      /*
       * (non-Javadoc)
       *
       * @see org.activiti.engine.impl.history.HistoryManagerInterface# recordTaskExecutionIdChange(java.lang.String, java.lang.String)
       */
      @Override
      public void recordTaskExecutionIdChange(String taskId, String executionId) {
          if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
              HistoricTaskInstanceEntity historicTaskInstance = getHistoricTaskInstanceEntityManager().findById(taskId);
              if (historicTaskInstance != null) {
                  historicTaskInstance.setExecutionId(executionId);
              }
          }
      }
  
      /*
       * (non-Javadoc)
       *
       * @see org.activiti.engine.impl.history.HistoryManagerInterface# recordTaskDefinitionKeyChange (org.activiti.engine.impl.persistence.entity.TaskEntity, java.lang.String)
       */
      @Override
      public void recordTaskDefinitionKeyChange(String taskId, String taskDefinitionKey) {
          if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
              HistoricTaskInstanceEntity historicTaskInstance = getHistoricTaskInstanceEntityManager().findById(taskId);
              if (historicTaskInstance != null) {
                  historicTaskInstance.setTaskDefinitionKey(taskDefinitionKey);
              }
          }
      }
  
      /* (non-Javadoc)
       * @see org.activiti.engine.impl.history.HistoryManagerInterface#recordTaskProcessDefinitionChange(java.lang.String, java.lang.String)
       */
      @Override
      public void recordTaskProcessDefinitionChange(String taskId, String processDefinitionId) {
          if (isHistoryLevelAtLeast(HistoryLevel.ACTIVITY)) {
              HistoricTaskInstanceEntity historicTaskInstance = getHistoricTaskInstanceEntityManager().findById(taskId);
              if (historicTaskInstance != null) {
                  historicTaskInstance.setProcessDefinitionId(processDefinitionId);
              }
          }
      }
  
      // Variables related history
  
      /*
       * (non-Javadoc)
       *
       * @see org.activiti.engine.impl.history.HistoryManagerInterface#recordVariableCreate (org.activiti.engine.impl.persistence.entity.VariableInstanceEntity)
       */
      @Override
      public void recordVariableCreate(VariableInstanceEntity variable) {
          // Historic variables
          if (isHistoryLevelAtLeast(HistoryLevel.ACTIVITY)) {
              getHistoricVariableInstanceEntityManager().copyAndInsert(variable);
          }
      }
  
      /*
       * (non-Javadoc)
       *
       * @see org.activiti.engine.impl.history.HistoryManagerInterface# recordHistoricDetailVariableCreate (org.activiti.engine.impl.persistence.entity.VariableInstanceEntity,
       * org.activiti.engine.impl.persistence.entity.ExecutionEntity, boolean)
       */
      @Override
      public void recordHistoricDetailVariableCreate(
          VariableInstanceEntity variable,
          ExecutionEntity sourceActivityExecution,
          boolean useActivityId
      ) {
          if (isHistoryLevelAtLeast(HistoryLevel.FULL)) {
              HistoricDetailVariableInstanceUpdateEntity historicVariableUpdate =
                  getHistoricDetailEntityManager().copyAndInsertHistoricDetailVariableInstanceUpdateEntity(variable);
  
              if (useActivityId && sourceActivityExecution != null) {
                  HistoricActivityInstanceEntity historicActivityInstance = findActivityInstance(
                      sourceActivityExecution,
                      false,
                      false
                  );
                  if (historicActivityInstance != null) {
                      historicVariableUpdate.setActivityInstanceId(historicActivityInstance.getId());
                  }
              }
          }
      }
  
      /*
       * (non-Javadoc)
       *
       * @see org.activiti.engine.impl.history.HistoryManagerInterface#recordVariableUpdate (org.activiti.engine.impl.persistence.entity.VariableInstanceEntity)
       */
      @Override
      public void recordVariableUpdate(VariableInstanceEntity variable) {
          if (isHistoryLevelAtLeast(HistoryLevel.ACTIVITY)) {
              HistoricVariableInstanceEntity historicProcessVariable = getEntityCache().findInCache(
                  HistoricVariableInstanceEntity.class,
                  variable.getId()
              );
              if (historicProcessVariable == null) {
                  historicProcessVariable =
                      getHistoricVariableInstanceEntityManager().findHistoricVariableInstanceByVariableInstanceId(
                          variable.getId()
                      );
              }
  
              if (historicProcessVariable != null) {
                  getHistoricVariableInstanceEntityManager().copyVariableValue(historicProcessVariable, variable);
              } else {
                  getHistoricVariableInstanceEntityManager().copyAndInsert(variable);
              }
          }
      }
  
      // Comment related history
  
      /*
       * (non-Javadoc)
       *
       * @see org.activiti.engine.impl.history.HistoryManagerInterface# createIdentityLinkComment(java.lang.String, java.lang.String, java.lang.String, java.lang.String, boolean)
       */
      @Override
      public void createIdentityLinkComment(String taskId, String userId, String groupId, String type, boolean create) {
          createIdentityLinkComment(taskId, userId, groupId, type, create, false);
      }
  
      @Override
      public void createUserIdentityLinkComment(String taskId, String userId, String type, boolean create) {
          createIdentityLinkComment(taskId, userId, null, type, create, false);
      }
  
      @Override
      public void createGroupIdentityLinkComment(String taskId, String groupId, String type, boolean create) {
          createIdentityLinkComment(taskId, null, groupId, type, create, false);
      }
  
      @Override
      public void createUserIdentityLinkComment(
          String taskId,
          String userId,
          String type,
          boolean create,
          boolean forceNullUserId
      ) {
          createIdentityLinkComment(taskId, userId, null, type, create, forceNullUserId);
      }
  
      /*
       * (non-Javadoc)
       *
       * @see org.activiti.engine.impl.history.HistoryManagerInterface# createIdentityLinkComment(java.lang.String, java.lang.String, java.lang.String, java.lang.String, boolean, boolean)
       */
      @Override
      public void createIdentityLinkComment(
          String taskId,
          String userId,
          String groupId,
          String type,
          boolean create,
          boolean forceNullUserId
      ) {
          if (isHistoryEnabled()) {
              String authenticatedUserId = Authentication.getAuthenticatedUserId();
              CommentEntity comment = getCommentEntityManager().create();
              comment.setUserId(authenticatedUserId);
              comment.setType(CommentEntity.TYPE_EVENT);
              comment.setTime(getClock().getCurrentTime());
              comment.setTaskId(taskId);
              if (userId != null || forceNullUserId) {
                  if (create) {
                      comment.setAction(Event.ACTION_ADD_USER_LINK);
                  } else {
                      comment.setAction(Event.ACTION_DELETE_USER_LINK);
                  }
                  comment.setMessage(new String[] { userId, type });
              } else {
                  if (create) {
                      comment.setAction(Event.ACTION_ADD_GROUP_LINK);
                  } else {
                      comment.setAction(Event.ACTION_DELETE_GROUP_LINK);
                  }
                  comment.setMessage(new String[] { groupId, type });
              }
  
              getCommentEntityManager().insert(comment);
          }
      }
  
      @Override
      public void createProcessInstanceIdentityLinkComment(
          String processInstanceId,
          String userId,
          String groupId,
          String type,
          boolean create
      ) {
          createProcessInstanceIdentityLinkComment(processInstanceId, userId, groupId, type, create, false);
      }
  
      @Override
      public void createProcessInstanceIdentityLinkComment(
          String processInstanceId,
          String userId,
          String groupId,
          String type,
          boolean create,
          boolean forceNullUserId
      ) {
          if (isHistoryEnabled()) {
              String authenticatedUserId = Authentication.getAuthenticatedUserId();
              CommentEntity comment = getCommentEntityManager().create();
              comment.setUserId(authenticatedUserId);
              comment.setType(CommentEntity.TYPE_EVENT);
              comment.setTime(getClock().getCurrentTime());
              comment.setProcessInstanceId(processInstanceId);
              if (userId != null || forceNullUserId) {
                  if (create) {
                      comment.setAction(Event.ACTION_ADD_USER_LINK);
                  } else {
                      comment.setAction(Event.ACTION_DELETE_USER_LINK);
                  }
                  comment.setMessage(new String[] { userId, type });
              } else {
                  if (create) {
                      comment.setAction(Event.ACTION_ADD_GROUP_LINK);
                  } else {
                      comment.setAction(Event.ACTION_DELETE_GROUP_LINK);
                  }
                  comment.setMessage(new String[] { groupId, type });
              }
              getCommentEntityManager().insert(comment);
          }
      }
  
      /*
       * (non-Javadoc)
       *
       * @see org.activiti.engine.impl.history.HistoryManagerInterface# createAttachmentComment(java.lang.String, java.lang.String, java.lang.String, boolean)
       */
      @Override
      public void createAttachmentComment(
          String taskId,
          String processInstanceId,
          String attachmentName,
          boolean create
      ) {
          if (isHistoryEnabled()) {
              String userId = Authentication.getAuthenticatedUserId();
              CommentEntity comment = getCommentEntityManager().create();
              comment.setUserId(userId);
              comment.setType(CommentEntity.TYPE_EVENT);
              comment.setTime(getClock().getCurrentTime());
              comment.setTaskId(taskId);
              comment.setProcessInstanceId(processInstanceId);
              if (create) {
                  comment.setAction(Event.ACTION_ADD_ATTACHMENT);
              } else {
                  comment.setAction(Event.ACTION_DELETE_ATTACHMENT);
              }
              comment.setMessage(attachmentName);
              getCommentEntityManager().insert(comment);
          }
      }
  
      // Identity link related history
      /*
       * (non-Javadoc)
       *
       * @see org.activiti.engine.impl.history.HistoryManagerInterface# recordIdentityLinkCreated (org.activiti.engine.impl.persistence.entity.IdentityLinkEntity)
       */
      @Override
      public void recordIdentityLinkCreated(IdentityLinkEntity identityLink) {
          // It makes no sense storing historic counterpart for an identity-link
          // that is related
          // to a process-definition only as this is never kept in history
          if (
              isHistoryLevelAtLeast(HistoryLevel.AUDIT) &&
              (identityLink.getProcessInstanceId() != null || identityLink.getTaskId() != null)
          ) {
              HistoricIdentityLinkEntity historicIdentityLinkEntity = getHistoricIdentityLinkEntityManager().create();
              historicIdentityLinkEntity.setId(identityLink.getId());
              historicIdentityLinkEntity.setGroupId(identityLink.getGroupId());
              historicIdentityLinkEntity.setProcessInstanceId(identityLink.getProcessInstanceId());
              historicIdentityLinkEntity.setTaskId(identityLink.getTaskId());
              historicIdentityLinkEntity.setType(identityLink.getType());
              historicIdentityLinkEntity.setUserId(identityLink.getUserId());
              historicIdentityLinkEntity.setDetails(identityLink.getDetails());
              getHistoricIdentityLinkEntityManager().insert(historicIdentityLinkEntity, false);
          }
      }
  
      /*
       * (non-Javadoc)
       *
       * @see org.activiti.engine.impl.history.HistoryManagerInterface# deleteHistoricIdentityLink(java.lang.String)
       */
      @Override
      public void deleteHistoricIdentityLink(String id) {
          if (isHistoryLevelAtLeast(HistoryLevel.AUDIT)) {
              getHistoricIdentityLinkEntityManager().delete(id);
          }
      }
  
      /*
       * (non-Javadoc)
       *
       * @see org.activiti.engine.impl.history.HistoryManagerInterface# updateProcessBusinessKeyInHistory (org.activiti.engine.impl.persistence.entity.ExecutionEntity)
       */
      @Override
      public void updateProcessBusinessKeyInHistory(ExecutionEntity processInstance) {
          if (isHistoryEnabled()) {
              if (log.isDebugEnabled()) {
                  log.debug("updateProcessBusinessKeyInHistory : {}", processInstance.getId());
              }
              if (processInstance != null) {
                  HistoricProcessInstanceEntity historicProcessInstance =
                      getHistoricProcessInstanceEntityManager().findById(processInstance.getId());
                  if (historicProcessInstance != null) {
                      historicProcessInstance.setBusinessKey(processInstance.getProcessInstanceBusinessKey());
                      getHistoricProcessInstanceEntityManager().update(historicProcessInstance, false);
                  }
              }
          }
      }
  
      @Override
      public void recordVariableRemoved(VariableInstanceEntity variable) {
          if (isHistoryLevelAtLeast(HistoryLevel.ACTIVITY)) {
              HistoricVariableInstanceEntity historicProcessVariable = getEntityCache().findInCache(
                  HistoricVariableInstanceEntity.class,
                  variable.getId()
              );
              if (historicProcessVariable == null) {
                  historicProcessVariable =
                      getHistoricVariableInstanceEntityManager().findHistoricVariableInstanceByVariableInstanceId(
                          variable.getId()
                      );
              }
  
              if (historicProcessVariable != null) {
                  getHistoricVariableInstanceEntityManager().delete(historicProcessVariable);
              }
          }
      }
  
      protected String parseActivityType(FlowElement element) {
          String elementType = element.getClass().getSimpleName();
          elementType = elementType.substring(0, 1).toLowerCase() + elementType.substring(1);
          return elementType;
      }
  
      protected EntityCache getEntityCache() {
          return getSession(EntityCache.class);
      }
  
      public HistoryLevel getHistoryLevel() {
          return historyLevel;
      }
  
      public void setHistoryLevel(HistoryLevel historyLevel) {
          this.historyLevel = historyLevel;
      }
  }
  