CardContentManager.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.card;

import ch.colabproject.colab.api.controller.common.DeletionManager;
import ch.colabproject.colab.api.controller.document.DocumentManager;
import ch.colabproject.colab.api.controller.document.IndexGeneratorHelper;
import ch.colabproject.colab.api.controller.document.RelatedPosition;
import ch.colabproject.colab.api.controller.document.ResourceReferenceSpreadingHelper;
import ch.colabproject.colab.api.controller.security.SecurityManager;
import ch.colabproject.colab.api.model.card.Card;
import ch.colabproject.colab.api.model.card.CardContent;
import ch.colabproject.colab.api.model.card.CardContentStatus;
import ch.colabproject.colab.api.model.common.ConversionStatus;
import ch.colabproject.colab.api.model.document.Document;
import ch.colabproject.colab.api.model.link.StickyNoteLink;
import ch.colabproject.colab.api.persistence.jpa.card.CardContentDao;
import ch.colabproject.colab.api.persistence.jpa.document.DocumentDao;
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;

/**
 * Card content specific logic
 *
 * @author sandra
 */
@Stateless
@LocalBean
public class CardContentManager {

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

    /**
     * Minimal completion level
     */
    private static final int MIN_COMPLETION_LEVEL = 0;

    /**
     * Maximal completion level
     */
    private static final int MAX_COMPLETION_LEVEL = 100;

    /**
     * Initial card status
     */
    private static final CardContentStatus CARD_CONTENT_INITIAL_STATUS = null;

    /**
     * Default value for frozen status
     */
    private static final boolean FROZEN_DEFAULT = false;

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

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

    /**
     * Card content persistence handler
     */
    @Inject
    private CardContentDao cardContentDao;

    /**
     * Document persistence handling
     */
    @Inject
    private DocumentDao documentDao;

    /**
     * Document specific logic
     */
    @Inject
    private DocumentManager documentManager;

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

    /**
     * Index generation specific logic management
     */
    @Inject
    private IndexGeneratorHelper<Document> indexGenerator;

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

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

    // *********************************************************************************************
    // find card contents
    // *********************************************************************************************

    /**
     * Retrieve the card content. If not found, throw a {@link HttpErrorMessage}.
     *
     * @param cardContentId the id of the card content
     *
     * @return the card content if found
     *
     * @throws HttpErrorMessage if the card content was not found
     */
    public CardContent assertAndGetCardContent(Long cardContentId) {
        CardContent cardContent = cardContentDao.findCardContent(cardContentId);

        if (cardContent == null) {
            logger.error("card content #{} not found", cardContentId);
            throw HttpErrorMessage.dataError(MessageI18nKey.DATA_NOT_FOUND);
        }

        return cardContent;
    }

    /**
     * Retrieve the grand parent card content of the given card.
     *
     * @param card the card
     *
     * @return the grand parent card content of the given card
     *
     * @throws HttpErrorMessage if any of the level was not found
     */
    public CardContent assertAndGetGrandParentCardContent(Card card) {
        if (card == null) {
            logger.error("card {} is null", card);
            throw HttpErrorMessage.dataError(MessageI18nKey.DATA_NOT_FOUND);
        }

        CardContent parentCardContent = card.getParent();
        if (parentCardContent == null) {
            logger.error("parent card content of card {} is null", card);
            throw HttpErrorMessage.dataError(MessageI18nKey.DATA_INTEGRITY_FAILURE);
        }

        Card parentCard = parentCardContent.getCard();
        if (parentCard == null) {
            logger.error("parent card of card {} is null", card);
            throw HttpErrorMessage.dataError(MessageI18nKey.DATA_INTEGRITY_FAILURE);
        }

        CardContent grandParentCardContent = parentCard.getParent();
        if (grandParentCardContent == null) {
            logger.error("grand parent card content of card {} is null", card);
            throw HttpErrorMessage.dataError(MessageI18nKey.DATA_INTEGRITY_FAILURE);
        }

        return grandParentCardContent;

    }

    /**
     * Get the card content identified by the given id
     *
     * @param id id of the card to access
     *
     * @throws HttpErrorMessage if the document was not found or access denied
     */
    public void assertCardContentReadWrite(Long id) {
        logger.debug("get document #{}", id);
        CardContent cardContent = assertAndGetCardContent(id);
        if (cardContent == null) {
            throw HttpErrorMessage.dataError(MessageI18nKey.DATA_NOT_FOUND);
        }
        securityManager.assertUpdatePermissionTx(cardContent);
    }

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

    /**
     * Complete and persist a new card content variant for the given card.
     * <p>
     * Also create its default resource references.
     *
     * @param cardId the id of the card needing a new card content variant
     *
     * @return a new, initialized and persisted card content
     */
    public CardContent createNewCardContent(Long cardId) {
        logger.debug("create a new card content for the card #{}", cardId);

        Card card = cardManager.assertAndGetCard(cardId);

        CardContent cardContent = initNewCardContentForCard(card);

        resourceReferenceSpreadingHelper.extractReferencesFromUp(cardContent);

        return cardContentDao.persistCardContent(cardContent);
    }

    /**
     * Initialize a new card content which will be a content of the given card.
     *
     * @param card the card needing a new card content
     *
     * @return a new, initialized card content (just the object, no persistence)
     */
    public CardContent initNewCardContentForCard(Card card) {
        CardContent cardContent = new CardContent();

        resetProgression(cardContent);

        cardContent.setCard(card);
        card.getContentVariants().add(cardContent);

        return cardContent;
    }

    /**
     * Reset progression data of the given card content : status, completion level and frozen
     *
     * @param cardContent the card content
     */
    public void resetProgression(CardContent cardContent) {
        cardContent.setStatus(CARD_CONTENT_INITIAL_STATUS);
        cardContent.setCompletionLevel(MIN_COMPLETION_LEVEL);
        cardContent.setFrozen(FROZEN_DEFAULT);
    }

    /**
     * Set the lexical conversion status.
     *
     * @param id     the id of the card content
     * @param status the new lexical conversion status to set
     */
    public void changeCardContentLexicalConversionStatus(Long id, ConversionStatus status) {
        logger.debug("change lexical conversion status to {} for card content #{}", status, id);

        CardContent cardContent = assertAndGetCardContent(id);

        cardContent.setLexicalConversion(status);
    }

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

        CardContent cardContent = assertAndGetCardContent(cardContentId);

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

        deletionManager.putInBin(cardContent);
    }

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

        CardContent cardContent = assertAndGetCardContent(cardContentId);

        deletionManager.restoreFromBin(cardContent);
    }

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

        CardContent cardContent = assertAndGetCardContent(cardContentId);

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

        deletionManager.markAsToDeleteForever(cardContent);
    }

    /**
     * Delete the given card content
     *
     * @param cardContentId the id of the card content to delete
     */
    public void deleteCardContent(Long cardContentId) {
        CardContent cardContent = assertAndGetCardContent(cardContentId);

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

        cardContent.getCard().getContentVariants().remove(cardContent);

        cardContentDao.deleteCardContent(cardContent);
    }

    /**
     * Ascertain that the card content can be deleted.
     *
     * @param cardContent the card content to check for deletion
     *
     * @return True iff it can be safely deleted
     */
    private boolean checkDeletionAcceptability(CardContent cardContent) {
        // A card must have at least one other alive card content
        if (cardContent.getCard().getContentVariants()
                .stream()
                .filter(content -> !Objects.equals(content, cardContent))
                .noneMatch(content -> deletionManager.isAlive(content))
        ) {
            return false;
        }

        return true;
    }

    // *********************************************************************************************
    // add a deliverable to a card content
    // *********************************************************************************************

    /**
     * Add the deliverable to the end of the card content.
     *
     * @param cardContentId the id of the card content
     * @param document      the document to use as deliverable. It must be a new document
     *
     * @return the newly created document
     */
    public Document addDeliverable(Long cardContentId, Document document) {
        logger.debug("add deliverable {} to card content #{}", document, cardContentId);

        return addDeliverable(cardContentId, document, RelatedPosition.AT_END);
    }

    /**
     * Add the deliverable to the card content. It will be placed on the given relatedPosition.
     *
     * @param cardContentId   the id of the card content
     * @param document        the document to use as deliverable. It must be a new document
     * @param relatedPosition to define the place where the deliverable will be added in the card
     *                        content
     *
     * @return the newly created document
     */
    public Document addDeliverable(Long cardContentId, Document document,
        RelatedPosition relatedPosition) {
        logger.debug("add deliverable {} {} to card content #{}", document, relatedPosition,
            cardContentId);

        return addDeliverable(cardContentId, document, relatedPosition, null);
    }

    /**
     * Add the deliverable to the card content.
     *
     * @param cardContentId   the id of the card content
     * @param document        the document to use as deliverable. It must be a new document
     * @param relatedPosition to define the place where the deliverable will be added in the card
     *                        content
     * @param neighbourDocId  the existing document which defines where the new document will be
     *                        set. If relatedPosition is BEFOR or AFTER, it must be not null
     *
     * @return the newly created document
     */
    public Document addDeliverable(Long cardContentId, Document document,
        RelatedPosition relatedPosition, Long neighbourDocId) {
        logger.debug("add deliverable {} to card content #{} {} doc #{}", document, cardContentId,
            relatedPosition, neighbourDocId);

        CardContent cardContent = assertAndGetCardContent(cardContentId);

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

        if (document.hasOwningResource() || document.hasOwningCardContent()) {
            throw HttpErrorMessage.dataError(MessageI18nKey.DATA_INTEGRITY_FAILURE);
        }

        if (cardContent.getDeliverables().contains(document)) {
            throw HttpErrorMessage.dataError(MessageI18nKey.DATA_INTEGRITY_FAILURE);
        }

        switch (relatedPosition) {
            case BEFORE:
                Document neighbourBDocument = documentManager.assertAndGetDocument(neighbourDocId);
                indexGenerator.moveItemBefore(document, neighbourBDocument,
                    cardContent.getDeliverables());
                break;
            case AFTER:
                Document neighbourADocument = documentManager.assertAndGetDocument(neighbourDocId);
                indexGenerator.moveItemAfter(document, neighbourADocument,
                    cardContent.getDeliverables());
                break;
            case AT_BEGINNING:
                indexGenerator.moveItemToBeginning(document, cardContent.getDeliverables());
                break;
            case AT_END:
            default:
                indexGenerator.moveItemToEnd(document, cardContent.getDeliverables());
                break;
        }

        cardContent.getDeliverables().add(document);
        document.setOwningCardContent(cardContent);

        return documentDao.persistDocument(document);
    }

    /**
     * Remove the deliverable of the card content.
     *
     * @param cardContentId the id of the card content
     * @param documentId    the id of the document to remove from the card content
     */
    public void removeDeliverable(Long cardContentId, Long documentId) {
        logger.debug("remove deliverable #{} of card content #{}", documentId, cardContentId);

        CardContent cardContent = assertAndGetCardContent(cardContentId);

        Document document = documentManager.assertAndGetDocument(documentId);

        if (!(cardContent.getDeliverables().contains(document))) {
            throw HttpErrorMessage.dataError(MessageI18nKey.DATA_INTEGRITY_FAILURE);
        }

        cardContent.getDeliverables().remove(document);

        documentDao.deleteDocument(document);
    }

    // *********************************************************************************************
    // retrieve the elements of a card content
    // *********************************************************************************************

    /**
     * Get all sub cards of a given card content
     *
     * @param cardContentId the id of the card content
     *
     * @return all cards of the card content
     */
    public List<Card> getSubCards(Long cardContentId) {
        logger.debug("get all sub cards of card content #{}", cardContentId);

        CardContent cardContent = assertAndGetCardContent(cardContentId);

        return cardContent.getSubCards();
    }

    /**
     * Get the deliverables of the card content
     *
     * @param cardContentId the id of the card content
     *
     * @return the deliverables linked to the card content
     */
    public List<Document> getDeliverablesOfCardContent(Long cardContentId) {
        logger.debug("get deliverables of card content #{}", cardContentId);

        CardContent cardContent = assertAndGetCardContent(cardContentId);

        return cardContent.getDeliverables();
    }

    /**
     * Get all sticky note links whose source is the given card content
     *
     * @param cardContentId the id of the card content
     *
     * @return all sticky note links linked to the card content
     */
    public List<StickyNoteLink> getStickyNoteLinkAsSrcCardContent(Long cardContentId) {
        logger.debug("get sticky note links where the card content #{} is the source",
            cardContentId);

        CardContent cardContent = assertAndGetCardContent(cardContentId);

        return cardContent.getStickyNoteLinksAsSrc();
    }

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

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

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

        if (cardContent.getCard() == null) {
            return false;
        }

        if (cardContent.getCompletionLevel() < MIN_COMPLETION_LEVEL) {
            return false;
        }

        if (cardContent.getCompletionLevel() > MAX_COMPLETION_LEVEL) {
            return false;
        }

        return true;
    }

    // *********************************************************************************************
    //
    // *********************************************************************************************
}