<commit_before>/*
 * Copyright (C) 2000 - 2021 Silverpeas
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * As a special exception to the terms and conditions of version 3.0 of
 * the GPL, you may redistribute this Program in connection with Free/Libre
 * Open Source Software ("FLOSS") applications as described in Silverpeas's
 * FLOSS exception. You should have recieved a copy of the text describing
 * the FLOSS exception, and it is also available here:
 * "http://www.silverpeas.org/docs/core/legal/floss_exception.html"
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */
package org.silverpeas.processmanager;

import org.silverpeas.core.SilverpeasRuntimeException;
import org.silverpeas.core.admin.user.model.Group;
import org.silverpeas.core.admin.user.model.UserDetail;
import org.silverpeas.core.contribution.content.form.DataRecord;
import org.silverpeas.core.contribution.content.form.DataRecordUtil;
import org.silverpeas.core.contribution.content.form.Field;
import org.silverpeas.core.contribution.content.form.FieldTemplate;
import org.silverpeas.core.contribution.content.form.Form;
import org.silverpeas.core.contribution.content.form.FormException;
import org.silverpeas.core.contribution.content.form.PagesContext;
import org.silverpeas.core.contribution.content.form.RecordTemplate;
import org.silverpeas.core.contribution.content.form.field.DateField;
import org.silverpeas.core.contribution.content.form.field.MultipleUserField;
import org.silverpeas.core.contribution.content.form.field.UserField;
import org.silverpeas.core.contribution.content.form.form.XmlForm;
import org.silverpeas.core.contribution.content.form.record.GenericFieldTemplate;
import org.silverpeas.core.contribution.content.form.record.GenericRecordTemplate;
import org.silverpeas.core.notification.message.MessageNotifier;
import org.silverpeas.core.util.DateUtil;
import org.silverpeas.core.util.MapUtil;
import org.silverpeas.core.util.Mutable;
import org.silverpeas.core.util.StringUtil;
import org.silverpeas.core.util.csv.CSVRow;
import org.silverpeas.core.util.logging.SilverLogger;
import org.silverpeas.core.web.export.ExportCSVBuilder;
import org.silverpeas.core.web.mvc.controller.AbstractComponentSessionController;
import org.silverpeas.core.web.mvc.controller.ComponentContext;
import org.silverpeas.core.web.mvc.controller.MainSessionController;
import org.silverpeas.core.web.mvc.webcomponent.WebMessager;
import org.silverpeas.core.workflow.api.UpdatableProcessInstanceManager;
import org.silverpeas.core.workflow.api.Workflow;
import org.silverpeas.core.workflow.api.WorkflowEngine;
import org.silverpeas.core.workflow.api.WorkflowException;
import org.silverpeas.core.workflow.api.error.WorkflowError;
import org.silverpeas.core.workflow.api.event.GenericEvent;
import org.silverpeas.core.workflow.api.event.QuestionEvent;
import org.silverpeas.core.workflow.api.event.ResponseEvent;
import org.silverpeas.core.workflow.api.event.TaskDoneEvent;
import org.silverpeas.core.workflow.api.event.TaskSavedEvent;
import org.silverpeas.core.workflow.api.instance.Actor;
import org.silverpeas.core.workflow.api.instance.HistoryStep;
import org.silverpeas.core.workflow.api.instance.ProcessInstance;
import org.silverpeas.core.workflow.api.instance.Question;
import org.silverpeas.core.workflow.api.instance.UpdatableProcessInstance;
import org.silverpeas.core.workflow.api.model.*;
import org.silverpeas.core.workflow.api.task.Task;
import org.silverpeas.core.workflow.api.user.Replacement;
import org.silverpeas.core.workflow.api.user.ReplacementList;
import org.silverpeas.core.workflow.api.user.User;
import org.silverpeas.core.workflow.api.user.UserInfo;
import org.silverpeas.core.workflow.api.user.UserSettings;
import org.silverpeas.core.workflow.engine.WorkflowHub;
import org.silverpeas.core.workflow.engine.datarecord.ProcessInstanceRowRecord;
import org.silverpeas.core.workflow.engine.instance.LockingUser;
import org.silverpeas.core.workflow.engine.model.ActionRefs;
import org.silverpeas.core.workflow.engine.model.ItemImpl;
import org.silverpeas.core.workflow.engine.user.UserSettingsService;
import org.silverpeas.processmanager.record.QuestionRecord;
import org.silverpeas.processmanager.record.QuestionTemplate;

import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import java.time.LocalDate;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.util.Collections.emptyList;
import static java.util.Optional.ofNullable;
import static org.silverpeas.core.cache.service.CacheServiceProvider.getRequestCacheService;
import static org.silverpeas.core.contribution.attachment.AttachmentService.VERSION_MODE;
import static org.silverpeas.core.util.CollectionUtil.asList;
import static org.silverpeas.core.util.StringUtil.isDefined;
import static org.silverpeas.core.workflow.util.WorkflowUtil.getItemByName;

/**
 * The ProcessManager Session controller
 */
public class ProcessManagerSessionController extends AbstractComponentSessionController {

  private static final String PROCESS_MANAGER_SESSION_CONTROLLER =
      "ProcessManagerSessionController";
  private static final String SUPERVISOR_ROLE = "supervisor";
  private static final String QUESTION_ACTION = "#question#";
  private static final String RESPONSE_ACTION = "#response#";
  private static final String RE_ASSIGN_ACTION = "#reAssign#";

  /**
   * Builds and init a new session controller
   * @param mainSessionCtrl
   * @param context
   * @throws ProcessManagerException
   */
  public ProcessManagerSessionController(MainSessionController mainSessionCtrl,
      ComponentContext context) throws ProcessManagerException {
    super(mainSessionCtrl, context, "org.silverpeas.processManager.multilang.processManagerBundle",
        "org.silverpeas.processManager.settings.processManagerIcons",
        "org.silverpeas.processManager.settings.processManagerSettings");
    // the peasId is the current component id.
    peasId = context.getCurrentComponentId();
    processModel = getProcessModel(peasId);


    // the current user is given by the main session controller.
    currentUser = getUser(mainSessionCtrl.getUserId());
    // the user roles are given by the context.
    userRoles = context.getCurrentProfile();
    if (userRoles == null || userRoles.length == 0) {
      throw new ProcessManagerException(PROCESS_MANAGER_SESSION_CONTROLLER,
          "processManager.UNAUTHORIZED_USER");
    }
    currentRole = userRoles[0];

    // Reset the user rights for creation
    resetCreationRights();

    // Load user information
    userSettings = UserSettingsService.get().get(mainSessionCtrl.getUserId(), peasId);
  }

  /**
   * Builds a ill session controller. Initialization is skipped and this session controller can
   * only * display the fatal exception. Used by the request router when a full session controller
   * can't be built.
   * @param mainSessionCtrl
   * @param context
   * @param fatal
   */
  public ProcessManagerSessionController(MainSessionController mainSessionCtrl,
      ComponentContext context, ProcessManagerException fatal) {
    super(mainSessionCtrl, context,
        "org.silverpeas.processManager.multilang.processManagerBundle",
        "org.silverpeas.processManager.settings.processManagerIcons",
        "org.silverpeas.processManager.settings.processManagerSettings");
    fatalException = fatal;
  }

  /**
   * Get the creation rights
   * @return true if user can do the "Creation" action
   */
  public boolean getCreationRights() {
    return creationRights;
  }

  /**
   * Compute the creation rights set creationRight to true if user can do the "Creation" action
   */
  public final void resetCreationRights() {
    creationRights = false;

    try {
      String[] roles = processModel.getCreationRoles();

      for (String role : roles) {
        if (role.equals(currentRole)) {
          creationRights = true;
        }
      }
    } catch (Exception e) {
      creationRights = false;
    }
  }

  /**
   * L'historique peut être filtré. Dans ce cas, les formulaires associés à chaque état sont
   * visibles uniquement si l'utilisateur courant était un working user ou un interested user.
   */
  public boolean filterHistory() {
    String parameterValue = getComponentParameterValue("filterHistory");
    return StringUtil.getBooleanValue(parameterValue);
  }

  public boolean isHistoryTabVisible() {
    String parameterValue = this.getComponentParameterValue("historyTabEnable");
    if (isDefined(parameterValue)) {
      return "yes".equalsIgnoreCase(parameterValue);
    }
    return true;
  }

  public boolean isCSVExportEnabled() {
    return "yes".equalsIgnoreCase(getComponentParameterValue("exportCSV"));
  }

  /**
   * Print button on an action can be disabled. So it's return the visibility status of that
   * button.
   * @return true is print button is visible
   */
  public boolean isPrintButtonEnabled() {
    return StringUtil.getBooleanValue(getComponentParameterValue("printButtonEnabled"));
  }

  /**
   * Save button on an action can be disabled. So it's return the visibility status of that button.
   * @return true is save button is visible
   */
  public boolean isSaveButtonEnabled() {
    return StringUtil.getBooleanValue(getComponentParameterValue("saveButtonEnabled"));
  }

  /**
   * Returns the last fatal exception
   */
  public ProcessManagerException getFatalException() {
    return fatalException;
  }

  /**
   * Set current Process instance to null
   */
  public void resetCurrentProcessInstance() {
    this.setResumingInstance(false);
    currentProcessInstance = null;
  }

  /**
   * Updates the current process instance from the given id and returns the associated
   * ProcessInstance object. If the instanceId parameter is null, updates nothing and returns the
   * current ProcessInstance. Throws ProcessManagerException when the instanceId is unkwown and
   * when
   * the current user is not allowed to access the instance. Doesn't change the current process
   * instance when an error occures.
   */
  public ProcessInstance resetCurrentProcessInstance(String instanceId)
      throws ProcessManagerException {
    this.setResumingInstance(false);
    if (instanceId != null) {
      ProcessInstance instance;
      try {
        instance = Workflow.getProcessInstanceManager().getProcessInstance(instanceId);
      } catch (WorkflowException e) {
        throw new ProcessManagerException(PROCESS_MANAGER_SESSION_CONTROLLER,
            "processManager.UNKNOWN_PROCESS_INSTANCE", instanceId, e);
      }
      currentProcessInstance = instance;

    }
    if (currentProcessInstance == null) {
      throw new ProcessManagerException(PROCESS_MANAGER_SESSION_CONTROLLER,
          "processManager.NO_CURRENT_PROCESS_INSTANCE");
    }
    return currentProcessInstance;
  }

  /**
   * Get the current process instance
   */
  public ProcessInstance getCurrentProcessInstance() {
    return currentProcessInstance;
  }

  /**
   * Returns the current instance list rows template.
   */
  public RecordTemplate getProcessListHeaders() {
    if (currentListHeaders == null) {
      resetCurrentProcessListHeaders();
    }
    return currentListHeaders;
  }

  /**
   * Reset the current instance list rows template.
   */
  public void resetCurrentProcessListHeaders() {
    currentListHeaders = processModel.getRowTemplate(currentRole, getLanguage());
  }

  /**
   * Returns the current process instance list.
   */
  public List<DataRecord> getCurrentProcessList() throws ProcessManagerException {
    if (currentProcessList == null) {
      return resetCurrentProcessList(false);
    } else {
      return currentProcessList;
    }
  }

  /**
   * Updates the current process instance list with current filter and returns this list. Doesn't
   * change the current process instance when an error occurs.
   */
  public List<DataRecord> resetCurrentProcessList(boolean doAPause) throws ProcessManagerException {
    // Wait to display processes list up-to-date
    if (doAPause) {
      doAPause();
    }

    try {
      User activeUser = getActiveUser();
      String[] groupIds = getOrganisationController().getAllGroupIdsOfUser(activeUser.getUserId());
      List<ProcessInstance> processList = Workflow.getProcessInstanceManager()
          .getProcessInstances(peasId, activeUser, currentRole, getUserRoles(), groupIds);
      currentProcessList = getCurrentFilter().filter(processList, currentRole, getLanguage());
    } catch (WorkflowException e) {
      throw new ProcessManagerException(PROCESS_MANAGER_SESSION_CONTROLLER,
          "processManager.GET_PROCESS_LIST_FAILED", peasId, e);
    }
    return currentProcessList;
  }

  /**
   * Get the role name of task referred by the todo with the given todo id
   */
  public String getRoleNameFromExternalTodoId(String externalTodoId)
      throws ProcessManagerException {
    try {
      return Workflow.getTaskManager().getRoleNameFromExternalTodoId(externalTodoId);
    } catch (WorkflowException e) {
      throw new ProcessManagerException(PROCESS_MANAGER_SESSION_CONTROLLER,
          "processManager.GET_ROLENAME_FROM_TODO_FAILED", "externalTodoId : " + externalTodoId, e);
    }
  }

  /**
   * Get the process instance Id referred by the todo with the given todo id
   * @param externalTodoId external todo identifier
   * @return the process instance Id referred by the todo with the given todo id.
   * @throws ProcessManagerException
   */
  public String getProcessInstanceIdFromExternalTodoId(String externalTodoId)
      throws ProcessManagerException {
    try {
      return Workflow.getTaskManager().getProcessInstanceIdFromExternalTodoId(externalTodoId);
    } catch (WorkflowException e) {
      throw new ProcessManagerException(PROCESS_MANAGER_SESSION_CONTROLLER,
          "processManager.GET_PROCESS_FROM_TODO_FAILED", "externalTodoId : " + externalTodoId, e);
    }
  }

  public boolean isUserAllowedOnActiveStates() {
    String[] states = currentProcessInstance.getActiveStates();
    if (states == null) {
      return false;
    }
    for (String state : states) {
      if (getActiveUsers(state).contains(getActiveUser().getUserId())) {
        return true;
      }
    }
    return false;
  }

  /**
   * Get the active states.
   * @return the active states.
   */
  public List<CurrentState> getActiveStates() throws ProcessManagerException {
    String[] stateNames = currentProcessInstance.getActiveStates();
    if (stateNames == null) {
      return emptyList();
    }

    Map<String, List<Task>> tasksByStates = getTasksByStates();

    List<CurrentState> currentStates = new ArrayList<>();
    for (String stateName : stateNames) {
      State state = getState(stateName);
      filterActions(state);
      CurrentState currentState = new CurrentState(state);
      currentState.setLabel(state.getLabel(currentRole, getLanguage()));
      currentState.setWorkingUsersAsString(getActiveRoles(state));

      List<Task> tasks = tasksByStates.get(stateName);
      currentState.setTasks(tasks);

      currentStates.add(currentState);
    }

    return currentStates;
  }

  private Map<String, List<Task>> getTasksByStates() throws ProcessManagerException {
    Map<String, List<Task>> result = new HashMap<>();
    Task[] tasks = getTasks();
    for (Task task : tasks) {
      State state = task.getState();
      MapUtil.putAddList(result, state.getName(), task);
    }
    return result;
  }

  private String getActiveRoles(State state) {

    QualifiedUsers workingUsers = state.getWorkingUsers();

    RelatedUser[] relatedUsers = workingUsers.getRelatedUsers();
    StringBuilder role = new StringBuilder();
    if (relatedUsers != null) {
      for (RelatedUser relatedUser : relatedUsers) {
        if (role.length() > 0) {
          role.append(", ");
        }
        // Process participants
        Participant participant = relatedUser.getParticipant();
        String relation = relatedUser.getRelation();
        if (participant != null && relation == null) {
          role.append(participant.getLabel(currentRole, getLanguage()));
        } else if (participant != null && relation != null) {
          // userSettings of requester (not current user)
          String requesterId = getCreatorIdOfCurrentProcessInstance();
          if (requesterId != null) {
            UserInfo userInfo = getSettingsOfUser(requesterId).getUserInfo(relation);
            if (userInfo != null) {
              role.append(getUserDetail(userInfo.getValue()).getDisplayedName());
            }
          }
        }

        // Process folder item
        Item item = relatedUser.getFolderItem();
        if (item != null) {
          try {
            Field field = currentProcessInstance.getField(item.getName());
            if (field instanceof UserField) {
              String userId = field.getStringValue();
              if (userId != null) {
                UserDetail user = getUserDetail(userId);
                if (user != null) {
                  role.append(user.getDisplayedName());
                }
              }
            } else if (field instanceof MultipleUserField) {
              MultipleUserField multipleUserField = (MultipleUserField) field;
              String[] userIds = multipleUserField.getUserIds();
              for (String userId : userIds) {
                if (userId != null) {
                  UserDetail user = getUserDetail(userId);
                  if (user != null) {
                    if (role.length() > 0) {
                      role.append(", ");
                    }
                    role.append(user.getDisplayedName());
                  }
                }
              }
            }
          } catch (Exception e) {
            SilverLogger.getLogger(this).warn(e);
          }
        }
      }
    }

    UserInRole[] userInRoles = workingUsers.getUserInRoles();
    if (userInRoles != null) {
      for (UserInRole userInRole : userInRoles) {
        if (role.length() > 0) {
          role.append(", ");
        }
        role.append(processModel.getRole(userInRole.getRoleName())
            .getLabel(currentRole, getLanguage()));
      }
    }

    RelatedGroup[] relatedGroups = workingUsers.getRelatedGroups();
    if (relatedGroups != null) {
      for (RelatedGroup relatedGroup : relatedGroups) {
        if (relatedGroup != null) {
          if (role.length() > 0) {
            role.append(", ");
          }

          // Process folder item
          Item item = relatedGroup.getFolderItem();
          try {
            if (item != null) {
              String groupId = currentProcessInstance.getField(item.getName()).getStringValue();
              if (groupId != null) {
                Group group = getOrganisationController().getGroup(groupId);
                if (group != null) {
                  role.append(group.getName());
                }
              }
            }
          } catch (Exception e) {
            SilverLogger.getLogger(this).warn(e);
          }
        }
      }
    }
    return role.toString();
  }

  /**
   * Get the active user names
   */
  private boolean isActiveUser() {
    if (currentProcessInstance == null) {
      return false;
    }
    final String[] states = currentProcessInstance.getActiveStates();
    return states != null && Stream.of(states)
        .flatMap(s -> {
          try {
            return Stream.of(currentProcessInstance.getWorkingUsers(s));
          } catch (WorkflowException ignored) {
            // ignore unknown state
          }
          return Stream.empty();
        })
        .anyMatch(u -> getActiveUser().getUserId().equals(u.getUser().getUserId()));
  }

  private List<String> getActiveUsers(String stateName) {
    List<String> activeUsers = new ArrayList<>();
    State state = getState(stateName);
    if (state != null) {
      activeUsers.addAll(getUsers(state.getWorkingUsers()));
      activeUsers.addAll(getUsers(state.getInterestedUsers()));
    }
    return activeUsers;
  }

  private List<String> getUsers(QualifiedUsers qualifiedUsers) {
    return getUsers(qualifiedUsers, false);
  }

  private List<String> getUsers(QualifiedUsers qualifiedUsers, boolean useCurrentRole) {
    List<String> users = new ArrayList<>();
    RelatedUser[] relatedUsers = qualifiedUsers.getRelatedUsers();
    RelatedUser relatedUser;
    List<String> roles = new ArrayList<>();
    for (final RelatedUser relatedUser1 : relatedUsers) {
      relatedUser = relatedUser1;
      // Process participants
      Participant participant = relatedUser.getParticipant();
      String relation = relatedUser.getRelation();
      if (participant != null && relation == null) {
        if (currentRole.equals(relatedUser.getRole())) {
          users.add(getActiveUser().getUserId());
        }
      } else if (participant != null && relation != null) {
        String requesterId = getCreatorIdOfCurrentProcessInstance();
        if (requesterId != null) {
          UserInfo userInfo = getSettingsOfUser(requesterId).getUserInfo(relation);
          if (userInfo != null) {
            users.add(userInfo.getValue());
          }
        }
      }

      // Process folder item
      Item item = relatedUser.getFolderItem();
      if (item != null) {
        try {
          Field field = currentProcessInstance.getField(item.getName());
          String role = relatedUser.getRole();
          if (field instanceof UserField) {
            if ((isDefined(role) && currentRole.equals(role)) ||
                StringUtil.isNotDefined(role)) {
              users.add(field.getStringValue());
            }
          } else if (field instanceof MultipleUserField) {
            MultipleUserField multipleUserField = (MultipleUserField) field;
            if ((isDefined(role) && currentRole.equals(role)) ||
                StringUtil.isNotDefined(role)) {
              users.addAll(Arrays.asList(multipleUserField.getUserIds()));
            }
          }
        } catch (WorkflowException we) {
          // ignore it.
        }
      }
    }

    UserInRole[] userInRoles = qualifiedUsers.getUserInRoles();
    for (final UserInRole userInRole : userInRoles) {
      roles.add(userInRole.getRoleName());
    }

    if (useCurrentRole) {
      if (roles.contains(currentRole)) {
        roles.clear();
        roles.add(currentRole);
      } else {
        roles.clear();
      }
    }

    String[] userIds = getOrganisationController().getUsersIdsByRoleNames(getComponentId(), roles);
    for (final String userId : userIds) {
      users.add(userId);
    }

    // Process related groups
    RelatedGroup[] relatedGroups = qualifiedUsers.getRelatedGroups();
    if (relatedGroups != null) {
      for (RelatedGroup relatedGroup : relatedGroups) {
        if (relatedGroup != null) {
          String role = relatedGroup.getRole();
          if (currentRole.equals(role) || StringUtil.isNotDefined(role)) {
            // Process folder item
            Item item = relatedGroup.getFolderItem();
            if (item != null) {
              try {
                String groupId = currentProcessInstance.getField(item.getName()).getStringValue();
                UserDetail[] usersOfGroup = getOrganisationController().getAllUsersOfGroup(groupId);
                for (UserDetail userOfGroup : usersOfGroup) {
                  users.add(userOfGroup.getId());
                }
              } catch (WorkflowException we) {// ignore it.
              }
            }
          }
        }
      }
    }

    return users;
  }

  /**
   * Returns the workflow user having the given id.
   * @param userId the user identifier
   * @return the workflow user having the given id.
   * @throws ProcessManagerException
   */
  public User getUser(String userId) throws ProcessManagerException {
    try {
      return Workflow.getUserManager().getUser(userId);
    } catch (WorkflowException e) {
      throw new ProcessManagerException("ProcessManagerSessionControler",
          "processManager.UNKNOWN_USER", userId, e);
    }
  }

  /**
   * Returns the process model having the given id.
   * @param modelId the model identifier
   * @return the process model having the given id.
   * @throws ProcessManagerException
   */
  public final ProcessModel getProcessModel(String modelId) throws ProcessManagerException {
    try {
      return Workflow.getProcessModelManager().getProcessModel(modelId);
    } catch (WorkflowException e) {
      throw new ProcessManagerException("ProcessManagerSessionControler",
          "processManager.UNKNOWN_PROCESS_MODEL", modelId, e);
    }
  }

  /**
   * @return the current role name.
   */
  public String getCurrentRole() {
    return currentRole;
  }

  /**
   * @return the current replacement if any.
   */
  public Replacement getCurrentReplacement() {
    return currentReplacement;
  }

  public void resetCurrentRoleAsSubstitute(String role, String incumbentId)
      throws ProcessManagerException {
    final Mutable<String> finalRole = Mutable.of(role);
    final Optional<Replacement> replacement = getCurrentUserReplacement(incumbentId, role);
    replacement.ifPresent(r -> finalRole.set(getRoleNameForSubstitute(r, role)));
    if (!replacement.isPresent()) {
      WebMessager.getInstance().addWarning(getMultilang()
          .getStringWithParams("processManager.replacements.errors.noMoreValid",
              getUser(incumbentId).getFullName()));
    }
    resetCurrentRole(finalRole.get());
  }

  /**
   * Returns the current role name.
   * @param role a role
   * @throws ProcessManagerException
   */
  public void resetCurrentRole(String role) throws ProcessManagerException {
    if (role != null && role.length() > 0) {
      final String[] roleCtx = role.split(":");
      if (roleCtx.length == 2) {
        Replacement.get(roleCtx[0]).ifPresent(r -> {
          this.currentReplacement = r;
          this.currentRole = roleCtx[1];
        });
      } else {
        this.currentReplacement = null;
        this.currentRole = role;
      }
    }
    resetCreationRights();
    resetProcessFilter();
    resetCurrentProcessList(false);
    resetCurrentProcessListHeaders();
  }

  /**
   * Returns the user roles as a list of (name, label) pair.
   */
  public NamedValue[] getUserRoleLabels() {
    return getRequestCacheService()
        .getCache()
        .computeIfAbsent("ProcessManagerSC.getUserRoleLabels" + getComponentId(), NamedValue[].class, () -> {
          final String lang = getLanguage();
          final Role[] roles = processModel.getRoles();
          final List<NamedValue> labels = new ArrayList<>();

          final List<Replacement> replacements = getCurrentUserReplacements();
          for (final Replacement replacement : replacements) {
            final List<String> incumbentRoles = getSubstituteRolesOf(replacement.getIncumbent());
            for (final String roleName : incumbentRoles) {
              getRoleLabel(replacement, roles, roleName, lang)
                  .filter(n -> labels.stream().noneMatch(l -> Objects.equals(n.getValue(), l.getValue())))
                  .ifPresent(labels::add);
            }
          }

          // quadratic search ! but it's ok : the list are about 3 or 4 length.
          for (final String userRole : userRoles) {
            getRoleLabel(null, roles, userRole, lang).ifPresent(labels::add);
          }

          labels.sort(NamedValue.ascendingValues);
          return labels.toArray(new NamedValue[0]);
        });
  }

  /**
   * Returns the component instance roles as a list of (name, label) pair.
   */
  public NamedValue[] getComponentInstanceRoleLabels() {
    String lang = getLanguage();
    Role[] roles = processModel.getRoles();
    List<NamedValue> labels = new ArrayList<>();
    for (final Role role : roles) {
      getRoleLabel(null, roles, role.getName(), lang).ifPresent(labels::add);
    }
    labels.sort(NamedValue.ascendingValues);
    return  labels.toArray(new NamedValue[0]);
  }

  /**
   * Returns the form presenting the folder of the current process instance.
   */
  public Form getPresentationForm() throws ProcessManagerException {
    try {
      Form form = getPresentationForm("presentationForm");
      if (form != null) {
        form.setViewForm(true);
        return form;
      }

      XmlForm xmlForm = new XmlForm(
          processModel.getDataFolder().toRecordTemplate(currentRole, getLanguage(), true));
      xmlForm.setTitle(getString("processManager.folder"));
      xmlForm.setViewForm(true);
      return xmlForm;
    } catch (FormException | WorkflowException e) {
      throw new ProcessManagerException("SessionController",
          "processManager.PRESENTATION_FORM_UNAVAILABLE", e);
    }
  }

  public Item[] getFolderItems() {
    return processModel.getDataFolder().getItems();
  }

  /**
   * Returns the folder data of the current process instance.
   */
  public DataRecord getFolderRecord() throws ProcessManagerException {
    DataRecord data = null;
    try {
      if (currentProcessInstance != null) {
        data = currentProcessInstance.getFormRecord("presentationForm", currentRole, getLanguage());

        if (data == null) {
          data = currentProcessInstance.getFolder();
        }
      }
    } catch (WorkflowException e) {
      throw new ProcessManagerException("SessionController",
          "processManager.PRESENTATION_DATA_UNAVAILABLE", e);
    }

    if (data == null) {
      throw new ProcessManagerException("SessionController",
          "processManager.PRESENTATION_DATA_UNAVAILABLE");
    }

    return data;
  }

  /**
   * Returns the creation task.
   */
  private Task getCreationTask() throws ProcessManagerException {
    try {
      return Workflow.getTaskManager().getCreationTask(getActiveUser(), currentRole, processModel);
    } catch (WorkflowException e) {
      throw new ProcessManagerException("SessionController",
          "processManager.CREATION_TASK_UNAVAILABLE", e);
    }
  }

  /**
   * Returns the form which starts a new instance.
   */
  public Form getCreationForm() throws ProcessManagerException {
    try {
      Action creation = processModel.getCreateAction(currentRole);
      return processModel.getPublicationForm(creation.getName(), currentRole, getLanguage());
    } catch (WorkflowException e) {
      throw new ProcessManagerException("SessionController", "processManager.NO_CREATION_FORM", e);
    }
  }

  /**
   * Returns the an empty creation record which will be filled with the creation form.
   */
  public DataRecord getEmptyCreationRecord() throws ProcessManagerException {
    try {
      Action creation = processModel.getCreateAction(currentRole);
      return processModel.getNewActionRecord(creation.getName(), currentRole, getLanguage(), null);
    } catch (WorkflowException e) {
      throw new ProcessManagerException("SessionController", "processManager.UNKNOWN_ACTION", e);
    }
  }

  /**
   * Returns the form to ask a question
   */
  public Form getQuestionForm(boolean readonly) throws ProcessManagerException {
    try {
      return new XmlForm(new QuestionTemplate(getLanguage(), readonly));
    } catch (FormException fe) {
      throw new ProcessManagerException(PROCESS_MANAGER_SESSION_CONTROLLER,
          "processManager.GET_QUESTION_FORM_FAILED", fe);
    }
  }

  /**
   * Returns the an empty question record which will be filled with the question form.
   */
  public DataRecord getEmptyQuestionRecord() {
    return new QuestionRecord("");
  }

  /**
   * get the question with the given id
   * @param questionId question id
   * @return the found question
   */
  public Question getQuestion(String questionId) {
    Question[] questions = currentProcessInstance.getQuestions();
    Question foundQuestion = null;

    for (int i = 0; foundQuestion == null && i < questions.length; i++) {
      if (questions[i].getId().equals(questionId)) {
        foundQuestion = questions[i];
      }
    }

    return foundQuestion;
  }

  /**
   * Returns the an empty question record which will be filled with the question form.
   */
  public DataRecord getQuestionRecord(String questionId) {
    Question question = getQuestion(questionId);
    return new QuestionRecord(question.getQuestionText());
  }

  /**
   * Get assign template (for the re-affectations)
   */
  private GenericRecordTemplate getAssignTemplate() throws ProcessManagerException {
    try {
      String[] activeStates = currentProcessInstance.getActiveStates();
      GenericRecordTemplate rt = new GenericRecordTemplate();

      for (final String activeState : activeStates) {
        State state = getState(activeState);
        Actor[] actors = currentProcessInstance.getWorkingUsers(activeState);

        for (int j = 0; actors != null && j < actors.length; j++) {
          GenericFieldTemplate fieldTemplate = new GenericFieldTemplate(
              state.getName() + "_" + actors[j].getUserRoleName() + "_" + j, "user");
          fieldTemplate.addLabel(state.getLabel(currentRole, getLanguage()), getLanguage());
          fieldTemplate.setDisplayerName("user");
          fieldTemplate.setMandatory(true);
          fieldTemplate.addParameter("roles", actors[j].getUserRoleName());
          rt.addFieldTemplate(fieldTemplate);
        }
      }
      return rt;
    } catch (FormException | WorkflowException ex) {
      throw new ProcessManagerException(PROCESS_MANAGER_SESSION_CONTROLLER,
          "processManager.GET_ASSIGN_TEMPLATE_FAILED", ex);
    }
  }

  /**
   * Get assign form (for the re-affectations)
   */
  public Form getAssignForm() throws ProcessManagerException {
    try {
      return new XmlForm(getAssignTemplate());
    } catch (FormException ex) {
      throw new ProcessManagerException(PROCESS_MANAGER_SESSION_CONTROLLER,
          "processManager.GET_ASSIGN_FORM_FAILED", ex);
    }
  }

  /**
   * Get assign data (for the re-affectations)
   */
  public DataRecord getAssignRecord() {
    String[] activeStates = currentProcessInstance.getActiveStates();
    Actor[] actors = null;

    try {
      DataRecord data = getAssignTemplate().getEmptyRecord();
      for (int i = 0; activeStates != null && i < activeStates.length; i++) {
        actors = currentProcessInstance.getWorkingUsers(activeStates[i]);
        for (int j = 0; actors != null && j < actors.length; j++) {
          Field field =
              data.getField(activeStates[i] + "_" + actors[j].getUserRoleName() + "_" + j);
          String value = actors[j].getUser().getUserId();

          if (value != null) {
            field.setStringValue(value);
          }
        }
      }
      return data;
    } catch (WorkflowException | ProcessManagerException | FormException e) {
      SilverLogger.getLogger(this).error(e.getLocalizedMessage(), e);
      return null;
    }
  }

  /**
   * Get assign data (for the re-affectations)
   */
  public void reAssign(DataRecord data) throws ProcessManagerException {
    Actor[] oldUsers;
    List<Actor> oldActors = new ArrayList<>();
    List<Actor> newActors = new ArrayList<>();
    Map<String, String> changes = new HashMap<>();

    try {
      WorkflowEngine wfEngine = Workflow.getWorkflowEngine();
      String[] activeStates = currentProcessInstance.getActiveStates();

      for (String activeState : activeStates) {
        // unassign old working users
        oldUsers = currentProcessInstance.getWorkingUsers(activeState);
        oldActors.addAll(Arrays.asList(oldUsers));
        RelatedUser[] relatedUsers =
            processModel.getState(activeState).getWorkingUsers().getRelatedUsers();

        // assign new working users
        for (int j = 0; j < oldUsers.length; j++) {
          String roleName = oldUsers[j].getUserRoleName();
          Field field = data.getField(activeState + "_" + roleName + "_" + j);
          String userId = field.getStringValue();
          User user = Workflow.getUserManager().getUser(userId);
          Actor newActor = Workflow.getProcessInstanceManager().createActor(
              user, roleName, oldUsers[j].getState());
          newActors.add(newActor);

          // detect folderItem where old userIds are stored
          for (RelatedUser relatedUser : relatedUsers) {
            Item item = relatedUser.getFolderItem();
            if (item != null && roleName.equals(relatedUser.getRole())) {
              addAnyChanges(userId, item, changes);
            }
          }
        }
      }

      wfEngine.reAssignActors((UpdatableProcessInstance) currentProcessInstance,
          oldActors.toArray(new Actor[oldActors.size()]),
          newActors.toArray(new Actor[newActors.size()]), currentUser);

      // update folder
      updateFolder(changes);
    } catch (WorkflowException | FormException we) {
      throw new ProcessManagerException(PROCESS_MANAGER_SESSION_CONTROLLER,
          "processManager.RE_ASSIGN_FAILED", we);
    }
  }
<commit_msg>fix #12558<commit_after>/*
 * Copyright (C) 2000 - 2021 Silverpeas
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * As a special exception to the terms and conditions of version 3.0 of
 * the GPL, you may redistribute this Program in connection with Free/Libre
 * Open Source Software ("FLOSS") applications as described in Silverpeas's
 * FLOSS exception. You should have recieved a copy of the text describing
 * the FLOSS exception, and it is also available here:
 * "http://www.silverpeas.org/docs/core/legal/floss_exception.html"
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */
package org.silverpeas.processmanager;

import org.silverpeas.core.SilverpeasRuntimeException;
import org.silverpeas.core.admin.user.model.Group;
import org.silverpeas.core.admin.user.model.UserDetail;
import org.silverpeas.core.contribution.content.form.DataRecord;
import org.silverpeas.core.contribution.content.form.DataRecordUtil;
import org.silverpeas.core.contribution.content.form.Field;
import org.silverpeas.core.contribution.content.form.FieldTemplate;
import org.silverpeas.core.contribution.content.form.Form;
import org.silverpeas.core.contribution.content.form.FormException;
import org.silverpeas.core.contribution.content.form.PagesContext;
import org.silverpeas.core.contribution.content.form.RecordTemplate;
import org.silverpeas.core.contribution.content.form.field.DateField;
import org.silverpeas.core.contribution.content.form.field.MultipleUserField;
import org.silverpeas.core.contribution.content.form.field.UserField;
import org.silverpeas.core.contribution.content.form.form.XmlForm;
import org.silverpeas.core.contribution.content.form.record.GenericFieldTemplate;
import org.silverpeas.core.contribution.content.form.record.GenericRecordTemplate;
import org.silverpeas.core.notification.message.MessageNotifier;
import org.silverpeas.core.util.DateUtil;
import org.silverpeas.core.util.MapUtil;
import org.silverpeas.core.util.Mutable;
import org.silverpeas.core.util.StringUtil;
import org.silverpeas.core.util.csv.CSVRow;
import org.silverpeas.core.util.logging.SilverLogger;
import org.silverpeas.core.web.export.ExportCSVBuilder;
import org.silverpeas.core.web.mvc.controller.AbstractComponentSessionController;
import org.silverpeas.core.web.mvc.controller.ComponentContext;
import org.silverpeas.core.web.mvc.controller.MainSessionController;
import org.silverpeas.core.web.mvc.webcomponent.WebMessager;
import org.silverpeas.core.workflow.api.UpdatableProcessInstanceManager;
import org.silverpeas.core.workflow.api.Workflow;
import org.silverpeas.core.workflow.api.WorkflowEngine;
import org.silverpeas.core.workflow.api.WorkflowException;
import org.silverpeas.core.workflow.api.error.WorkflowError;
import org.silverpeas.core.workflow.api.event.GenericEvent;
import org.silverpeas.core.workflow.api.event.QuestionEvent;
import org.silverpeas.core.workflow.api.event.ResponseEvent;
import org.silverpeas.core.workflow.api.event.TaskDoneEvent;
import org.silverpeas.core.workflow.api.event.TaskSavedEvent;
import org.silverpeas.core.workflow.api.instance.Actor;
import org.silverpeas.core.workflow.api.instance.HistoryStep;
import org.silverpeas.core.workflow.api.instance.ProcessInstance;
import org.silverpeas.core.workflow.api.instance.Question;
import org.silverpeas.core.workflow.api.instance.UpdatableProcessInstance;
import org.silverpeas.core.workflow.api.model.*;
import org.silverpeas.core.workflow.api.task.Task;
import org.silverpeas.core.workflow.api.user.Replacement;
import org.silverpeas.core.workflow.api.user.ReplacementList;
import org.silverpeas.core.workflow.api.user.User;
import org.silverpeas.core.workflow.api.user.UserInfo;
import org.silverpeas.core.workflow.api.user.UserSettings;
import org.silverpeas.core.workflow.engine.WorkflowHub;
import org.silverpeas.core.workflow.engine.datarecord.ProcessInstanceRowRecord;
import org.silverpeas.core.workflow.engine.instance.LockingUser;
import org.silverpeas.core.workflow.engine.model.ActionRefs;
import org.silverpeas.core.workflow.engine.model.ItemImpl;
import org.silverpeas.core.workflow.engine.user.UserSettingsService;
import org.silverpeas.processmanager.record.QuestionRecord;
import org.silverpeas.processmanager.record.QuestionTemplate;

import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import java.time.LocalDate;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.util.Collections.emptyList;
import static java.util.Optional.ofNullable;
import static org.silverpeas.core.cache.service.CacheServiceProvider.getRequestCacheService;
import static org.silverpeas.core.contribution.attachment.AttachmentService.VERSION_MODE;
import static org.silverpeas.core.util.CollectionUtil.asList;
import static org.silverpeas.core.util.StringUtil.isDefined;
import static org.silverpeas.core.workflow.util.WorkflowUtil.getItemByName;

/**
 * The ProcessManager Session controller
 */
public class ProcessManagerSessionController extends AbstractComponentSessionController {

  private static final String PROCESS_MANAGER_SESSION_CONTROLLER =
      "ProcessManagerSessionController";
  private static final String SUPERVISOR_ROLE = "supervisor";
  private static final String QUESTION_ACTION = "#question#";
  private static final String RESPONSE_ACTION = "#response#";
  private static final String RE_ASSIGN_ACTION = "#reAssign#";

  /**
   * Builds and init a new session controller
   * @param mainSessionCtrl
   * @param context
   * @throws ProcessManagerException
   */
  public ProcessManagerSessionController(MainSessionController mainSessionCtrl,
      ComponentContext context) throws ProcessManagerException {
    super(mainSessionCtrl, context, "org.silverpeas.processManager.multilang.processManagerBundle",
        "org.silverpeas.processManager.settings.processManagerIcons",
        "org.silverpeas.processManager.settings.processManagerSettings");
    // the peasId is the current component id.
    peasId = context.getCurrentComponentId();
    processModel = getProcessModel(peasId);


    // the current user is given by the main session controller.
    currentUser = getUser(mainSessionCtrl.getUserId());
    // the user roles are given by the context.
    userRoles = context.getCurrentProfile();
    if (userRoles == null || userRoles.length == 0) {
      throw new ProcessManagerException(PROCESS_MANAGER_SESSION_CONTROLLER,
          "processManager.UNAUTHORIZED_USER");
    }
    currentRole = userRoles[0];

    // Reset the user rights for creation
    resetCreationRights();

    // Load user information
    userSettings = UserSettingsService.get().get(mainSessionCtrl.getUserId(), peasId);
  }

  /**
   * Builds a ill session controller. Initialization is skipped and this session controller can
   * only * display the fatal exception. Used by the request router when a full session controller
   * can't be built.
   * @param mainSessionCtrl
   * @param context
   * @param fatal
   */
  public ProcessManagerSessionController(MainSessionController mainSessionCtrl,
      ComponentContext context, ProcessManagerException fatal) {
    super(mainSessionCtrl, context,
        "org.silverpeas.processManager.multilang.processManagerBundle",
        "org.silverpeas.processManager.settings.processManagerIcons",
        "org.silverpeas.processManager.settings.processManagerSettings");
    fatalException = fatal;
  }

  /**
   * Get the creation rights
   * @return true if user can do the "Creation" action
   */
  public boolean getCreationRights() {
    return creationRights;
  }

  /**
   * Compute the creation rights set creationRight to true if user can do the "Creation" action
   */
  public final void resetCreationRights() {
    creationRights = false;

    try {
      String[] roles = processModel.getCreationRoles();

      for (String role : roles) {
        if (role.equals(currentRole)) {
          creationRights = true;
        }
      }
    } catch (Exception e) {
      creationRights = false;
    }
  }

  /**
   * L'historique peut être filtré. Dans ce cas, les formulaires associés à chaque état sont
   * visibles uniquement si l'utilisateur courant était un working user ou un interested user.
   */
  public boolean filterHistory() {
    String parameterValue = getComponentParameterValue("filterHistory");
    return StringUtil.getBooleanValue(parameterValue);
  }

  public boolean isHistoryTabVisible() {
    String parameterValue = this.getComponentParameterValue("historyTabEnable");
    if (isDefined(parameterValue)) {
      return "yes".equalsIgnoreCase(parameterValue);
    }
    return true;
  }

  public boolean isCSVExportEnabled() {
    return "yes".equalsIgnoreCase(getComponentParameterValue("exportCSV"));
  }

  /**
   * Print button on an action can be disabled. So it's return the visibility status of that
   * button.
   * @return true is print button is visible
   */
  public boolean isPrintButtonEnabled() {
    return StringUtil.getBooleanValue(getComponentParameterValue("printButtonEnabled"));
  }

  /**
   * Save button on an action can be disabled. So it's return the visibility status of that button.
   * @return true is save button is visible
   */
  public boolean isSaveButtonEnabled() {
    return StringUtil.getBooleanValue(getComponentParameterValue("saveButtonEnabled"));
  }

  /**
   * Returns the last fatal exception
   */
  public ProcessManagerException getFatalException() {
    return fatalException;
  }

  /**
   * Set current Process instance to null
   */
  public void resetCurrentProcessInstance() {
    this.setResumingInstance(false);
    currentProcessInstance = null;
  }

  /**
   * Updates the current process instance from the given id and returns the associated
   * ProcessInstance object. If the instanceId parameter is null, updates nothing and returns the
   * current ProcessInstance. Throws ProcessManagerException when the instanceId is unkwown and
   * when
   * the current user is not allowed to access the instance. Doesn't change the current process
   * instance when an error occures.
   */
  public ProcessInstance resetCurrentProcessInstance(String instanceId)
      throws ProcessManagerException {
    this.setResumingInstance(false);
    if (instanceId != null) {
      ProcessInstance instance;
      try {
        instance = Workflow.getProcessInstanceManager().getProcessInstance(instanceId);
      } catch (WorkflowException e) {
        throw new ProcessManagerException(PROCESS_MANAGER_SESSION_CONTROLLER,
            "processManager.UNKNOWN_PROCESS_INSTANCE", instanceId, e);
      }
      currentProcessInstance = instance;

    }
    if (currentProcessInstance == null) {
      throw new ProcessManagerException(PROCESS_MANAGER_SESSION_CONTROLLER,
          "processManager.NO_CURRENT_PROCESS_INSTANCE");
    }
    return currentProcessInstance;
  }

  /**
   * Get the current process instance
   */
  public ProcessInstance getCurrentProcessInstance() {
    return currentProcessInstance;
  }

  /**
   * Returns the current instance list rows template.
   */
  public RecordTemplate getProcessListHeaders() {
    if (currentListHeaders == null) {
      resetCurrentProcessListHeaders();
    }
    return currentListHeaders;
  }

  /**
   * Reset the current instance list rows template.
   */
  public void resetCurrentProcessListHeaders() {
    currentListHeaders = processModel.getRowTemplate(currentRole, getLanguage());
  }

  /**
   * Returns the current process instance list.
   */
  public List<DataRecord> getCurrentProcessList() throws ProcessManagerException {
    if (currentProcessList == null) {
      return resetCurrentProcessList(false);
    } else {
      return currentProcessList;
    }
  }

  /**
   * Updates the current process instance list with current filter and returns this list. Doesn't
   * change the current process instance when an error occurs.
   */
  public List<DataRecord> resetCurrentProcessList(boolean doAPause) throws ProcessManagerException {
    // Wait to display processes list up-to-date
    if (doAPause) {
      doAPause();
    }

    try {
      User activeUser = getActiveUser();
      String[] groupIds = getOrganisationController().getAllGroupIdsOfUser(activeUser.getUserId());
      List<ProcessInstance> processList = Workflow.getProcessInstanceManager()
          .getProcessInstances(peasId, activeUser, currentRole, getUserRoles(), groupIds);
      currentProcessList = getCurrentFilter().filter(processList, currentRole, getLanguage());
    } catch (WorkflowException e) {
      throw new ProcessManagerException(PROCESS_MANAGER_SESSION_CONTROLLER,
          "processManager.GET_PROCESS_LIST_FAILED", peasId, e);
    }
    return currentProcessList;
  }

  /**
   * Get the role name of task referred by the todo with the given todo id
   */
  public String getRoleNameFromExternalTodoId(String externalTodoId)
      throws ProcessManagerException {
    try {
      return Workflow.getTaskManager().getRoleNameFromExternalTodoId(externalTodoId);
    } catch (WorkflowException e) {
      throw new ProcessManagerException(PROCESS_MANAGER_SESSION_CONTROLLER,
          "processManager.GET_ROLENAME_FROM_TODO_FAILED", "externalTodoId : " + externalTodoId, e);
    }
  }

  /**
   * Get the process instance Id referred by the todo with the given todo id
   * @param externalTodoId external todo identifier
   * @return the process instance Id referred by the todo with the given todo id.
   * @throws ProcessManagerException
   */
  public String getProcessInstanceIdFromExternalTodoId(String externalTodoId)
      throws ProcessManagerException {
    try {
      return Workflow.getTaskManager().getProcessInstanceIdFromExternalTodoId(externalTodoId);
    } catch (WorkflowException e) {
      throw new ProcessManagerException(PROCESS_MANAGER_SESSION_CONTROLLER,
          "processManager.GET_PROCESS_FROM_TODO_FAILED", "externalTodoId : " + externalTodoId, e);
    }
  }

  public boolean isUserAllowedOnActiveStates() {
    String[] states = currentProcessInstance.getActiveStates();
    if (states == null) {
      return false;
    }
    for (String state : states) {
      if (getActiveUsers(state).contains(getActiveUser().getUserId())) {
        return true;
      }
    }
    return false;
  }

  /**
   * Get the active states.
   * @return the active states.
   */
  public List<CurrentState> getActiveStates() throws ProcessManagerException {
    String[] stateNames = currentProcessInstance.getActiveStates();
    if (stateNames == null) {
      return emptyList();
    }

    Map<String, List<Task>> tasksByStates = getTasksByStates();

    List<CurrentState> currentStates = new ArrayList<>();
    for (String stateName : stateNames) {
      State state = getState(stateName);
      filterActions(state);
      CurrentState currentState = new CurrentState(state);
      currentState.setLabel(state.getLabel(currentRole, getLanguage()));
      currentState.setWorkingUsersAsString(getActiveRoles(state));

      List<Task> tasks = tasksByStates.get(stateName);
      currentState.setTasks(tasks);

      currentStates.add(currentState);
    }

    return currentStates;
  }

  private Map<String, List<Task>> getTasksByStates() throws ProcessManagerException {
    Map<String, List<Task>> result = new HashMap<>();
    Task[] tasks = getTasks();
    for (Task task : tasks) {
      State state = task.getState();
      MapUtil.putAddList(result, state.getName(), task);
    }
    return result;
  }

  private String getActiveRoles(State state) {

    QualifiedUsers workingUsers = state.getWorkingUsers();

    RelatedUser[] relatedUsers = workingUsers.getRelatedUsers();
    StringBuilder role = new StringBuilder();
    if (relatedUsers != null) {
      for (RelatedUser relatedUser : relatedUsers) {
        if (role.length() > 0) {
          role.append(", ");
        }
        // Process participants
        Participant participant = relatedUser.getParticipant();
        String relation = relatedUser.getRelation();
        if (participant != null && relation == null) {
          role.append(participant.getLabel(currentRole, getLanguage()));
        } else if (participant != null && relation != null) {
          // userSettings of requester (not current user)
          String requesterId = getCreatorIdOfCurrentProcessInstance();
          if (requesterId != null) {
            UserInfo userInfo = getSettingsOfUser(requesterId).getUserInfo(relation);
            if (userInfo != null) {
              role.append(getUserDetail(userInfo.getValue()).getDisplayedName());
            }
          }
        }

        // Process folder item
        Item item = relatedUser.getFolderItem();
        if (item != null) {
          try {
            Field field = currentProcessInstance.getField(item.getName());
            if (field instanceof UserField) {
              String userId = field.getStringValue();
              if (userId != null) {
                UserDetail user = getUserDetail(userId);
                if (user != null) {
                  role.append(user.getDisplayedName());
                }
              }
            } else if (field instanceof MultipleUserField) {
              MultipleUserField multipleUserField = (MultipleUserField) field;
              String[] userIds = multipleUserField.getUserIds();
              for (String userId : userIds) {
                if (userId != null) {
                  UserDetail user = getUserDetail(userId);
                  if (user != null) {
                    if (role.length() > 0) {
                      role.append(", ");
                    }
                    role.append(user.getDisplayedName());
                  }
                }
              }
            }
          } catch (Exception e) {
            SilverLogger.getLogger(this).warn(e);
          }
        }
      }
    }

    UserInRole[] userInRoles = workingUsers.getUserInRoles();
    if (userInRoles != null) {
      for (UserInRole userInRole : userInRoles) {
        if (role.length() > 0) {
          role.append(", ");
        }
        role.append(processModel.getRole(userInRole.getRoleName())
            .getLabel(currentRole, getLanguage()));
      }
    }

    RelatedGroup[] relatedGroups = workingUsers.getRelatedGroups();
    if (relatedGroups != null) {
      for (RelatedGroup relatedGroup : relatedGroups) {
        if (relatedGroup != null) {
          if (role.length() > 0) {
            role.append(", ");
          }

          // Process folder item
          Item item = relatedGroup.getFolderItem();
          try {
            if (item != null) {
              String groupId = currentProcessInstance.getField(item.getName()).getStringValue();
              if (groupId != null) {
                Group group = getOrganisationController().getGroup(groupId);
                if (group != null) {
                  role.append(group.getName());
                }
              }
            }
          } catch (Exception e) {
            SilverLogger.getLogger(this).warn(e);
          }
        }
      }
    }
    return role.toString();
  }

  /**
   * Get the active user names
   */
  private boolean isActiveUser() {
    if (currentProcessInstance == null) {
      return false;
    }
    final String[] states = currentProcessInstance.getActiveStates();
    return states != null && Stream.of(states)
        .flatMap(s -> {
          try {
            return Stream.of(currentProcessInstance.getWorkingUsers(s));
          } catch (WorkflowException ignored) {
            // ignore unknown state
          }
          return Stream.empty();
        })
        .anyMatch(u -> getActiveUser().getUserId().equals(u.getUser().getUserId()));
  }

  private List<String> getActiveUsers(String stateName) {
    List<String> activeUsers = new ArrayList<>();
    State state = getState(stateName);
    if (state != null) {
      activeUsers.addAll(getUsers(state.getWorkingUsers()));
      activeUsers.addAll(getUsers(state.getInterestedUsers()));
    }
    return activeUsers;
  }

  private List<String> getUsers(QualifiedUsers qualifiedUsers) {
    return getUsers(qualifiedUsers, false);
  }

  private List<String> getUsers(QualifiedUsers qualifiedUsers, boolean useCurrentRole) {
    List<String> users = new ArrayList<>();
    RelatedUser[] relatedUsers = qualifiedUsers.getRelatedUsers();
    RelatedUser relatedUser;
    List<String> roles = new ArrayList<>();
    for (final RelatedUser relatedUser1 : relatedUsers) {
      relatedUser = relatedUser1;
      // Process participants
      Participant participant = relatedUser.getParticipant();
      String relation = relatedUser.getRelation();
      if (participant != null && relation == null) {
        if (currentRole.equals(relatedUser.getRole())) {
          users.add(getActiveUser().getUserId());
        }
      } else if (participant != null && relation != null) {
        String requesterId = getCreatorIdOfCurrentProcessInstance();
        if (requesterId != null) {
          UserInfo userInfo = getSettingsOfUser(requesterId).getUserInfo(relation);
          if (userInfo != null) {
            users.add(userInfo.getValue());
          }
        }
      }

      // Process folder item
      Item item = relatedUser.getFolderItem();
      if (item != null) {
        try {
          Field field = currentProcessInstance.getField(item.getName());
          String role = relatedUser.getRole();
          if (field instanceof UserField) {
            if ((isDefined(role) && currentRole.equals(role)) ||
                StringUtil.isNotDefined(role)) {
              users.add(field.getStringValue());
            }
          } else if (field instanceof MultipleUserField) {
            MultipleUserField multipleUserField = (MultipleUserField) field;
            if ((isDefined(role) && currentRole.equals(role)) ||
                StringUtil.isNotDefined(role)) {
              users.addAll(Arrays.asList(multipleUserField.getUserIds()));
            }
          }
        } catch (WorkflowException we) {
          // ignore it.
        }
      }
    }

    UserInRole[] userInRoles = qualifiedUsers.getUserInRoles();
    for (final UserInRole userInRole : userInRoles) {
      roles.add(userInRole.getRoleName());
    }

    if (useCurrentRole) {
      if (roles.contains(currentRole)) {
        roles.clear();
        roles.add(currentRole);
      } else {
        roles.clear();
      }
    }

    String[] userIds = getOrganisationController().getUsersIdsByRoleNames(getComponentId(), roles);
    for (final String userId : userIds) {
      users.add(userId);
    }

    // Process related groups
    RelatedGroup[] relatedGroups = qualifiedUsers.getRelatedGroups();
    if (relatedGroups != null) {
      for (RelatedGroup relatedGroup : relatedGroups) {
        if (relatedGroup != null) {
          String role = relatedGroup.getRole();
          if (currentRole.equals(role) || StringUtil.isNotDefined(role)) {
            // Process folder item
            Item item = relatedGroup.getFolderItem();
            if (item != null) {
              try {
                String groupId = currentProcessInstance.getField(item.getName()).getStringValue();
                UserDetail[] usersOfGroup = getOrganisationController().getAllUsersOfGroup(groupId);
                for (UserDetail userOfGroup : usersOfGroup) {
                  users.add(userOfGroup.getId());
                }
              } catch (WorkflowException we) {// ignore it.
              }
            }
          }
        }
      }
    }

    return users;
  }

  /**
   * Returns the workflow user having the given id.
   * @param userId the user identifier
   * @return the workflow user having the given id.
   * @throws ProcessManagerException
   */
  public User getUser(String userId) throws ProcessManagerException {
    try {
      return Workflow.getUserManager().getUser(userId);
    } catch (WorkflowException e) {
      throw new ProcessManagerException("ProcessManagerSessionControler",
          "processManager.UNKNOWN_USER", userId, e);
    }
  }

  /**
   * Returns the process model having the given id.
   * @param modelId the model identifier
   * @return the process model having the given id.
   * @throws ProcessManagerException
   */
  public final ProcessModel getProcessModel(String modelId) throws ProcessManagerException {
    try {
      return Workflow.getProcessModelManager().getProcessModel(modelId);
    } catch (WorkflowException e) {
      throw new ProcessManagerException("ProcessManagerSessionControler",
          "processManager.UNKNOWN_PROCESS_MODEL", modelId, e);
    }
  }

  /**
   * @return the current role name.
   */
  public String getCurrentRole() {
    return currentRole;
  }

  /**
   * @return the current replacement if any.
   */
  public Replacement getCurrentReplacement() {
    return currentReplacement;
  }

  public void resetCurrentRoleAsSubstitute(String role, String incumbentId)
      throws ProcessManagerException {
    final Mutable<String> finalRole = Mutable.of(role);
    final Optional<Replacement> replacement = getCurrentUserReplacement(incumbentId, role);
    replacement.ifPresent(r -> finalRole.set(getRoleNameForSubstitute(r, role)));
    if (!replacement.isPresent()) {
      WebMessager.getInstance().addWarning(getMultilang()
          .getStringWithParams("processManager.replacements.errors.noMoreValid",
              getUser(incumbentId).getFullName()));
    }
    resetCurrentRole(finalRole.get());
  }

  /**
   * Returns the current role name.
   * @param role a role
   * @throws ProcessManagerException
   */
  public void resetCurrentRole(String role) throws ProcessManagerException {
    if (role != null && role.length() > 0) {
      final String[] roleCtx = role.split(":");
      if (roleCtx.length == 2) {
        Replacement.get(roleCtx[0]).ifPresent(r -> {
          this.currentReplacement = r;
          this.currentRole = roleCtx[1];
        });
      } else {
        this.currentReplacement = null;
        this.currentRole = role;
      }
    }
    resetCreationRights();
    resetProcessFilter();
    resetCurrentProcessList(false);
    resetCurrentProcessListHeaders();
  }

  /**
   * Returns the user roles as a list of (name, label) pair.
   */
  public NamedValue[] getUserRoleLabels() {
    return getRequestCacheService()
        .getCache()
        .computeIfAbsent("ProcessManagerSC.getUserRoleLabels" + getComponentId(), NamedValue[].class, () -> {
          final String lang = getLanguage();
          final Role[] roles = processModel.getRoles();
          final List<NamedValue> labels = new ArrayList<>();

          final List<Replacement> replacements = getCurrentUserReplacements();
          for (final Replacement replacement : replacements) {
            final List<String> incumbentRoles = getSubstituteRolesOf(replacement.getIncumbent());
            for (final String roleName : incumbentRoles) {
              getRoleLabel(replacement, roles, roleName, lang)
                  .filter(n -> labels.stream().noneMatch(l -> Objects.equals(n.getValue(), l.getValue())))
                  .ifPresent(labels::add);
            }
          }

          // quadratic search ! but it's ok : the list are about 3 or 4 length.
          for (final String userRole : userRoles) {
            getRoleLabel(null, roles, userRole, lang).ifPresent(labels::add);
          }

          labels.sort(NamedValue.ascendingValues);
          return labels.toArray(new NamedValue[0]);
        });
  }

  /**
   * Returns the component instance roles as a list of (name, label) pair.
   */
  public NamedValue[] getComponentInstanceRoleLabels() {
    String lang = getLanguage();
    Role[] roles = processModel.getRoles();
    List<NamedValue> labels = new ArrayList<>();
    for (final Role role : roles) {
      getRoleLabel(null, roles, role.getName(), lang).ifPresent(labels::add);
    }
    labels.sort(NamedValue.ascendingValues);
    return  labels.toArray(new NamedValue[0]);
  }

  /**
   * Returns the form presenting the folder of the current process instance.
   */
  public Form getPresentationForm() throws ProcessManagerException {
    try {
      Form form = getPresentationForm("presentationForm");
      if (form != null) {
        form.setViewForm(true);
        return form;
      }

      XmlForm xmlForm = new XmlForm(
          processModel.getDataFolder().toRecordTemplate(currentRole, getLanguage(), true));
      xmlForm.setTitle(getString("processManager.folder"));
      xmlForm.setViewForm(true);
      return xmlForm;
    } catch (FormException | WorkflowException e) {
      throw new ProcessManagerException("SessionController",
          "processManager.PRESENTATION_FORM_UNAVAILABLE", e);
    }
  }

  public Item[] getFolderItems() {
    return processModel.getDataFolder().getItems();
  }

  /**
   * Returns the folder data of the current process instance.
   */
  public DataRecord getFolderRecord() throws ProcessManagerException {
    DataRecord data = null;
    try {
      if (currentProcessInstance != null) {
        data = currentProcessInstance.getFormRecord("presentationForm", currentRole, getLanguage());

        if (data == null) {
          data = currentProcessInstance.getFolder();
        }
      }
    } catch (WorkflowException e) {
      throw new ProcessManagerException("SessionController",
          "processManager.PRESENTATION_DATA_UNAVAILABLE", e);
    }

    if (data == null) {
      throw new ProcessManagerException("SessionController",
          "processManager.PRESENTATION_DATA_UNAVAILABLE");
    }

    return data;
  }

  /**
   * Returns the creation task.
   */
  private Task getCreationTask() throws ProcessManagerException {
    try {
      return Workflow.getTaskManager().getCreationTask(getActiveUser(), currentRole, processModel);
    } catch (WorkflowException e) {
      throw new ProcessManagerException("SessionController",
          "processManager.CREATION_TASK_UNAVAILABLE", e);
    }
  }

  /**
   * Returns the form which starts a new instance.
   */
  public Form getCreationForm() throws ProcessManagerException {
    try {
      Action creation = processModel.getCreateAction(currentRole);
      return processModel.getPublicationForm(creation.getName(), currentRole, getLanguage());
    } catch (WorkflowException e) {
      throw new ProcessManagerException("SessionController", "processManager.NO_CREATION_FORM", e);
    }
  }

  /**
   * Returns the an empty creation record which will be filled with the creation form.
   */
  public DataRecord getEmptyCreationRecord() throws ProcessManagerException {
    try {
      Action creation = processModel.getCreateAction(currentRole);
      return processModel.getNewActionRecord(creation.getName(), currentRole, getLanguage(), null);
    } catch (WorkflowException e) {
      throw new ProcessManagerException("SessionController", "processManager.UNKNOWN_ACTION", e);
    }
  }

  /**
   * Returns the form to ask a question
   */
  public Form getQuestionForm(boolean readonly) throws ProcessManagerException {
    try {
      return new XmlForm(new QuestionTemplate(getLanguage(), readonly));
    } catch (FormException fe) {
      throw new ProcessManagerException(PROCESS_MANAGER_SESSION_CONTROLLER,
          "processManager.GET_QUESTION_FORM_FAILED", fe);
    }
  }

  /**
   * Returns the an empty question record which will be filled with the question form.
   */
  public DataRecord getEmptyQuestionRecord() {
    return new QuestionRecord("");
  }

  /**
   * get the question with the given id
   * @param questionId question id
   * @return the found question
   */
  public Question getQuestion(String questionId) {
    Question[] questions = currentProcessInstance.getQuestions();
    Question foundQuestion = null;

    for (int i = 0; foundQuestion == null && i < questions.length; i++) {
      if (questions[i].getId().equals(questionId)) {
        foundQuestion = questions[i];
      }
    }

    return foundQuestion;
  }

  /**
   * Returns the an empty question record which will be filled with the question form.
   */
  public DataRecord getQuestionRecord(String questionId) {
    Question question = getQuestion(questionId);
    return new QuestionRecord(question.getQuestionText());
  }

  /**
   * Get assign template (for the re-affectations)
   */
  private GenericRecordTemplate getAssignTemplate() throws ProcessManagerException {
    try {
      String[] activeStates = currentProcessInstance.getActiveStates();
      GenericRecordTemplate rt = new GenericRecordTemplate();

      for (final String activeState : activeStates) {
        State state = getState(activeState);
        Actor[] actors = currentProcessInstance.getWorkingUsers(activeState);

        for (int j = 0; actors != null && j < actors.length; j++) {
          GenericFieldTemplate fieldTemplate = new GenericFieldTemplate(
              processStateName(state.getName()) + "_" + actors[j].getUserRoleName() + "_" + j,
              "user");
          fieldTemplate.addLabel(state.getLabel(currentRole, getLanguage()), getLanguage());
          fieldTemplate.setDisplayerName("user");
          fieldTemplate.setMandatory(true);
          fieldTemplate.addParameter("roles", actors[j].getUserRoleName());
          rt.addFieldTemplate(fieldTemplate);
        }
      }
      return rt;
    } catch (FormException | WorkflowException ex) {
      throw new ProcessManagerException(PROCESS_MANAGER_SESSION_CONTROLLER,
          "processManager.GET_ASSIGN_TEMPLATE_FAILED", ex);
    }
  }

  /**
   * Deletion of problematic characters in state name (spaces)
   * @param name
   * @return
   */
  private String processStateName(String name) {
    return name.replaceAll(" ", "");
  }

  /**
   * Get assign form (for the re-affectations)
   */
  public Form getAssignForm() throws ProcessManagerException {
    try {
      return new XmlForm(getAssignTemplate());
    } catch (FormException ex) {
      throw new ProcessManagerException(PROCESS_MANAGER_SESSION_CONTROLLER,
          "processManager.GET_ASSIGN_FORM_FAILED", ex);
    }
  }

  /**
   * Get assign data (for the re-affectations)
   */
  public DataRecord getAssignRecord() {
    String[] activeStates = currentProcessInstance.getActiveStates();
    Actor[] actors = null;

    try {
      DataRecord data = getAssignTemplate().getEmptyRecord();
      for (int i = 0; activeStates != null && i < activeStates.length; i++) {
        actors = currentProcessInstance.getWorkingUsers(activeStates[i]);
        for (int j = 0; actors != null && j < actors.length; j++) {
          Field field =
              data.getField(processStateName(activeStates[i]) + "_" + actors[j].getUserRoleName() + "_" + j);
          String value = actors[j].getUser().getUserId();

          if (value != null) {
            field.setStringValue(value);
          }
        }
      }
      return data;
    } catch (WorkflowException | ProcessManagerException | FormException e) {
      SilverLogger.getLogger(this).error(e.getLocalizedMessage(), e);
      return null;
    }
  }

  /**
   * Get assign data (for the re-affectations)
   */
  public void reAssign(DataRecord data) throws ProcessManagerException {
    Actor[] oldUsers;
    List<Actor> oldActors = new ArrayList<>();
    List<Actor> newActors = new ArrayList<>();
    Map<String, String> changes = new HashMap<>();

    try {
      WorkflowEngine wfEngine = Workflow.getWorkflowEngine();
      String[] activeStates = currentProcessInstance.getActiveStates();

      for (String activeState : activeStates) {
        // unassign old working users
        oldUsers = currentProcessInstance.getWorkingUsers(activeState);
        oldActors.addAll(Arrays.asList(oldUsers));
        RelatedUser[] relatedUsers =
            processModel.getState(activeState).getWorkingUsers().getRelatedUsers();

        // assign new working users
        for (int j = 0; j < oldUsers.length; j++) {
          String roleName = oldUsers[j].getUserRoleName();
          Field field = data.getField(processStateName(activeState) + "_" + roleName + "_" + j);
          String userId = field.getStringValue();
          User user = Workflow.getUserManager().getUser(userId);
          Actor newActor = Workflow.getProcessInstanceManager().createActor(
              user, roleName, oldUsers[j].getState());
          newActors.add(newActor);

          // detect folderItem where old userIds are stored
          for (RelatedUser relatedUser : relatedUsers) {
            Item item = relatedUser.getFolderItem();
            if (item != null && roleName.equals(relatedUser.getRole())) {
              addAnyChanges(userId, item, changes);
            }
          }
        }
      }

      wfEngine.reAssignActors((UpdatableProcessInstance) currentProcessInstance,
          oldActors.toArray(new Actor[oldActors.size()]),
          newActors.toArray(new Actor[newActors.size()]), currentUser);

      // update folder
      updateFolder(changes);
    } catch (WorkflowException | FormException we) {
      throw new ProcessManagerException(PROCESS_MANAGER_SESSION_CONTROLLER,
          "processManager.RE_ASSIGN_FAILED", we);
    }
  }

{'fraction_non_alphanumeric': 0.10016047958195733, 'fraction_numerical': 0.0009151315004177774, 'mean_word_length': 3.1478710529211136, 'pattern_counts': {'":': 2, '<': 89, '<?xml version=': 0, '>': 103, 'https://': 0, 'lorem ipsum': 0, 'www.': 4, 'xml': 8}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6993468', 'n_tokens_mistral': 20712, 'n_tokens_neox': 20331, 'n_words': 4778}