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.runtime.api.impl;
  
  import java.util.ArrayList;
  import java.util.List;
  import java.util.Objects;
  import org.activiti.api.model.shared.model.VariableInstance;
  import org.activiti.api.runtime.shared.NotFoundException;
  import org.activiti.api.runtime.shared.query.Page;
  import org.activiti.api.runtime.shared.query.Pageable;
  import org.activiti.api.runtime.shared.security.SecurityManager;
  import org.activiti.api.task.model.Task;
  import org.activiti.api.task.model.builders.TaskPayloadBuilder;
  import org.activiti.api.task.model.impl.TaskImpl;
  import org.activiti.api.task.model.payloads.AssignTaskPayload;
  import org.activiti.api.task.model.payloads.CandidateGroupsPayload;
  import org.activiti.api.task.model.payloads.CandidateUsersPayload;
  import org.activiti.api.task.model.payloads.ClaimTaskPayload;
  import org.activiti.api.task.model.payloads.CompleteTaskPayload;
  import org.activiti.api.task.model.payloads.CreateTaskPayload;
  import org.activiti.api.task.model.payloads.CreateTaskVariablePayload;
  import org.activiti.api.task.model.payloads.DeleteTaskPayload;
  import org.activiti.api.task.model.payloads.GetTaskVariablesPayload;
  import org.activiti.api.task.model.payloads.GetTasksPayload;
  import org.activiti.api.task.model.payloads.ReleaseTaskPayload;
  import org.activiti.api.task.model.payloads.SaveTaskPayload;
  import org.activiti.api.task.model.payloads.UpdateTaskPayload;
  import org.activiti.api.task.model.payloads.UpdateTaskVariablePayload;
  import org.activiti.api.task.runtime.TaskRuntime;
  import org.activiti.api.task.runtime.conf.TaskRuntimeConfiguration;
  import org.activiti.engine.TaskService;
  import org.activiti.engine.task.IdentityLink;
  import org.activiti.engine.task.IdentityLinkType;
  import org.activiti.engine.task.TaskQuery;
  import org.activiti.runtime.api.model.impl.APITaskConverter;
  import org.activiti.runtime.api.model.impl.APIVariableInstanceConverter;
  import org.activiti.runtime.api.query.impl.PageImpl;
  import org.springframework.security.access.prepost.PreAuthorize;
  
  @PreAuthorize("hasRole('ACTIVITI_USER')")
  public class TaskRuntimeImpl implements TaskRuntime {
  
      private final TaskService taskService;
  
      private final APITaskConverter taskConverter;
  
      private final APIVariableInstanceConverter variableInstanceConverter;
  
      private final TaskRuntimeConfiguration configuration;
  
      private final SecurityManager securityManager;
  
      private final TaskRuntimeHelper taskRuntimeHelper;
  
      public TaskRuntimeImpl(
          TaskService taskService,
          SecurityManager securityManager,
          APITaskConverter taskConverter,
          APIVariableInstanceConverter variableInstanceConverter,
          TaskRuntimeConfiguration configuration,
          TaskRuntimeHelper taskRuntimeHelper
      ) {
          this.taskService = taskService;
          this.securityManager = securityManager;
          this.taskConverter = taskConverter;
          this.variableInstanceConverter = variableInstanceConverter;
          this.configuration = configuration;
          this.taskRuntimeHelper = taskRuntimeHelper;
      }
  
      @Override
      public TaskRuntimeConfiguration configuration() {
          return configuration;
      }
  
      @Override
      public Task task(String taskId) {
          return taskConverter.fromWithCandidates(taskRuntimeHelper.getInternalTaskWithChecks(taskId));
      }
  
      private Task reassignedTask(String taskId) {
          return taskConverter.fromWithCandidates(taskRuntimeHelper.getInternalTask(taskId));
      }
  
      @Override
      public Page<Task> tasks(Pageable pageable) {
          String authenticatedUserId = securityManager.getAuthenticatedUserId();
          if (authenticatedUserId != null && !authenticatedUserId.isEmpty()) {
              List<String> userGroups = securityManager.getAuthenticatedUserGroups();
              return tasks(
                  pageable,
                  TaskPayloadBuilder.tasks().withAssignee(authenticatedUserId).withGroups(userGroups).build()
              );
          }
          throw new IllegalStateException("You need an authenticated user to perform a task query");
      }
  
      @Override
      public Page<Task> tasks(Pageable pageable, GetTasksPayload getTasksPayload) {
          TaskQuery taskQuery = taskService.createTaskQuery();
          if (getTasksPayload == null) {
              getTasksPayload = TaskPayloadBuilder.tasks().build();
          }
          String authenticatedUserId = securityManager.getAuthenticatedUserId();
          if (authenticatedUserId != null && !authenticatedUserId.isEmpty()) {
              List<String> userGroups = securityManager.getAuthenticatedUserGroups();
              getTasksPayload.setAssigneeId(authenticatedUserId);
              getTasksPayload.setGroups(userGroups);
          } else {
              throw new IllegalStateException("You need an authenticated user to perform a task query");
          }
          taskQuery = taskQuery
              .or()
              .taskCandidateOrAssigned(getTasksPayload.getAssigneeId(), getTasksPayload.getGroups())
              .taskOwner(authenticatedUserId)
              .endOr();
  
          if (getTasksPayload.getProcessInstanceId() != null) {
              taskQuery = taskQuery.processInstanceId(getTasksPayload.getProcessInstanceId());
          }
          if (getTasksPayload.getParentTaskId() != null) {
              taskQuery = taskQuery.taskParentTaskId(getTasksPayload.getParentTaskId());
          }
          List<Task> tasks = taskConverter.from(taskQuery.listPage(pageable.getStartIndex(), pageable.getMaxItems()));
          return new PageImpl<>(tasks, Math.toIntExact(taskQuery.count()));
      }
  
      @Override
      public Task complete(CompleteTaskPayload completeTaskPayload) {
          //@TODO: not the most efficient way to return the just completed task, improve
          //      we might need to create an empty shell with the task ID and Status only
          Task task;
          String authenticatedUserId = securityManager.getAuthenticatedUserId();
          try {
              task = task(completeTaskPayload.getTaskId());
          } catch (IllegalStateException ex) {
              throw new IllegalStateException(
                  "The authenticated user cannot complete task" +
                  completeTaskPayload.getTaskId() +
                  " due he/she cannot access to the task"
              );
          }
          // validate the task does have an assignee
          if (task.getAssignee() == null || task.getAssignee().isEmpty()) {
              throw new IllegalStateException("The task needs to be claimed before trying to complete it");
          }
          if (!task.getAssignee().equals(authenticatedUserId)) {
              throw new IllegalStateException("You cannot complete the task if you are not assigned to it");
          }
  
          taskRuntimeHelper.handleCompleteTaskPayload(completeTaskPayload);
  
          taskService.complete(completeTaskPayload.getTaskId(), completeTaskPayload.getVariables(), true);
  
          ((TaskImpl) task).setCompletedBy(authenticatedUserId);
          ((TaskImpl) task).setStatus(Task.TaskStatus.COMPLETED);
  
          return task;
      }
  
      @Override
      public Task claim(ClaimTaskPayload claimTaskPayload) {
          // Validate that the task is visible by the currently authorized user
          Task task;
          try {
              task = task(claimTaskPayload.getTaskId());
          } catch (IllegalStateException ex) {
              throw new IllegalStateException(
                  "The authenticated user cannot claim task" +
                  claimTaskPayload.getTaskId() +
                  " due it is not a candidate for it"
              );
          }
          // validate the task doesn't have an assignee
          if (task.getAssignee() != null && !task.getAssignee().isEmpty()) {
              throw new IllegalStateException(
                  "The task was already claimed, the assignee of this task needs to release it first for you to claim it"
              );
          }
  
          String authenticatedUserId = securityManager.getAuthenticatedUserId();
          claimTaskPayload.setAssignee(authenticatedUserId);
          taskService.claim(claimTaskPayload.getTaskId(), claimTaskPayload.getAssignee());
  
          return task(claimTaskPayload.getTaskId());
      }
  
      @Override
      public Task release(ReleaseTaskPayload releaseTaskPayload) {
          String taskId = releaseTaskPayload.getTaskId();
          releaseTask(taskId);
          return task(taskId);
      }
  
      @Override
      public Task update(UpdateTaskPayload updateTaskPayload) {
          return taskRuntimeHelper.applyUpdateTaskPayload(false, updateTaskPayload);
      }
  
      @Override
      public Task delete(DeleteTaskPayload deleteTaskPayload) {
          //@TODO: not the most efficient way to return the just deleted task, improve
          //      we might need to create an empty shell with the task ID and Status only
          Task task;
          try {
              task = task(deleteTaskPayload.getTaskId());
          } catch (IllegalStateException ex) {
              throw new IllegalStateException(
                  "The authenticated user cannot delete the task" +
                  deleteTaskPayload.getTaskId() +
                  " due it is not the current assignee"
              );
          }
          String authenticatedUserId = securityManager.getAuthenticatedUserId();
          // validate that you are trying to delete task where you are the assignee or the owner
          if (
              (task.getAssignee() == null ||
                  task.getAssignee().isEmpty() ||
                  !task.getAssignee().equals(authenticatedUserId)) &&
              (task.getOwner() == null || task.getOwner().isEmpty() || !task.getOwner().equals(authenticatedUserId))
          ) {
              throw new IllegalStateException("You cannot delete a task where you are not the assignee/owner");
          }
          TaskImpl deletedTaskData = new TaskImpl(task.getId(), task.getName(), Task.TaskStatus.CANCELLED);
          if (!deleteTaskPayload.hasReason()) {
              deleteTaskPayload.setReason("Task deleted by " + authenticatedUserId);
          }
          taskService.deleteTask(deleteTaskPayload.getTaskId(), deleteTaskPayload.getReason(), true);
          return deletedTaskData;
      }
  
      @Override
      public Task create(CreateTaskPayload createTaskPayload) {
          if (createTaskPayload.getName() == null || createTaskPayload.getName().isEmpty()) {
              throw new IllegalStateException("You cannot create a task without name");
          }
  
          org.activiti.engine.task.Task task = taskService.newTask();
          task.setName(createTaskPayload.getName());
          task.setDescription(createTaskPayload.getDescription());
          task.setDueDate(createTaskPayload.getDueDate());
          task.setPriority(createTaskPayload.getPriority());
          if (createTaskPayload.getAssignee() != null && !createTaskPayload.getAssignee().isEmpty()) {
              task.setAssignee(createTaskPayload.getAssignee());
          }
          task.setParentTaskId(createTaskPayload.getParentTaskId());
          task.setFormKey(createTaskPayload.getFormKey());
          task.setOwner(securityManager.getAuthenticatedUserId());
          taskService.saveTask(task);
          if (createTaskPayload.getCandidateGroups() != null && !createTaskPayload.getCandidateGroups().isEmpty()) {
              for (String g : createTaskPayload.getCandidateGroups()) {
                  taskService.addCandidateGroup(task.getId(), g);
              }
          }
  
          if (createTaskPayload.getCandidateUsers() != null && !createTaskPayload.getCandidateUsers().isEmpty()) {
              for (String u : createTaskPayload.getCandidateUsers()) {
                  taskService.addCandidateUser(task.getId(), u);
              }
          }
  
          return taskConverter.from(task);
      }
  
      @Override
      public void addCandidateUsers(CandidateUsersPayload candidateUsersPayload) {
          org.activiti.engine.task.Task internalTask;
          try {
              internalTask = taskRuntimeHelper.getInternalTaskWithChecks(candidateUsersPayload.getTaskId());
          } catch (IllegalStateException ex) {
              throw new IllegalStateException(
                  "The authenticated user cannot update the task" +
                  candidateUsersPayload.getTaskId() +
                  " due it is not the current assignee"
              );
          }
  
          String authenticatedUserId = securityManager.getAuthenticatedUserId();
  
          // validate that you are trying to add CandidateUsers to the task where you are the assignee
          if (!Objects.equals(internalTask.getAssignee(), authenticatedUserId)) {
              throw new IllegalStateException("You cannot update a task where you are not the assignee");
          }
  
          if (candidateUsersPayload.getCandidateUsers() != null && !candidateUsersPayload.getCandidateUsers().isEmpty()) {
              for (String u : candidateUsersPayload.getCandidateUsers()) {
                  taskService.addCandidateUser(internalTask.getId(), u);
              }
          }
      }
  
      @Override
      public void deleteCandidateUsers(CandidateUsersPayload candidateUsersPayload) {
          org.activiti.engine.task.Task internalTask;
          try {
              internalTask = taskRuntimeHelper.getInternalTaskWithChecks(candidateUsersPayload.getTaskId());
          } catch (IllegalStateException ex) {
              throw new IllegalStateException(
                  "The authenticated user cannot update the task" +
                  candidateUsersPayload.getTaskId() +
                  " due it is not the current assignee"
              );
          }
  
          String authenticatedUserId = securityManager.getAuthenticatedUserId();
  
          // validate that you are trying to add CandidateUsers to the task where you are the assignee
          if (!Objects.equals(internalTask.getAssignee(), authenticatedUserId)) {
              throw new IllegalStateException("You cannot update a task where you are not the assignee");
          }
  
          if (candidateUsersPayload.getCandidateUsers() != null && !candidateUsersPayload.getCandidateUsers().isEmpty()) {
              for (String u : candidateUsersPayload.getCandidateUsers()) {
                  taskService.deleteCandidateUser(internalTask.getId(), u);
              }
          }
      }
  
      @Override
      public void addCandidateGroups(CandidateGroupsPayload candidateGroupsPayload) {
          org.activiti.engine.task.Task internalTask;
          try {
              internalTask = taskRuntimeHelper.getInternalTaskWithChecks(candidateGroupsPayload.getTaskId());
          } catch (IllegalStateException ex) {
              throw new IllegalStateException(
                  "The authenticated user cannot update the task" +
                  candidateGroupsPayload.getTaskId() +
                  " due it is not the current assignee"
              );
          }
  
          String authenticatedUserId = securityManager.getAuthenticatedUserId();
          // validate that you are trying to add CandidateGroups to the task where you are the assignee
          if (!Objects.equals(internalTask.getAssignee(), authenticatedUserId)) {
              throw new IllegalStateException("You cannot update a task where you are not the assignee");
          }
  
          if (
              candidateGroupsPayload.getCandidateGroups() != null &&
              !candidateGroupsPayload.getCandidateGroups().isEmpty()
          ) {
              for (String g : candidateGroupsPayload.getCandidateGroups()) {
                  taskService.addCandidateGroup(internalTask.getId(), g);
              }
          }
      }
  
      @Override
      public void deleteCandidateGroups(CandidateGroupsPayload candidateGroupsPayload) {
          org.activiti.engine.task.Task internalTask;
          try {
              internalTask = taskRuntimeHelper.getInternalTaskWithChecks(candidateGroupsPayload.getTaskId());
          } catch (IllegalStateException ex) {
              throw new IllegalStateException(
                  "The authenticated user cannot update the task" +
                  candidateGroupsPayload.getTaskId() +
                  " due it is not the current assignee"
              );
          }
  
          String authenticatedUserId = securityManager.getAuthenticatedUserId();
          // validate that you are trying to add CandidateGroups to the task where you are the assignee
          if (!Objects.equals(internalTask.getAssignee(), authenticatedUserId)) {
              throw new IllegalStateException("You cannot update a task where you are not the assignee");
          }
  
          if (
              candidateGroupsPayload.getCandidateGroups() != null &&
              !candidateGroupsPayload.getCandidateGroups().isEmpty()
          ) {
              for (String g : candidateGroupsPayload.getCandidateGroups()) {
                  taskService.deleteCandidateGroup(internalTask.getId(), g);
              }
          }
      }
  
      @Override
      public List<String> userCandidates(String taskId) {
          List<IdentityLink> identityLinks = getIdentityLinks(taskId);
          List<String> userCandidates = new ArrayList<>();
          if (identityLinks != null) {
              for (IdentityLink i : identityLinks) {
                  if (i.getUserId() != null) {
                      if (i.getType().equals(IdentityLinkType.CANDIDATE)) {
                          userCandidates.add(i.getUserId());
                      }
                  }
              }
          }
          return userCandidates;
      }
  
      @Override
      public List<String> groupCandidates(String taskId) {
          List<IdentityLink> identityLinks = getIdentityLinks(taskId);
          List<String> groupCandidates = new ArrayList<>();
          if (identityLinks != null) {
              for (IdentityLink i : identityLinks) {
                  if (i.getGroupId() != null) {
                      if (i.getType().equals(IdentityLinkType.CANDIDATE)) {
                          groupCandidates.add(i.getGroupId());
                      }
                  }
              }
          }
          return groupCandidates;
      }
  
      @Override
      public List<VariableInstance> variables(GetTaskVariablesPayload getTaskVariablesPayload) {
          taskRuntimeHelper.assertHasAccessToTask(getTaskVariablesPayload.getTaskId());
          return variableInstanceConverter.from(
              taskRuntimeHelper.getInternalTaskVariables(getTaskVariablesPayload.getTaskId()).values()
          );
      }
  
      @Override
      public void createVariable(CreateTaskVariablePayload createTaskVariablePayload) {
          taskRuntimeHelper.createVariable(false, createTaskVariablePayload);
      }
  
      @Override
      public void updateVariable(UpdateTaskVariablePayload updateTaskVariablePayload) {
          taskRuntimeHelper.updateVariable(false, updateTaskVariablePayload);
      }
  
      @Override
      public void save(SaveTaskPayload saveTaskPayload) {
          taskRuntimeHelper.assertHasAccessToTask(saveTaskPayload.getTaskId());
  
          taskRuntimeHelper.handleSaveTaskPayload(saveTaskPayload);
  
          taskService.setVariablesLocal(saveTaskPayload.getTaskId(), saveTaskPayload.getVariables());
      }
  
      @Override
      public Task assign(AssignTaskPayload assignTaskPayload) {
          String assignee = assignTaskPayload.getAssignee();
          String taskId = assignTaskPayload.getTaskId();
  
          assertAssigneeIsACandidateUser(taskId, assignee);
          reassignTask(taskId, assignee);
          return reassignedTask(taskId);
      }
  
      private List<IdentityLink> getIdentityLinks(String taskId) {
          String authenticatedUserId = securityManager.getAuthenticatedUserId();
          if (authenticatedUserId != null && !authenticatedUserId.isEmpty()) {
              List<String> userRoles = securityManager.getAuthenticatedUserRoles();
              List<String> userGroups = securityManager.getAuthenticatedUserGroups();
              org.activiti.engine.task.Task internalTask = taskService
                  .createTaskQuery()
                  .taskCandidateOrAssigned(authenticatedUserId, userGroups)
                  .taskId(taskId)
                  .singleResult();
              if (internalTask == null) {
                  throw new NotFoundException(
                      "Unable to find task for the given id: " +
                      taskId +
                      " for user: " +
                      authenticatedUserId +
                      " (with groups: " +
                      userGroups +
                      " & with roles: " +
                      userRoles +
                      ")"
                  );
              }
              return taskService.getIdentityLinksForTask(taskId);
          }
          throw new IllegalStateException("There is no authenticated user, we need a user authenticated to find tasks");
      }
  
      private void assertAssigneeIsACandidateUser(String taskId, String assignee) {
          List<String> userCandidates = userCandidates(taskId);
          if (!userCandidates.contains(assignee)) {
              throw new IllegalStateException(
                  "You cannot assign a task to " + assignee + " due it is not a candidate for it"
              );
          }
      }
  
      private void reassignTask(String taskId, String assignee) {
          releaseTask(taskId);
          taskService.claim(taskId, assignee);
      }
  
      private void releaseTask(String taskId) {
          assertCanReleaseTask(taskId);
          taskService.unclaim(taskId);
      }
  
      private void assertCanReleaseTask(String taskId) {
          Task task = task(taskId);
  
          if (task.getAssignee() == null || task.getAssignee().isEmpty()) {
              throw new IllegalStateException("You cannot release a task that is not claimed");
          }
          String authenticatedUserId = securityManager.getAuthenticatedUserId();
          if (!task.getAssignee().equals(authenticatedUserId)) {
              throw new IllegalStateException("You cannot release a task where you are not the assignee");
          }
      }
  }
  