TeamManager.java

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

import ch.colabproject.colab.api.controller.card.CardManager;
import ch.colabproject.colab.api.controller.project.ProjectManager;
import ch.colabproject.colab.api.controller.security.SecurityManager;
import ch.colabproject.colab.api.controller.token.TokenManager;
import ch.colabproject.colab.api.model.card.Card;
import ch.colabproject.colab.api.model.project.Project;
import ch.colabproject.colab.api.model.team.TeamMember;
import ch.colabproject.colab.api.model.team.TeamRole;
import ch.colabproject.colab.api.model.team.acl.HierarchicalPosition;
import ch.colabproject.colab.api.model.user.User;
import ch.colabproject.colab.api.persistence.jpa.team.TeamMemberDao;
import ch.colabproject.colab.api.persistence.jpa.team.TeamRoleDao;
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.List;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * Some logic to manage project teams
 *
 * @author maxence
 */
@Stateless
@LocalBean
public class TeamManager {

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

    /** Team members persistence */
    @Inject
    private TeamMemberDao teamMemberDao;

    /** Team roles persistence */
    @Inject
    private TeamRoleDao teamRoleDao;

    /** Project specific logic handling */
    @Inject
    private ProjectManager projectManager;

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

    /** Token Facade */
    @Inject
    private TokenManager tokenManager;

    /** Access control manager */
    @Inject
    private SecurityManager securityManager;

    // *********************************************************************************************
    // find team member
    // *********************************************************************************************

    /**
     * Retrieve the team member. If not found, throw a {@link HttpErrorMessage}.
     *
     * @param memberId the id of the team member
     *
     * @return the team member if found
     *
     * @throws HttpErrorMessage if the team member was not found
     */
    public TeamMember assertAndGetMember(Long memberId) {
        TeamMember member = teamMemberDao.findTeamMember(memberId);

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

        return member;
    }

    // *********************************************************************************************
    // team members
    // *********************************************************************************************
    /**
     * Add given user to the project teams
     *
     * @param project  the project
     * @param user     the user
     * @param position hierarchical position of the user
     *
     * @return the brand-new member
     */
    public TeamMember addMember(Project project, User user, HierarchicalPosition position) {
        logger.debug("Add member {} in {}", user, project);

        if (project == null) {
            throw HttpErrorMessage.dataError(MessageI18nKey.DATA_INTEGRITY_FAILURE);
        }

        if (user != null && findMemberByProjectAndUser(project, user) != null) {
            throw HttpErrorMessage.dataError(MessageI18nKey.DATA_INTEGRITY_FAILURE);
        }

        TeamMember teamMember = new TeamMember();

        teamMember.setUser(user);
        teamMember.setProject(project);
        teamMember.setPosition(position);

        teamMemberDao.persistTeamMember(teamMember);

        project.getTeamMembers().add(teamMember);

        return teamMember;
    }

    /**
     * Get the members of the given project
     *
     * @param projectId the id of the project
     *
     * @return list of team members
     */
    public List<TeamMember> getTeamMembersForProject(Long projectId) {
        logger.debug("Get team members of project #{}", projectId);

        Project project = projectManager.assertAndGetProject(projectId);

        return project.getTeamMembers();
    }

    /**
     * Get all members of the given project
     *
     * @param id id of the project
     *
     * @return all members of the project team
     */
    public List<TeamMember> getTeamMembers(Long id) {
        Project project = projectManager.assertAndGetProject(id);
        logger.debug("Get team members: {}", project);

        return project.getTeamMembers();
    }

    /**
     * Find the teamMember who match the given project and the given user.
     *
     * @param project the project
     * @param user    the user
     *
     * @return the teamMember or null
     */
    public TeamMember findMemberByProjectAndUser(Project project, User user) {
        return teamMemberDao.findMemberByProjectAndUser(project, user);
    }

    /**
     * Retrieve all users of the team members
     *
     * @param projectId the id of the project
     *
     * @return list of users
     */
    public List<User> getUsersForProject(Long projectId) {
        return getTeamMembersForProject(projectId).stream()
            .filter(m -> {
                return m.getUser() != null;
            })
            .map(m -> {
                return m.getUser();
            })
            .collect(Collectors.toList());
    }

    /**
     * Are two user teammate?
     *
     * @param a a user
     * @param b another user
     *
     * @return true if both user are both member of the same team
     */
    public boolean areUserTeammate(User a, User b) {
        return teamMemberDao.findIfUserAreTeammate(a, b);
    }

    /**
     * Update hierarchical position of a member
     *
     * @param memberId id of the member
     * @param position new hierarchical position
     */
    public void updatePosition(Long memberId, HierarchicalPosition position) {
        TeamMember member = teamMemberDao.findTeamMember(memberId);

        if (member != null && position != null) {
            if (position == HierarchicalPosition.OWNER
                || member.getPosition() == HierarchicalPosition.OWNER) {
                assertCurrentUserIsOwnerOfTheProject(member.getProject());
            }

            member.setPosition(position);
            assertTeamIntegrity(member.getProject());

        } else {
            throw HttpErrorMessage.dataError(MessageI18nKey.DATA_INTEGRITY_FAILURE);
        }
    }

    /**
     * Make sure the team of a project make sense. It means:
     * <ul>
     * <li>at least one "owner"
     * </ul>
     *
     * @param project the project to check
     *
     * @throws HttpErrorMessage if team is broken
     */
    public void assertTeamIntegrity(Project project) {

        if (project.getTeamMembersByPosition(HierarchicalPosition.OWNER).isEmpty()) {
            throw HttpErrorMessage.dataError(MessageI18nKey.DATA_INTEGRITY_FAILURE);
        }
    }

    /**
     * Make sure the current user is an owner of the given project
     *
     * @param project the project
     *
     * @throws HttpErrorMessage if not
     */
    private void assertCurrentUserIsOwnerOfTheProject(Project project) {
        if (!securityManager.isCurrentUserOwnerOfTheProject(project)) {
            throw HttpErrorMessage.dataError(MessageI18nKey.DATA_INTEGRITY_FAILURE);
        }
    }

    /**
     * Delete the given team member
     *
     * @param teamMemberId the id of the team member
     */
    public void deleteTeamMember(Long teamMemberId) {
        TeamMember teamMember = assertAndGetMember(teamMemberId);

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

        // assignments deleted by cascade

        // delete invitation token
        tokenManager.deleteInvitationsByTeamMember(teamMember);

        if (teamMember.getProject() != null) {
            teamMember.getProject().getTeamMembers().remove(teamMember);
        }

        teamMemberDao.deleteTeamMember(teamMember);
    }

    /**
     * Ascertain that the team member can be deleted
     *
     * @param teamMember the team member to check for deletion
     *
     * @return True iff it can be safely deleted
     */
    public boolean checkDeletionAcceptability(TeamMember teamMember) {
        if (teamMember.getPosition() == HierarchicalPosition.OWNER &&
            teamMember.getProject().getTeamMembersByPosition(HierarchicalPosition.OWNER)
                .size() < 2) {
            return false;
        }

        return true;
    }

    // *********************************************************************************************
    // Invitations and sharing
    // *********************************************************************************************

    /**
     * Send invitation
     *
     * @param projectId id of the project
     * @param email     send invitation to this address
     *
     * @return the pending new teamMember
     */
    public TeamMember invite(Long projectId, String email) {
        Project project = projectManager.assertAndGetProject(projectId);
        logger.debug("Invite {} to join {}", email, project);
        return tokenManager.sendMembershipInvitation(project, email);
    }

    /**
     * Create a token to share the project.
     *
     * @param projectId The id of the project that will become visible (mandatory)
     * @param cardId    The id of the card that will become editable (optional)
     *
     * @return the URL to use to consume the token
     */
    public String generateSharingLinkToken(Long projectId, Long cardId) {
        Project project = projectManager.assertAndGetProject(projectId);
        Card card = cardManager.assertAndGetCard(cardId);
        logger.debug("Generate sharing link token for project {} and card {}", project, card);
        return tokenManager.generateSharingLinkToken(project, card);
    }

    /**
     * Delete all sharing link tokens for the given project.
     *
     * @param projectId the id of the project
     */
    public void deleteSharingLinkTokensByProject(Long projectId) {
        Project project = projectManager.assertAndGetProject(projectId);
        logger.debug("Delete sharing link token for project {}", projectId);
        tokenManager.deleteSharingLinkTokensByProject(project);
    }

    /**
     * Delete all sharing link tokens for the given card.
     *
     * @param cardId the id of the card
     */
    public void deleteSharingLinkTokensByCard(Long cardId) {
        Card card = cardManager.assertAndGetCard(cardId);
        logger.debug("Delete sharing link token for card {}", cardId);
        tokenManager.deleteSharingLinkTokensByCard(card);
    }

    // *********************************************************************************************
    // Roles
    // *********************************************************************************************

    /**
     * Retrieve the role. If not found, throw a {@link HttpErrorMessage}.
     *
     * @param roleId the id of the role
     *
     * @return the role if found
     *
     * @throws HttpErrorMessage if the role was not found
     */
    public TeamRole assertAndGetRole(Long roleId) {
        TeamRole role = teamRoleDao.findRole(roleId);

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

        return role;
    }

    /**
     * Get all the roles defined in the given project
     *
     * @param id the project
     *
     * @return list of roles
     */
    public List<TeamRole> getProjectRoles(Long id) {
        Project project = projectManager.assertAndGetProject(id);
        return project.getRoles();
    }

    /**
     * Get the team roles defined in the given project
     *
     * @param projectId the id of the project
     *
     * @return list of team roles
     */
    public List<TeamRole> getTeamRolesForProject(Long projectId) {
        logger.debug("Get team roles of project #{}", projectId);

        Project project = projectManager.assertAndGetProject(projectId);

        return project.getRoles();
    }

    /**
     * Create a role. The role must have a projectId set.
     *
     * @param role role to create
     *
     * @return the brand new persisted role
     */
    public TeamRole createRole(TeamRole role) {
        if (role.getProjectId() != null) {
            Project project = projectManager.assertAndGetProject(role.getProjectId());
            if (project.getRoleByName(role.getName()) == null) {
                project.getRoles().add(role);
                role.setProject(project);
                return role;
            }
        }
        throw HttpErrorMessage.dataError(MessageI18nKey.DATA_INTEGRITY_FAILURE);
    }

    /**
     * Delete role
     *
     * @param roleId id of the role to delete
     */
    public void deleteRole(Long roleId) {
        TeamRole role = teamRoleDao.findRole(roleId);
        if (role != null) {
            Project project = role.getProject();
            if (project != null) {
                project.getRoles().remove(role);
            }
            role.getMembers().forEach(member -> {
                List<TeamRole> roles = member.getRoles();
                if (roles != null) {
                    roles.remove(role);
                }
            });
            teamRoleDao.deleteRole(role);
        }
    }

    /**
     * Give a role to someone. The role and the member must belong to the very same project.
     *
     * @param roleId   id of the role
     * @param memberId id of the teamMember
     */
    public void giveRole(Long roleId, Long memberId) {
        TeamRole role = teamRoleDao.findRole(roleId);
        TeamMember member = teamMemberDao.findTeamMember(memberId);
        if (role != null && member != null) {
            if (Objects.equals(role.getProject(), member.getProject())) {
                List<TeamMember> members = role.getMembers();
                List<TeamRole> roles = member.getRoles();
                if (!members.contains(member)) {
                    members.add(member);
                }
                if (!roles.contains(role)) {
                    roles.add(role);
                }
            } else {
                throw HttpErrorMessage.badRequest();
            }
        }
    }

    /**
     * Remove a role from someone.
     *
     * @param roleId   id of the role
     * @param memberId id of the member
     */
    public void removeRole(Long roleId, Long memberId) {
        TeamRole role = teamRoleDao.findRole(roleId);
        TeamMember member = teamMemberDao.findTeamMember(memberId);
        if (role != null && member != null) {
            List<TeamMember> members = role.getMembers();
            List<TeamRole> roles = member.getRoles();
            members.remove(member);
            roles.remove(role);
        }
    }
}