ProjectManager.java

/*
 * The coLAB project
 * Copyright (C) 2021-2023 AlbaSim, MEI, HEIG-VD, HES-SO
 *
 * Licensed under the MIT License
 */
package ch.colabproject.colab.api.controller.project;

import ch.colabproject.colab.api.controller.DuplicationManager;
import ch.colabproject.colab.api.controller.RequestManager;
import ch.colabproject.colab.api.controller.card.CardContentManager;
import ch.colabproject.colab.api.controller.card.CardManager;
import ch.colabproject.colab.api.controller.card.CardTypeManager;
import ch.colabproject.colab.api.controller.common.DeletionManager;
import ch.colabproject.colab.api.controller.document.FileManager;
import ch.colabproject.colab.api.controller.document.ResourceReferenceSpreadingHelper;
import ch.colabproject.colab.api.controller.security.SecurityManager;
import ch.colabproject.colab.api.controller.team.TeamManager;
import ch.colabproject.colab.api.controller.token.TokenManager;
import ch.colabproject.colab.api.model.DuplicationParam;
import ch.colabproject.colab.api.model.card.AbstractCardType;
import ch.colabproject.colab.api.model.card.Card;
import ch.colabproject.colab.api.model.card.CardContent;
import ch.colabproject.colab.api.model.link.ActivityFlowLink;
import ch.colabproject.colab.api.model.project.CopyParam;
import ch.colabproject.colab.api.model.project.Project;
import ch.colabproject.colab.api.model.team.TeamMember;
import ch.colabproject.colab.api.model.team.acl.HierarchicalPosition;
import ch.colabproject.colab.api.model.user.User;
import ch.colabproject.colab.api.persistence.jpa.project.CopyParamDao;
import ch.colabproject.colab.api.persistence.jpa.project.ProjectDao;
import ch.colabproject.colab.api.rest.project.bean.ProjectCreationData;
import ch.colabproject.colab.api.rest.project.bean.ProjectStructure;
import ch.colabproject.colab.generator.model.exceptions.HttpErrorMessage;
import ch.colabproject.colab.generator.model.exceptions.MessageI18nKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.ejb.LocalBean;
import javax.ejb.Stateless;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * Project specific logic
 *
 * @author maxence
 * @author sandra
 */
@Stateless
@LocalBean
public class ProjectManager {

    /** logger */
    private static final Logger logger = LoggerFactory.getLogger(ProjectManager.class);

    // *********************************************************************************************
    // injections
    // *********************************************************************************************

    /**
     * Common deletion lifecycle management
     */
    @Inject
    private DeletionManager deletionManager;

    /**
     * To control access
     */
    @Inject
    private SecurityManager securityManager;

    /**
     * Request management
     */
    @Inject
    private RequestManager requestManager;

    /**
     * Project persistence handler
     */
    @Inject
    private ProjectDao projectDao;

    /**
     * Copy parameter persistence handler
     */
    @Inject
    private CopyParamDao copyParamDao;

    /**
     * Team specific logic management
     */
    @Inject
    private TeamManager teamManager;

    /**
     * Card specific logic management
     */
    @Inject
    private CardManager cardManager;

    /** to load cardContents */
    @Inject
    private CardContentManager cardContentManager;

    /**
     * Card type specific logic management
     */
    @Inject
    private CardTypeManager cardTypeManager;

    /**
     * Token specific logic management
     */
    @Inject
    private TokenManager tokenManager;

    /**
     * Resource reference spreading specific logic handling
     */
    @Inject
    private ResourceReferenceSpreadingHelper resourceReferenceSpreadingHelper;

    /**
     * File persistence management
     */
    @Inject
    private FileManager fileManager;

    // *********************************************************************************************
    // find projects
    // *********************************************************************************************

    /**
     * Retrieve the project. If not found, throw a {@link HttpErrorMessage}.
     *
     * @param projectId the id of the project
     *
     * @return the project if found
     *
     * @throws HttpErrorMessage if the project was not found
     */
    public Project assertAndGetProject(Long projectId) {
        Project project = projectDao.findProject(projectId);

        if (project == null) {
            logger.error("project #{} not found", projectId);
            throw HttpErrorMessage.dataError(MessageI18nKey.DATA_NOT_FOUND);
        }

        return project;
    }

    /**
     * Retrieve the copy parameter. If not found, throw a {@link HttpErrorMessage}.
     *
     * @param copyParamId the id of the copy parameter
     *
     * @return the copy parameter if found
     *
     * @throws HttpErrorMessage if the copy parameter was not found
     */
    public CopyParam assertAndGetCopyParam(Long copyParamId) {
        CopyParam copyParam = copyParamDao.findCopyParamByProject(copyParamId);

        if (copyParam == null) {
            logger.error("copy param #{} not found", copyParamId);
            throw HttpErrorMessage.dataError(MessageI18nKey.DATA_NOT_FOUND);
        }

        return copyParam;
    }

    /**
     * Retrieve all projects the current user is a team member of
     *
     * @return list of matching projects
     */
    public List<Project> findProjectsWhereTeamMember() {
        User user = securityManager.assertAndGetCurrentUser();

        List<Project> projects = projectDao.findProjectsByTeamMember(user.getId());

        logger.debug("found projects where the user {} is a team member : {}", user, projects);

        return projects;
    }

    /**
     * Retrieve all projects the current user is an instance maker for
     *
     * @return list of matching projects
     */
    public List<Project> findInstanceableModelsForCurrentUser() {
        User user = securityManager.assertAndGetCurrentUser();

        List<Project> projects = projectDao.findProjectsByInstanceMaker(user.getId());

        logger.debug("found models where the user {} is an instance maker : {}", user, projects);

        return projects;
    }

    // *********************************************************************************************
    // life cycle
    // *********************************************************************************************

    /**
     * Create a new project with the provided data and register current user as a member.
     *
     * @param creationData the data needed to fill the project
     *
     * @return the persisted new project
     */
    public Project createProject(ProjectCreationData creationData) {
        if (creationData.getBaseProjectId() == null) {
            return createProjectFromScratch(creationData);
        } else {
            return createProjectFromModel(creationData);
        }
    }

    /**
     * Create a new project from scratch with the provided data and register current user as a
     * member.
     *
     * @param creationData the data needed to fill the project
     *
     * @return the persisted new project
     */
    private Project createProjectFromScratch(ProjectCreationData creationData) {
        logger.debug("Create a project with {}", creationData);

        Project project = new Project();
        project.setType(creationData.getType());
        project.setName(creationData.getName());
        project.setDescription(creationData.getDescription());
        project.setIllustration(creationData.getIllustration());

        createNewProject(project);

        return project;
    }

    /**
     * Complete and persist the given project and add the current user to the project team
     *
     * @param project project to persist
     *
     * @return the new persisted project
     */
    private Project createNewProject(Project project) {
        try {
            return requestManager.sudo(() -> {
                logger.debug("Create project {}", project);

                initProject(project);

                return projectDao.persistProject(project);
            });
        } catch (RuntimeException ex) {
            throw ex;
        } catch (Exception ex) {
            return null;
        }
    }

    /**
     * Initialize the project with a root card (if it does not have already one) and set the current
     * user as a team member owner of the project
     *
     * @param project the project to fill
     */
    private void initProject(Project project) {
        if (project.getRootCard() == null) {
            Card rootCard = cardManager.initNewRootCard();

            project.setRootCard(rootCard);
            rootCard.setRootCardProject(project);
        }

        User user = securityManager.assertAndGetCurrentUser();
        Optional<TeamMember> currentUserTeamMember = project.getTeamMembers().stream()
            .filter(tm -> tm.getUserId() == user.getId()).findFirst();
        if (currentUserTeamMember.isPresent()) {
            currentUserTeamMember.get().setPosition(HierarchicalPosition.OWNER);
        } else {
            teamManager.addMember(project, user, HierarchicalPosition.OWNER);
        }
    }

    /**
     * Create a new project from a model with the provided data and register current user as a
     * member.
     *
     * @param creationData the data needed to fill the project
     *
     * @return id of the persisted new project
     */
    private Project createProjectFromModel(ProjectCreationData creationData) {
        logger.debug("Create a project with {}", creationData);

        try {
            return requestManager.sudo(() -> {

                DuplicationParam effectiveParams = creationData.getDuplicationParam();
                if (effectiveParams == null) {
                    effectiveParams = DuplicationParam.buildForProjectCreationFromModel();
                }

                // override by model copy parameters
                CopyParam copyParam = getCopyParam(creationData.getBaseProjectId());
                if (copyParam != null) {
                    effectiveParams.setWithRoles(copyParam.isWithRoles());
                    effectiveParams.setWithDeliverables(copyParam.isWithDeliverables());
                    effectiveParams.setWithResources(copyParam.isWithResources());
                }

                Project project = duplicateProject(creationData.getBaseProjectId(),
                    effectiveParams);

                project.setType(creationData.getType());
                project.setName(creationData.getName());
                project.setDescription(creationData.getDescription());
                project.setIllustration(creationData.getIllustration());

                return project;
            });

        } catch (RuntimeException ex) {
            throw ex;
        } catch (Exception ex) {
            return null;
        }
    }

    /**
     * @param projectId the id of the project
     *
     * @return the related copy parameter
     */
    public CopyParam getCopyParam(Long projectId) {
        Project project = assertAndGetProject(projectId);

        CopyParam copyParam = copyParamDao.findCopyParamByProject(projectId);

        if (copyParam == null) {
            copyParam = CopyParam.buildDefault(project);
            copyParamDao.persistCopyParam(copyParam);
        }

        return copyParam;
    }

    /**
     * Put the given project in the bin. (= set DeletionStatus to BIN + set erasure
     * tracking data)
     *
     * @param projectId the id of the project
     */
    public void putProjectInBin(Long projectId) {
        logger.debug("put in bin project #{}", projectId);

        Project project = assertAndGetProject(projectId);

        deletionManager.putInBin(project);
    }

    /**
     * Restore from the bin. The project won't contain any deletion or erasure data anymore.
     * <p>
     * It means that the project is back at its place.
     *
     * @param projectId the id of the project
     */
    public void restoreProjectFromBin(Long projectId) {
        logger.debug("restore from bin project #{}", projectId);

        Project project = assertAndGetProject(projectId);

        deletionManager.restoreFromBin(project);
    }

    /**
     * Set the deletion status to TO_DELETE.
     * <p>
     * It means that the project is only visible in the bin panel.
     *
     * @param projectId the id of the project
     */
    public void markProjectAsToDeleteForever(Long projectId) {
        logger.debug("mark project #{} as to delete forever", projectId);

        Project project = assertAndGetProject(projectId);

        deletionManager.markAsToDeleteForever(project);
    }

    /**
     * Delete the given project
     *
     * @param projectId the id of the project to delete
     */
    public void deleteProject(Long projectId) {
        Project project = assertAndGetProject(projectId);

//        if (!checkDeletionAcceptability(project)) {
//            throw HttpErrorMessage.dataError(MessageI18nKey.DATA_INTEGRITY_FAILURE);
//        }

//      tokenManager.deleteTokensByProject(project);
        project.getTeamMembers().stream()
            .forEach(member -> tokenManager.deleteInvitationsByTeamMember(member));

        // everything else is deleted by cascade

        projectDao.deleteProject(project);
    }

//    /**
//     * Ascertain that the project can be deleted
//     *
//     * @param project the project to check for deletion
//     *
//     * @return True iff it can be safely deleted
//     */
//    private boolean checkDeletionAcceptability(Project project) {
//        return true;
//    }

    // *********************************************************************************************
    // duplication
    // *********************************************************************************************

    /**
     * Duplicate the given project with the given parameters to fine tune the duplication
     *
     * @param projectId the id of the project to duplicate
     * @param params    the parameters to fine tune the duplication
     *
     * @return the new project
     */
    public Project duplicateProject(Long projectId, DuplicationParam params) {

        try {
            return requestManager.sudo(() -> { // TODO ! move sudo where really useful
                Project originalProject = assertAndGetProject(projectId);

                DuplicationManager duplicator = new DuplicationManager(params,
                    resourceReferenceSpreadingHelper, fileManager, cardContentManager);

                Project newProjectJavaObject = duplicator.duplicateProject(originalProject);

                Project newProject = createNewProject(newProjectJavaObject);

                duplicator.duplicateDataIntoJCR();
                duplicator.duplicateLexicalData();

                duplicator.clear();

                return newProject;
            });

        } catch (RuntimeException ex) {
            throw ex;
        } catch (Exception ex) {
            return null;
        }
    }

    // *********************************************************************************************
    // retrieve the elements of a project
    // *********************************************************************************************

    /**
     * Get the root card of the given project
     *
     * @param projectId the id of the project
     *
     * @return The root card linked to the project
     */
    public Card getRootCard(Long projectId) {
        logger.debug("get the root card of the project #{}", projectId);

        Project project = assertAndGetProject(projectId);

        return project.getRootCard();
    }

    /**
     * Get all cards of the given project
     *
     * @param projectId the id of the project
     *
     * @return all cards of the project
     */
    public Set<Card> getCards(Long projectId) {
        logger.debug("get all card of project #{}", projectId);

        Project project = assertAndGetProject(projectId);

        return cardManager.getAllCards(project.getRootCard());
    }

    /**
     * Get all cardContents of the given project
     *
     * @param projectId the id of the project
     *
     * @return all cards contents of the project
     */
    public Set<CardContent> getCardContents(Long projectId) {
        logger.debug("get all card contents of project #{}", projectId);

        Project project = assertAndGetProject(projectId);

        return cardManager.getAllCardContents(project.getRootCard());
    }

    /**
     * Get project whole structure
     *
     * @param projectId id of the project
     *
     * @return project structure
     */
    public ProjectStructure getStructure(Long projectId) {
        logger.debug("get all card contents of project #{}", projectId);

        Project project = assertAndGetProject(projectId);

        ProjectStructure structure = new ProjectStructure();
        Card rootCard = project.getRootCard();

        if (rootCard != null) {
            structure.setRootCardId(rootCard.getId());
        }

        structure.setCards(cardManager.getAllCards(rootCard));

        structure.setCardContents(cardManager.getAllCardContents(rootCard));

        return structure;
    }

    /**
     * Get all card types of the given project
     *
     * @param projectId the id of the project
     *
     * @return all card types of the project
     */
    public Set<AbstractCardType> getCardTypes(Long projectId) {
        logger.debug("get card types of project #{}", projectId);

        Project project = assertAndGetProject(projectId);

        return cardTypeManager.getExpandedProjectTypes(project);
    }

    /**
     * Get all activity flow links belonging to the given project
     *
     * @param projectId the id of the project
     *
     * @return all activityFlowLinks linked to the project
     */
    public Set<ActivityFlowLink> getActivityFlowLinks(Long projectId) {
        logger.debug("Get activity flow links of project #{}", projectId);

        Project project = assertAndGetProject(projectId);

        return cardManager
            .getAllCards(project.getRootCard())
            .stream().flatMap(card -> {
                return card.getActivityFlowLinksAsPrevious().stream();
            }).collect(Collectors.toSet());
    }

    // *********************************************************************************************
    // dedicated to access control
    // *********************************************************************************************

    /**
     * Retrieve the ids of the projects the current user is a member of.
     * <p>
     * We do not load the java objects. Just work with the ids. Two reasons : 1. the ids are enough,
     * so it is lighter + 2. prevent from loading object the user is not allowed to read
     *
     * @return the ids of the matching projects
     */
    public List<Long> findIdsOfProjectsCurrentUserIsMemberOf() {
        User user = securityManager.assertAndGetCurrentUser();

        List<Long> projectsIds = projectDao.findProjectsIdsByTeamMember(user.getId());

        logger.debug("found projects' id where the user {} is a team member : {}", user,
            projectsIds);

        return projectsIds;
    }

    /**
     * Retrieve the ids of the projects the current user is an instance maker for.
     * <p>
     * We do not load the java objects. Just work with the ids. Two reasons : 1. the ids are enough,
     * so it is lighter + 2. prevent from loading object the user is not allowed to read
     *
     * @return the ids of the matching projects
     */
    public List<Long> findIdsOfProjectsCurrentUserIsInstanceMakerFor() {
        User user = securityManager.assertAndGetCurrentUser();

        List<Long> projectsIds = projectDao.findProjectsIdsByInstanceMaker(user.getId());

        logger.debug("found projects' id for which the user {} is an instance maker : {}", user,
            projectsIds);

        return projectsIds;
    }

    /**
     * Retrieve the ids of the project for which the current user can read at least one local card
     * type or reference
     * <p>
     * We do not load the java objects. Just work with the ids. Two reasons : 1. the ids are enough,
     * so it is lighter + 2. prevent from loading object the user is not allowed to read
     *
     * @return the ids of the matching projects
     */
    public List<Long> findIdsOfProjectsReadableThroughCardTypes() {
        List<Long> cardTypeOrRefIds = cardTypeManager.findCurrentUserReadableProjectsCardTypesIds();

        List<Long> projectsIds = cardTypeManager.findProjectIdsFromCardTypeIds(cardTypeOrRefIds);

        logger.debug("found projects from readable card types {} : {}", cardTypeOrRefIds,
            projectsIds);

        return projectsIds;
    }

    /**
     * Do the two users have common project ?
     *
     * @param a one user
     * @param b another user
     *
     * @return true if both users are related to the same project
     */
    public boolean doUsersHaveCommonProject(User a, User b) {
        return projectDao.findIfUsersHaveCommonProject(a, b);
    }

    // *********************************************************************************************
    // integrity check
    // *********************************************************************************************

    /**
     * Check the integrity of the project
     *
     * @param project the project to check
     *
     * @return true iff the project is complete and safe
     */
    public boolean checkIntegrity(Project project) {
        if (project == null) {
            return false;
        }

        if (project.getRootCard() == null) {
            return false;
        }

        List<TeamMember> teamMembers = new ArrayList<>(project.getTeamMembers());
        if (teamMembers.stream().noneMatch(m -> m.getPosition() == HierarchicalPosition.OWNER)) {
            return false;
        }

        return true;
    }

}