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;
}
}