CardTypeManager.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.card;
import ch.colabproject.colab.api.controller.document.BlockManager;
import ch.colabproject.colab.api.controller.document.ResourceReferenceSpreadingHelper;
import ch.colabproject.colab.api.controller.project.ProjectManager;
import ch.colabproject.colab.api.controller.security.SecurityManager;
import ch.colabproject.colab.api.model.card.AbstractCardType;
import ch.colabproject.colab.api.model.card.CardType;
import ch.colabproject.colab.api.model.card.CardTypeRef;
import ch.colabproject.colab.api.model.document.TextDataBlock;
import ch.colabproject.colab.api.model.project.Project;
import ch.colabproject.colab.api.model.user.User;
import ch.colabproject.colab.api.persistence.jpa.card.CardTypeDao;
import ch.colabproject.colab.generator.model.exceptions.HttpErrorMessage;
import ch.colabproject.colab.generator.model.exceptions.MessageI18nKey;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import javax.ejb.LocalBean;
import javax.ejb.Stateless;
import javax.inject.Inject;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.ListUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.Lists;
/**
* Card type and reference specific logic
*
* @author maxence
* @author sandra
*/
@Stateless
@LocalBean
public class CardTypeManager {
/** logger */
private static final Logger logger = LoggerFactory.getLogger(CardTypeManager.class);
// *********************************************************************************************
// injections
// *********************************************************************************************
/**
* Access control manager
*/
@Inject
private SecurityManager securityManager;
/**
* Card type persistence handler
*/
@Inject
private CardTypeDao cardTypeDao;
/**
* Project specific logic management
*/
@Inject
private ProjectManager projectManager;
/**
* Block logic manager
*/
@Inject
private BlockManager blockManager;
/**
* Resource reference spreading specific logic handling
*/
@Inject
private ResourceReferenceSpreadingHelper resourceReferenceSpreadingHelper;
// *********************************************************************************************
// find card types and references
// *********************************************************************************************
/**
* Retrieve the card type (or reference). If not found, throw a {@link HttpErrorMessage}.
*
* @param cardTypeOrRefId the id of the card type (or reference)
*
* @return the card type (or reference) if found
*
* @throws HttpErrorMessage if the card type (or reference) was not found
*/
public AbstractCardType assertAndGetCardTypeOrRef(Long cardTypeOrRefId) {
AbstractCardType cardTypeOrRef = cardTypeDao.findAbstractCardType(cardTypeOrRefId);
if (cardTypeOrRef == null) {
logger.error("card type or reference #{} not found", cardTypeOrRefId);
throw HttpErrorMessage.dataError(MessageI18nKey.DATA_NOT_FOUND);
}
return cardTypeOrRef;
}
/**
* Retrieve the card type. If not found, throw a {@link HttpErrorMessage}.
*
* @param cardTypeId the id of the card type
*
* @return the card type if found
*
* @throws HttpErrorMessage if the card type was not found
*/
public CardType assertAndGetCardType(Long cardTypeId) {
AbstractCardType cardTypeOrRef = assertAndGetCardTypeOrRef(cardTypeId);
if (!(cardTypeOrRef instanceof CardType)) {
logger.error("#{} is not a card type", cardTypeId);
throw HttpErrorMessage.dataError(MessageI18nKey.DATA_NOT_FOUND);
}
return (CardType) cardTypeOrRef;
}
/**
* Retrieve the card type reference. If not found, throw a {@link HttpErrorMessage}.
*
* @param cardTypeRefId the id of the card type reference
*
* @return the card type reference if found
*
* @throws HttpErrorMessage if the card type reference was not found
*/
public CardTypeRef assertAndGetCardTypeRef(Long cardTypeRefId) {
AbstractCardType cardTypeOrRef = assertAndGetCardTypeOrRef(cardTypeRefId);
if (!(cardTypeOrRef instanceof CardTypeRef)) {
logger.error("#{} is not a card type reference", cardTypeRefId);
throw HttpErrorMessage.dataError(MessageI18nKey.DATA_NOT_FOUND);
}
return (CardTypeRef) cardTypeOrRef;
}
/**
* Expand project own types.
*
* @param project type owner
*
* @return set of concrete types and all transitive ref to reach them
*/
public Set<AbstractCardType> getExpandedProjectTypes(Project project) {
return this.expand(project.getElementsToBeDefined());
}
/**
* Expand all type that belong to a project and the current user has access to
*
* @return set of concrete types and all transitive ref to reach them
*/
public Set<AbstractCardType> getCurrentUserExpandedPublishedProjectTypes() {
User user = securityManager.assertAndGetCurrentUser();
return this.expand(cardTypeDao.findPublishedProjectCardTypes(user.getId()));
}
/**
* Get the abstract card type and expand it
*
* @param id the id of the wanted abstract card type
*
* @return the corresponding abstract card type and all its targets recursively until the card
* type
*/
public List<AbstractCardType> getExpandedCardType(Long id) {
AbstractCardType wanted = cardTypeDao.findAbstractCardType(id);
return wanted.expand();
}
/**
* Expand given types
*
* @param types to expand
*
* @return all types and all transitive ref to reach them
*/
private Set<AbstractCardType> expand(List<AbstractCardType> types) {
Set<AbstractCardType> allTypes = new HashSet<>();
types.forEach(type -> {
allTypes.addAll(type.expand());
});
return allTypes;
}
// *********************************************************************************************
// life cycle
// *********************************************************************************************
/**
* Complete and persist the given new card type.
* <p>
* The new type is a global type if it is not bound to any project.
*
* @param cardType the type to persist
*
* @return the new persisted card type
*/
public CardType createCardType(CardType cardType) {
Long projectId = cardType.getProjectId();
if (projectId != null) {
logger.debug("create a card type in the project #{}", projectId);
Project project = projectManager.assertAndGetProject(projectId);
project.getElementsToBeDefined().add(cardType);
cardType.setProject(project);
} else {
logger.debug("create a global card type");
cardType.setProject(null);
}
if (cardType.getPurpose() == null) {
TextDataBlock purposeTextDataBlock = blockManager.makeNewTextDataBlock();
cardType.setPurpose(purposeTextDataBlock);
purposeTextDataBlock.setPurposingCardType(cardType);
}
return cardTypeDao.persistAbstractCardType(cardType);
}
// TODO sandra work in progress - still need to be sharpened
/**
* If the card type is not already in the project, create a reference to it. Else simply return
* the card type.
*
* @param cardTypeId the id of the target card type
* @param projectId the id of the project in which we want to use the card type
*
* @return The reference to the card type
*/
public AbstractCardType useCardTypeInProject(Long cardTypeId, Long projectId) {
AbstractCardType cardTypeOrRef = assertAndGetCardTypeOrRef(cardTypeId);
Project project = projectManager.assertAndGetProject(projectId);
return computeEffectiveCardTypeOrRef(cardTypeOrRef, project);
}
/**
* Remove the card type use of the project. That means delete the reference in the project if it
* has no usage. If the abstract card type is used, throws an error.
*
* @param cardTypeId the id of the card type reference no more useful for the project
* @param projectId the id of the project in which we don't want to use the card type anymore
*/
public void removeCardTypeRefFromProject(Long cardTypeId, Long projectId) {
CardTypeRef cardTypeOrRef = assertAndGetCardTypeRef(cardTypeId);
Project project = projectManager.assertAndGetProject(projectId);
if (!(project.getElementsToBeDefined().contains(cardTypeOrRef))) {
// the job is already done
return;
}
deleteCardTypeOrRef(cardTypeOrRef);
}
/**
* Delete the given card type
*
* @param cardTypeId the id of the card type to delete
*/
public void deleteCardType(Long cardTypeId) {
CardType cardType = assertAndGetCardType(cardTypeId);
deleteCardTypeOrRef(cardType);
}
/**
* Delete the given card type
*
* @param cardTypeId the id of the card type to delete
*/
private void deleteCardTypeOrRef(AbstractCardType cardTypeOrRef) {
if (!checkDeletionAcceptability(cardTypeOrRef)) {
throw HttpErrorMessage.dataError(MessageI18nKey.DATA_INTEGRITY_FAILURE);
}
if (cardTypeOrRef.getProject() != null) {
cardTypeOrRef.getProject().getElementsToBeDefined().remove(cardTypeOrRef);
}
cardTypeDao.deleteAbstractCardType(cardTypeOrRef);
}
/**
* Ascertain that the card type (or reference) can be deleted.
*
* @param cardTypeOrRef the card type (or reference) to check for deletion
*
* @return True iff it can be safely deleted
*/
private boolean checkDeletionAcceptability(AbstractCardType cardTypeOrRef) {
if (CollectionUtils.isNotEmpty(cardTypeOrRef.getImplementingCards())) {
return false;
}
if (CollectionUtils.isNotEmpty(cardTypeDao.findDirectReferences(cardTypeOrRef))) {
return false;
}
return true;
}
// *********************************************************************************************
// reference handling
// *********************************************************************************************
/**
* Get a card type located in the given project and targeting the given card type.
*
* @param cardType the target card type
* @param project the project the card type is located
*
* @return the unique card type
*/
public AbstractCardType computeEffectiveCardTypeOrRef(AbstractCardType cardType,
Project project) {
// The given type belongs to the project
// it can be used as-is
if (project.equals(cardType.getProject())) {
return cardType;
}
// So, cardType belongs to another project or is global
// it must be accessed via a reference belonging to the given project
// Check if the project already got a direct reference the super-type
// shall we check something to prevent extending same type several times?
Optional<AbstractCardType> directRefToCardTypeInProject = project.getElementsToBeDefined()
.stream()
.filter(type -> {
return isDirectRef(type, cardType);
})
.findFirst();
// direct reference found, reuse it
if (directRefToCardTypeInProject.isPresent()) {
return directRefToCardTypeInProject.get();
}
// no direct reference. Create one.
return createNewCardReference(cardType, project);
}
/**
* Is the given child a direct reference to the given parent.
*
* @param child the child
* @param parent the parent
*
* @return true if child is a direct reference to the parent
*/
private boolean isDirectRef(AbstractCardType child, AbstractCardType parent) {
if (child instanceof CardTypeRef) {
CardTypeRef ref = (CardTypeRef) child;
AbstractCardType refTarget = ref.getTarget();
return Objects.equals(refTarget, parent);
}
return false;
}
/**
* Complete and persist a new type reference to the given type. The ref will belongs to the
* given project.
*
* @param cardType the type to reference
* @param project the reference owner
*
* @return a new, initialized card type reference (just the object, no persistence)
*
* @throws HttpErrorMessage if we try to create a reference in the same project than the target
*/
private CardTypeRef createNewCardReference(AbstractCardType cardType, Project project) {
if (cardType.getProjectId() == project.getId()) {
throw HttpErrorMessage.dataError(MessageI18nKey.DATA_INTEGRITY_FAILURE);
}
CardTypeRef ref = initNewCardTypeRef();
ref.setProject(project);
project.getElementsToBeDefined().add(ref);
ref.setTarget(cardType);
resourceReferenceSpreadingHelper.extractReferencesFromUp(ref);
return cardTypeDao.persistAbstractCardType(ref);
}
/**
* Initialize a new card type reference
*
* @return a new, initialized card type reference (just the object, no persistence)
*/
private CardTypeRef initNewCardTypeRef() {
CardTypeRef ref = new CardTypeRef();
ref.setDeprecated(false);
ref.setPublished(false);
return ref;
}
/**
* Is the given ancestor an ancestor of the given child
*
* @param child the child
* @param ancestor the ancestor
*
* @return true if child is a ref and if ancestor is an ancestor of the ref
*/
public boolean isTransitiveRef(AbstractCardType child, AbstractCardType ancestor) {
if (isDirectRef(child, ancestor)) {
return true;
} else if (child instanceof CardTypeRef) {
AbstractCardType parent = ((CardTypeRef) child).getTarget();
return this.isTransitiveRef(parent, ancestor);
}
return false;
}
// *********************************************************************************************
// dedicated to access control
// *********************************************************************************************
/**
* Retrieve the ids of the global (not in a project) published card types.
*
* @return the ids of the matching card types
*/
public List<Long> findGlobalPublishedCardTypeIds() {
return cardTypeDao.findIdsOfPublishedGlobalCardTypes();
}
/**
* Retrieve the id of the all the card types the current user can read.
* <p>
* That means every card type (or reference) owned by a project the current user is member of
* and all the targets of these.
*
* @return the ids of the matching card types or references
*/
public List<Long> findCurrentUserReadableProjectsCardTypesIds() {
List<Long> result = Lists.newArrayList();
List<Long> directInOwnProjects = findCurrentUserDirectProjectsCardTypesIds();
List<Long> above = retrieveDirectAndTransitiveAboveCardTypesOrRefs(directInOwnProjects);
List<Long> below = retrieveDirectAndTransitiveBelowCardTypesOrRefs(directInOwnProjects);
result.addAll(directInOwnProjects);
result.addAll(above);
result.addAll(below);
return result;
}
/**
* Retrieve the ids of the card types (or references) owned by a project the current user is a
* member of
*
* @return the ids of the matching card types or references
*/
private List<Long> findCurrentUserDirectProjectsCardTypesIds() {
User user = securityManager.assertAndGetCurrentUser();
List<Long> cardTypeOrRefIds = cardTypeDao.findIdsOfProjectCardType(user.getId());
logger.debug("found direct project's card types' id : {} ", cardTypeOrRefIds);
return cardTypeOrRefIds;
}
/**
* Retrieve the ids of the card types or references with their direct and transitive targets.
*
* @param cardTypeOrRefIds
*
* @return the ids of the matching card types or references
*/
private List<Long> retrieveDirectAndTransitiveAboveCardTypesOrRefs(
List<Long> cardTypeOrRefIds) {
return retrieveDirectAndTransitiveAboveCardTypesOrRefs(cardTypeOrRefIds,
Lists.newArrayList());
}
/**
* Retrieve the ids of the card types or references with their direct and transitive targets.
*
* @param toProcess the ids of card types and references
* @param alreadyDone the already processed ids
*
* @return the ids of the matching card types or references
*/
private List<Long> retrieveDirectAndTransitiveAboveCardTypesOrRefs(List<Long> toProcess,
List<Long> alreadyDone) {
List<Long> remainsToProcess = ListUtils.removeAll(toProcess, alreadyDone);
if (CollectionUtils.isEmpty(remainsToProcess)) {
return alreadyDone;
}
alreadyDone.addAll(remainsToProcess);
List<Long> directTargets = findDirectTargets(remainsToProcess);
retrieveDirectAndTransitiveAboveCardTypesOrRefs(directTargets, alreadyDone);
return alreadyDone;
}
/**
* Retrieve the ids of the direct target of each card type or reference
*
* @param cardTypeOrRefIds the ids of card types and references
*
* @return the ids of the matching card types or references
*/
private List<Long> findDirectTargets(List<Long> cardTypeOrRefIds) {
List<Long> result = cardTypeDao.findTargetIdsOf(cardTypeOrRefIds);
logger.debug("found targets : {} ", result);
return result;
}
/**
* Retrieve the ids of the card types or references with their direct and transitive references.
*
* @param cardTypeOrRefIds
*
* @return the ids of the matching card types or references
*/
private List<Long> retrieveDirectAndTransitiveBelowCardTypesOrRefs(
List<Long> cardTypeOrRefIds) {
return retrieveDirectAndTransitiveBelowCardTypesOrRefs(cardTypeOrRefIds,
Lists.newArrayList());
}
/**
* Retrieve the ids of the card types or references with their direct and transitive references.
*
* @param toProcess the ids of card types and references
* @param alreadyDone the already processed ids
*
* @return the ids of the matching card types or references
*/
private List<Long> retrieveDirectAndTransitiveBelowCardTypesOrRefs(List<Long> toProcess,
List<Long> alreadyDone) {
List<Long> remainsToProcess = ListUtils.removeAll(toProcess, alreadyDone);
if (CollectionUtils.isEmpty(remainsToProcess)) {
return alreadyDone;
}
alreadyDone.addAll(remainsToProcess);
List<Long> directRefs = findDirectRefs(remainsToProcess);
retrieveDirectAndTransitiveBelowCardTypesOrRefs(directRefs, alreadyDone);
return alreadyDone;
}
/**
* Retrieve the ids of the direct references of each card type or reference
*
* @param cardTypeOrRefIds the ids of card types and references
*
* @return the ids of the matching card types or references
*/
private List<Long> findDirectRefs(List<Long> cardTypeOrRefIds) {
List<Long> result = cardTypeDao.findDirectReferencesIdsOf(cardTypeOrRefIds);
logger.debug("found refs : {}", result);
return result;
}
/**
* Retrieve the ids of the projects owning any card type or reference.
*
* @param cardTypeOrRefIds the ids of card types and references
*
* @return the ids of the matching projects
*/
public List<Long> findProjectIdsFromCardTypeIds(List<Long> cardTypeOrRefIds) {
return cardTypeDao.findProjectIdsOf(cardTypeOrRefIds);
}
// *********************************************************************************************
// integrity check
// *********************************************************************************************
/**
* Check the integrity of the card type (or reference)
*
* @param cardTypeOrRef the card type (or reference) to check
*
* @return true iff the project is complete and safe
*/
public boolean checkIntegrity(AbstractCardType cardTypeOrRef) {
if (cardTypeOrRef == null) {
return false;
}
if (cardTypeOrRef instanceof CardTypeRef) {
CardTypeRef reference = (CardTypeRef) cardTypeOrRef;
CardType finalTarget = reference.resolve();
if (finalTarget == null) {
return false;
}
}
// just one ref by target/project
return true;
}
// *********************************************************************************************
//
// *********************************************************************************************
}