ResourceManager.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.document;

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.project.ProjectManager;
import ch.colabproject.colab.api.controller.security.SecurityManager;
import ch.colabproject.colab.api.exceptions.ColabMergeException;
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.common.ConversionStatus;
import ch.colabproject.colab.api.model.document.AbstractResource;
import ch.colabproject.colab.api.model.document.Document;
import ch.colabproject.colab.api.model.document.Resource;
import ch.colabproject.colab.api.model.document.ResourceRef;
import ch.colabproject.colab.api.model.document.Resourceable;
import ch.colabproject.colab.api.model.document.TextDataBlock;
import ch.colabproject.colab.api.model.link.StickyNoteLink;
import ch.colabproject.colab.api.model.project.Project;
import ch.colabproject.colab.api.persistence.jpa.document.DocumentDao;
import ch.colabproject.colab.api.persistence.jpa.document.ResourceDao;
import ch.colabproject.colab.api.rest.document.bean.ResourceExternalReference;
import ch.colabproject.colab.generator.model.exceptions.HttpErrorMessage;
import ch.colabproject.colab.generator.model.exceptions.MessageI18nKey;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import javax.ejb.LocalBean;
import javax.ejb.Stateless;
import javax.inject.Inject;
import org.apache.commons.collections4.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Resource and resource reference specific logic
 *
 * @author sandra
 */
// TODO requestingForGlory handling
@Stateless
@LocalBean
public class ResourceManager {

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

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

    /**
     * Resource / resource reference persistence handling
     */
    @Inject
    private ResourceDao resourceDao;

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

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

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

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

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

    /**
     * Block specific logic management
     */
    @Inject
    private BlockManager blockManager;

    /**
     * Project specific logic management
     */
    @Inject
    private ProjectManager projectManager;
    /**
     * TO sudo
     */
    @Inject
    private RequestManager requestManager;

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

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

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

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

    // *********************************************************************************************
    // find resource
    // *********************************************************************************************

    /**
     * Retrieve the resource (or reference). If not found, throw a {@link HttpErrorMessage}.
     *
     * @param resourceOrRefId the id of the resource or reference
     *
     * @return the resource or reference if found
     *
     * @throws HttpErrorMessage if the resource or reference was not found
     */
    public AbstractResource assertAndGetResourceOrRef(Long resourceOrRefId) {
        AbstractResource resourceOrRef = resourceDao.findResourceOrRef(resourceOrRefId);

        if (resourceOrRef == null) {
            logger.error("resource or reference #{} not found", resourceOrRefId);
            throw HttpErrorMessage.dataError(MessageI18nKey.DATA_NOT_FOUND);
        }

        return resourceOrRef;
    }

    /**
     * Retrieve the resource. If not found, throw a {@link HttpErrorMessage}.
     *
     * @param resourceId the id of the resource
     *
     * @return the resource if found
     *
     * @throws HttpErrorMessage if the resource was not found
     */
    public Resource assertAndGetResource(Long resourceId) {
        AbstractResource abstractResource = assertAndGetResourceOrRef(resourceId);

        if (!(abstractResource instanceof Resource)) {
            throw HttpErrorMessage.dataError(MessageI18nKey.DATA_NOT_FOUND);
        }

        return (Resource) abstractResource;
    }

           /**
     * 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 assertResourceReadWrite(Long id) {
        logger.debug("get document #{}", id);
        Resource resource = assertAndGetResource(id);
        if (resource == null) {
            throw HttpErrorMessage.dataError(MessageI18nKey.DATA_NOT_FOUND);
        }
        securityManager.assertUpdatePermissionTx(resource);
    }

    /**
     * Retrieve the resource reference. If not found, throw a {@link HttpErrorMessage}.
     *
     * @param resourceRefId the id of the resource reference
     *
     * @return the resource reference if found
     *
     * @throws HttpErrorMessage if the resource reference was not found
     */
    public ResourceRef assertAndGetResourceRef(Long resourceRefId) {
        AbstractResource abstractResource = assertAndGetResourceOrRef(resourceRefId);

        if (!(abstractResource instanceof ResourceRef)) {
            throw HttpErrorMessage.dataError(MessageI18nKey.DATA_NOT_FOUND);
        }

        return (ResourceRef) abstractResource;

    }

    // *********************************************************************************************
    // find all resources for a card type / card / card content
    // *********************************************************************************************

    /**
     * Retrieve the abstract resources directly linked to an abstract card type and all the chain of
     * references to the concrete resource
     *
     * @param cardTypeOrRefId The id of the card type or reference
     *
     * @return For each direct abstract resource, the chain of abstract resources to the resource
     */
    public List<List<AbstractResource>> getExpandedResourcesForAbstractCardType(
        Long cardTypeOrRefId) {
        logger.debug("get expanded resources linked to abstract card type #{}", cardTypeOrRefId);

        AbstractCardType cardTypeOrRef = cardTypeManager.assertAndGetCardTypeOrRef(cardTypeOrRefId);

        List<AbstractResource> directResourcesAndRefs = cardTypeOrRef.getDirectAbstractResources();

        return expandCompleteChains(directResourcesAndRefs);
    }

    /**
     * Retrieve the abstract resources directly linked to a card and all the chain of references to
     * the concrete resource
     *
     * @param cardId The id of the card
     *
     * @return For each direct abstract resource, the chain of abstract resources to the resource
     */
    public List<List<AbstractResource>> getExpandedResourcesForCard(Long cardId) {
        logger.debug("get expanded resources linked to card #{}", cardId);

        Card card = cardManager.assertAndGetCard(cardId);

        List<AbstractResource> directResourcesAndRefs = card.getDirectAbstractResources();

        return expandCompleteChains(directResourcesAndRefs);
    }

    /**
     * Retrieve the abstract resources directly linked to a card content and all the chain of
     * references to the concrete resource
     *
     * @param cardContentId The id of the card content
     *
     * @return For each direct abstract resource, the chain of abstract resources to the resource
     */
    public List<List<AbstractResource>> getExpandedResourcesForCardContent(Long cardContentId) {
        logger.debug("get expanded resources linked to card content #{}", cardContentId);

        CardContent cardContent = cardContentManager.assertAndGetCardContent(cardContentId);

        List<AbstractResource> directResourcesAndRefs = cardContent.getDirectAbstractResources();

        return expandCompleteChains(directResourcesAndRefs);
    }

    /**
     * @param directResourcesAndRefs List of resources or references
     *
     * @return for each resource / resource reference, retrieve the all chain from it until a
     *         concrete resource
     */
    private List<List<AbstractResource>> expandCompleteChains(
        List<AbstractResource> directResourcesAndRefs) {
        return directResourcesAndRefs
            .stream()
            .map(resourceOrRef -> resourceOrRef.expand())
            .collect(Collectors.toList());
    }

    // *********************************************************************************************
    // find all direct resources in the project
    // *********************************************************************************************

    /**
     * Get the resources directly linked to the given project.
     * <p>
     * Does not fetch all chain references.
     *
     * @param projectId the id of the project
     *
     * @return resources directly linked to the given project
     */
    public List<AbstractResource> getDirectAbstractResourcesOfProject(Long projectId) {
        logger.debug("get all the resources directly linked to the project #{}", projectId);

        Project project = projectManager.assertAndGetProject(projectId);

        List<Resourceable> allResourceables = new ArrayList<>();

        allResourceables.addAll(project.getElementsToBeDefined());
        allResourceables.addAll(projectManager.getCards(projectId));
        allResourceables.addAll(projectManager.getCardContents(projectId));

        List<AbstractResource> allDirectResources = new ArrayList<>();

        allDirectResources.addAll(allResourceables.stream().flatMap(card -> {
            return card.getDirectAbstractResources().stream();
        }).collect(Collectors.toList()));

        return allDirectResources;
    }

    /**
     * Get the list of project which reference the given resource, excluding the project which owns
     * the resource.
     *
     * @param abstractResourceId if of the targeted resource
     *
     * @return list of externalReference
     */
    public List<ResourceExternalReference> getResourceExternalReferences(Long abstractResourceId) {
        AbstractResource resource = resourceDao.findResourceOrRef(abstractResourceId);
        Project owner = resource.getProject();

        HashMap<Project, List<ResourceRef>> perProject = new HashMap<>();

        resourceDao.findAllReferences(resource)
            .stream()
            .filter(res -> {
                // only keep ones not linked to current project
                return res != null && !res.getProject().equals(owner);
            })
            .forEach(res -> {
                Project p = res.getProject();
                if (p != null) {
                    // group resource by project
                    var list = perProject.get(res.getProject());
                    if (list == null) {
                        list = new ArrayList<>();
                        perProject.put(p, list);
                    }
                    list.add(res);
                }
            });

        return perProject.entrySet().stream().map(entry -> {
            Project p = entry.getKey();

            ResourceExternalReference extRef = new ResourceExternalReference();
            extRef.setProject(p);

            List<ResourceRef> leaves = entry.getValue().stream().filter(res -> {
                return res.getCardContentId() != null || res.getCardId() != null;
            }).collect(Collectors.toList());

            if (leaves.isEmpty()) {
                // even if the project references the resource through the cardType,
                // not a single card or card content references the resource
                extRef.setUsage(ResourceExternalReference.Usage.UNUSED);
                return extRef;
            }

            // if at least one leaf is not refused, this is globally not refused
            boolean used = leaves.stream().anyMatch(res -> {
                return !res.isRefused();
            });

            if (used) {
                extRef.setUsage(ResourceExternalReference.Usage.USED);
            } else {
                extRef.setUsage(ResourceExternalReference.Usage.REFUSED);
            }

            return extRef;
        }).collect(Collectors.toList());
    }

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

    /**
     * Complete and persist the given resource and create all the references needed.
     * <p>
     * Every child of the entity acquires a reference to that resource.<br>
     * And recursively the grand children acquires a reference to the reference of their parent.
     *
     * @param resource the resource to create
     *
     * @return the brand new resource
     */
    public Resource createResource(Resource resource) {
        logger.debug("create resource {}", resource);

        if (resource.getTeaser() == null) {
            TextDataBlock teaserTextDataBlock = blockManager.makeNewTextDataBlock();

            resource.setTeaser(teaserTextDataBlock);
            teaserTextDataBlock.setTeasingResource(resource);
        }

        // implicitly resource.setPublished(false);
        // implicitly resource.setRequestingForGlory(false);
        // implicitly resource.setDeprecated(false);

        Resourceable owner;
        if (resource.getAbstractCardTypeId() != null) {
            owner = cardTypeManager.assertAndGetCardTypeOrRef(resource.getAbstractCardTypeId());

        } else if (resource.getCardId() != null) {
            owner = cardManager.assertAndGetCard(resource.getCardId());

        } else if (resource.getCardContentId() != null) {
            owner = cardContentManager.assertAndGetCardContent(resource.getCardContentId());

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

        resource.setOwner(owner);
        owner.getDirectAbstractResources().add(resource);

        resourceReferenceSpreadingHelper.spreadAvailableResourceDown(resource);

        return resourceDao.persistResource(resource);
    }

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

        Resource resource = assertAndGetResource(id);

        resource.setLexicalConversion(status);
    }

    /**
     * Delete the given resource
     *
     * @param resourceId the id of the resource to delete
     */
    public void deleteResource(Long resourceId) {
        logger.debug("delete resource #{}", resourceId);

        AbstractResource resource = assertAndGetResourceOrRef(resourceId);

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

        deleteResourceAndRefs(resource);
    }

    /**
     * Ascertain that the resource can be deleted.
     *
     * @param resourceOrRef the resource (or reference) to check for deletion
     *
     * @return True iff it can be safely deleted
     */
    private boolean checkDeletionAcceptability(AbstractResource resourceOrRef) {
        // the only resources we delete manually are the concrete resources
        if (!(resourceOrRef instanceof Resource)) {
            return false;
        }

        return true;
    }

    /**
     * Delete each reference pointing at the given resourceOrRef and remove the resource from the
     * card type / card / card content.
     *
     * @param resourceOrRef The initial abstract resource to delete
     */
    private void deleteResourceAndRefs(AbstractResource resourceOrRef) {
        requestManager.sudo(() -> {
            List<ResourceRef> references = resourceDao.findDirectReferences(resourceOrRef);
            if (references != null) {
                references.stream().forEach(ref -> deleteResourceAndRefs(ref));
            }

            if (resourceOrRef.getOwner() != null) {
                Resourceable owner = resourceOrRef.getOwner();
                owner.getDirectAbstractResources().remove(resourceOrRef);
            }

            resourceDao.deleteResourceOrRef(resourceOrRef);

            // Note : the document is deleted by cascade
        });
    }

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

    /**
     * Duplicate the given resource with the given parameters to fine tune the duplication
     *
     * @param resourceId the id of the project to duplicate
     * @param params     the parameters to fine tune the duplication
     * @param parentType the new owner
     * @param parentId   if of the new owner
     * @param published  new publication status
     *
     * @return the new project
     */
    public Resource copyResourceTo(Long resourceId, DuplicationParam params, String parentType,
        Long parentId, boolean published) {
        AbstractResource originalResourceOrRef = assertAndGetResourceOrRef(resourceId);

        Resource originalResource;
        if (originalResourceOrRef instanceof Resource) {
            originalResource = (Resource) originalResourceOrRef;
        } else if (originalResourceOrRef instanceof ResourceRef) {
            originalResource = originalResourceOrRef.resolve();
        } else {
            throw new IllegalStateException("abstract card type implementation not handled");
        }

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

        Resource newResourceJavaObject;
        try {
            newResourceJavaObject = (Resource) duplicator.duplicateResource(originalResource);
        } catch (ColabMergeException e) {
            throw HttpErrorMessage.dataError(MessageI18nKey.DATA_INTEGRITY_FAILURE);
        }

        Resourceable owner = null;
        if ("Card".equals(parentType)) {
            owner = cardManager.assertAndGetCard(parentId);
        } else if ("CardContent".equals(parentType)) {
            owner = cardContentManager.assertAndGetCardContent(parentId);
        } else if ("CardType".equals(parentType)) {
            owner = cardTypeManager.assertAndGetCardTypeOrRef(parentId);
        } else {
            throw HttpErrorMessage.dataError(MessageI18nKey.DATA_INTEGRITY_FAILURE);
        }
        newResourceJavaObject.setOwner(owner);
        owner.getDirectAbstractResources().add(newResourceJavaObject);

        Resource newResource = createResource(newResourceJavaObject);

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

        duplicator.clear();

        // init or revive new references
        resourceReferenceSpreadingHelper.spreadAvailableResourceDown(newResource);

        return newResource;
    }

    // *********************************************************************************************
    // state cycle
    // *********************************************************************************************

    /**
     * Discard the resource (or reference) so that it cannot be visible anymore
     *
     * @param resourceOrRefId the id of the resource or reference
     */
    public void discardResourceOrRef(Long resourceOrRefId) {
        AbstractResource resourceOrRef = assertAndGetResourceOrRef(resourceOrRefId);

        if (resourceOrRef instanceof Resource) {
            Resource resource = (Resource) resourceOrRef;
            resource.setDeprecated(true);

            // if resource in card, refuse also the resource in the variants
            if (resource.getCard() != null) {
                List<ResourceRef> references = resourceDao.findDirectReferences(resource);
                for (ResourceRef ref : references) {
                    if (Objects.equals(resource.getCard(), ref.getCardContent().getCard())) {
                        resourceReferenceSpreadingHelper.refuseRecursively(ref);
                    }
                }
            }

        } else if (resourceOrRef instanceof ResourceRef) {
            ResourceRef resourceRef = (ResourceRef) resourceOrRef;
            resourceReferenceSpreadingHelper.refuseRecursively(resourceRef);
        }
    }

    /**
     * Restore the resource (or reference) so that it can be visible again
     *
     * @param resourceOrRefId the id of the resource or reference
     */
    public void restoreResourceOrRef(Long resourceOrRefId) {
        AbstractResource resourceOrRef = assertAndGetResourceOrRef(resourceOrRefId);

        if (resourceOrRef instanceof Resource) {
            Resource resource = (Resource) resourceOrRef;
            if (resource.isDeprecated()) {
                resource.setDeprecated(false);
            }

            // if resource in card, un refuse also the resource in the variants
            if (resource.getCard() != null) {
                List<ResourceRef> references = resourceDao.findDirectReferences(resource);
                for (ResourceRef ref : references) {
                    if (Objects.equals(resource.getCard(), ref.getCardContent().getCard())) {
                        resourceReferenceSpreadingHelper.unRefuseRecursively(ref);
                    }
                }
            }

        } else if (resourceOrRef instanceof ResourceRef) {
            ResourceRef resourceRef = (ResourceRef) resourceOrRef;
            if (resourceRef.isRefused()) {
                resourceReferenceSpreadingHelper.unRefuseRecursively(resourceRef);

            }

            if (resourceRef.isResidual()) {
                resourceReferenceSpreadingHelper.reviveRecursively(resourceRef);

            }
        }
    }

    /**
     * Set the resource as published or not
     *
     * @param resourceId        the id of the resource
     * @param newPublishedValue if it is published or not
     */
    public void changeResourcePublication(Long resourceId, boolean newPublishedValue) {
        Resource resource = assertAndGetResource(resourceId);

        resource.setPublished(newPublishedValue);

        if (newPublishedValue) {
            resourceReferenceSpreadingHelper.spreadAvailableResourceDown(resource);
        } else {
            resourceReferenceSpreadingHelper.spreadDisableResourceDown(resource);
        }
    }

    /**
     * Move a resource to a new resourceable
     *
     * @param resourceId id of the resource to move
     * @param newOwner   the new owner
     * @param published  new publication status
     */
    public void moveResource(Long resourceId, Resourceable newOwner, boolean published) {
        Resource resource = assertAndGetResource(resourceId);

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

        Resourceable previousOwner = resource.getOwner();

        if (newOwner.equals(previousOwner)) {
            // not really a move...
            this.changeResourcePublication(resourceId, published);
        } else {

            // mark all references as residual
            resourceReferenceSpreadingHelper.spreadDisableResourceDown(resource, true);

            // move the resource
            if (previousOwner != null) {
                previousOwner.getDirectAbstractResources().remove(resource);
            }

            newOwner.getDirectAbstractResources().add(resource);
            resource.setOwner(newOwner);
            resource.setPublished(published);

            // init or revive new references
            resourceReferenceSpreadingHelper.spreadAvailableResourceDown(resource);
        }
    }

    /**
     * Move a resource to a new resourceable.
     *
     * @param resourceId id of the resource to move
     * @param parentType the new owner
     * @param parentId   if of the new owner
     * @param published  new publication status
     */
    public void moveResource(Long resourceId, String parentType, Long parentId, boolean published) {
        Resourceable parent = null;
        // not that happy with this resourceable resolver
        // todo: normalize it
        if ("Card".equals(parentType)) {
            parent = cardManager.assertAndGetCard(parentId);
        } else if ("CardContent".equals(parentType)) {
            parent = cardContentManager.assertAndGetCardContent(parentId);
        } else if ("CardType".equals(parentType)) {
            parent = cardTypeManager.assertAndGetCardTypeOrRef(parentId);
        }
        this.moveResource(resourceId, parent, published);
    }

    // *********************************************************************************************
    // add a document to a resource
    // *********************************************************************************************

    /**
     * Add the document to the end of the resource.
     *
     * @param resourceId the id of the resource
     * @param document   the document to use in the resource. It must be a new document
     *
     * @return the newly created document
     */
    public Document addDocument(Long resourceId, Document document) {
        logger.debug("add document {} to resource #{}", document, resourceId);

        return addDocument(resourceId, document, RelatedPosition.AT_END);
    }

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

        return addDocument(resourceId, document, relatedPosition, null);
    }

    /**
     * Add the document to the resource.
     *
     * @param resourceId      the id of the resource
     * @param document        the document to use in the resource. It must be a new document
     * @param relatedPosition to define the place where the document will be added in the resource
     * @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 addDocument(Long resourceId, Document document, RelatedPosition relatedPosition,
        Long neighbourDocId) {
        logger.debug("add document {} to resource #{} {} doc #{}", document, resourceId,
            relatedPosition, neighbourDocId);

        Resource resource = assertAndGetResource(resourceId);

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

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

        if (resource.getDocuments().contains(document)) {
            throw HttpErrorMessage.dataError(MessageI18nKey.DATA_INTEGRITY_FAILURE);
        }

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

        resource.getDocuments().add(document);
        document.setOwningResource(resource);

        return documentDao.persistDocument(document);
    }

    /**
     * Remove the document of the resource
     *
     * @param resourceId the id of the resource
     * @param documentId the id of the document to remove
     */
    public void removeDocument(Long resourceId, Long documentId) {
        logger.debug("remove document #{} of resource #{}", documentId, resourceId);

        Resource resource = assertAndGetResource(resourceId);

        Document document = documentManager.assertAndGetDocument(documentId);

        if (!(resource.getDocuments().contains(document))) {
            throw HttpErrorMessage.dataError(MessageI18nKey.DATA_INTEGRITY_FAILURE);
        }

        resource.getDocuments().remove(document);

        documentDao.deleteDocument(document);
    }

    // *********************************************************************************************
    // retrieve the elements of a resource
    // *********************************************************************************************
    /**
     * Get the documents of the resource
     *
     * @param resourceId the id of the resource
     *
     * @return the documents linked to the resource
     */
    public List<Document> getDocumentsOfResource(Long resourceId) {
        logger.debug("get documents of resource #{}", resourceId);

        Resource resource = assertAndGetResource(resourceId);

        return resource.getDocuments();
    }

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

        AbstractResource resource = assertAndGetResourceOrRef(resourceOrRefId);

        return resource.getStickyNoteLinksAsSrc();
    }

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

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

    /**
     * Check the integrity of the card type (or reference)
     *
     * @param resourceOrRef the resource (or reference) to check
     *
     * @return true iff the project is complete and safe
     */
    public boolean checkIntegrity(AbstractResource resourceOrRef) {
        if (resourceOrRef == null) {
            return false;
        }

        if (resourceOrRef instanceof Resource) {
            Resource resource = (Resource) resourceOrRef;
            if (CollectionUtils.isEmpty(resource.getDocuments())) {
                return false;
            }
        }

        if (resourceOrRef instanceof ResourceRef) {
            ResourceRef reference = (ResourceRef) resourceOrRef;
            Resource finalTarget = reference.resolve();
            if (finalTarget == null) {
                return false;
            }
        }

        int nbCardType = resourceOrRef.getAbstractCardType() == null ? 0 : 1;
        int nbCard = resourceOrRef.getCard() == null ? 0 : 1;
        int nbCardContent = resourceOrRef.getCardContent() == null ? 0 : 1;
        if ((nbCardType + nbCard + nbCardContent) != 1) {
            return false;
        }

        return true;
    }

    // *********************************************************************************************
    //
    // *********************************************************************************************

}