DuplicationManager.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;

import ch.colabproject.colab.api.controller.card.CardContentManager;
import ch.colabproject.colab.api.controller.document.FileManager;
import ch.colabproject.colab.api.controller.document.ResourceReferenceSpreadingHelper;
import ch.colabproject.colab.api.controller.document.YjsException;
import ch.colabproject.colab.api.controller.document.YjsLexicalCaller;
import ch.colabproject.colab.api.exceptions.ColabMergeException;
import ch.colabproject.colab.api.model.ColabEntity;
import ch.colabproject.colab.api.model.DuplicationParam;
import ch.colabproject.colab.api.model.card.*;
import ch.colabproject.colab.api.model.document.*;
import ch.colabproject.colab.api.model.link.ActivityFlowLink;
import ch.colabproject.colab.api.model.link.StickyNoteLink;
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.Assignment;
import ch.colabproject.colab.api.model.user.User;
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.jcr.RepositoryException;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.Map.Entry;

/**
 * Duplication of colab entities.
 * <p>
 * Usage :
 * <ul>
 * <li>Create a {@link DuplicationManager}</li>
 * <li>Duplicate the object with any duplicateXXX</li>
 * <li>Save to JPA database</li>
 * <li>Call {@link #duplicateDataIntoJCR()} in order to save to JCR
 * database</li>
 * </ul>
 *
 * @author sandra
 */
public class DuplicationManager {

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

    /** Comparator for sorting data to create objects in the same order */
    private static final Comparator<ColabEntity> ID_COMPARATOR = Comparator
            .comparingLong(entity -> entity.getId());

    /** parameters to fine tune a duplication */
    private final DuplicationParam params;

    /** helper for resource references */
    private final ResourceReferenceSpreadingHelper resourceSpreader;

    /** File persistence management */
    private final FileManager fileManager;

    /** Card content specific logic handling */
    private final CardContentManager cardContentManager;

    /** To call the YJS lexical server */
    private YjsLexicalCaller yjsLexicalCaller;

    /** Matching between the old id and the new team roles */
    private Map<Long, TeamRole> teamRoleMatching = new HashMap<>();

    /** Matching between the old id and the new team members */
    private Map<Long, TeamMember> teamMemberMatching = new HashMap<>();

    /** Matching between the old id and the new card types */
    private Map<Long, AbstractCardType> cardTypeMatching = new HashMap<>();

    /** Matching between the old id and the new cards */
    private Map<Long, Card> cardMatching = new HashMap<>();

    /** Matching between the old id and the new card contents */
    private Map<Long, CardContent> cardContentMatching = new HashMap<>();

    /** Matching between the old id and the new resources */
    private Map<Long, AbstractResource> resourceMatching = new HashMap<>();

    /** Matching between the old id and the new document */
    private Map<Long, Document> documentMatching = new HashMap<>();

    /** the sticky note links to duplicate. They are filled when handling cards */
    private List<StickyNoteLink> stickyNoteLinksToDuplicate = new ArrayList<>();

    /** the activity flow links to duplicate. They are filled when handling cards */
    private List<ActivityFlowLink> activityFlowLinksToDuplicate = new ArrayList<>();

    /** Document files to process once the ids are here */
    private Map<Long, DocumentFile> documentFilesToProcessOnceIds = new HashMap<>();

    /**
     * @param params             Parameters to fine tune duplication
     * @param resourceSpreader   Helper for resource references
     * @param fileManager        File persistence management
     * @param cardContentManager Card content specific logic handling
     */
    public DuplicationManager(DuplicationParam params,
            ResourceReferenceSpreadingHelper resourceSpreader,
            FileManager fileManager, CardContentManager cardContentManager) {
        this.params = params;
        this.resourceSpreader = resourceSpreader;
        this.fileManager = fileManager;
        this.cardContentManager = cardContentManager;
    }

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

    /**
     * Duplicate the given project. No database action is provided.
     *
     * @param originalProject the project to duplicate
     *
     * @return the duplicated project
     */
    public Project duplicateProject(Project originalProject) {
        try {
            Project newProject = new Project();
            newProject.mergeToDuplicate(originalProject);

            ////////////////////////////////////////////////////////////////////////////////////////
            // Team roles
            if (params.isWithRoles()) {
                List<TeamRole> teamRoles = originalProject.getRoles();
                teamRoles.sort(ID_COMPARATOR);

                for (TeamRole original : teamRoles) {
                    TeamRole newTeamRole = duplicateTeamRole(original);

                    newTeamRole.setProject(newProject);
                    newProject.getRoles().add(newTeamRole);
                }
            } else {
                logger.info("param do not duplicate project's team roles");
            }

            ////////////////////////////////////////////////////////////////////////////////////////
            // Team members
            if (params.isWithTeamMembers()) {
                List<TeamMember> teamMembers = originalProject.getTeamMembers();
                teamMembers.sort(ID_COMPARATOR);

                for (TeamMember original : teamMembers) {
                    TeamMember newTeamMember = duplicateTeamMember(original);

                    newTeamMember.setProject(newProject);
                    newProject.getTeamMembers().add(newTeamMember);
                }
            } else {
                logger.info("param do not duplicate project's team members");
            }

            ////////////////////////////////////////////////////////////////////////////////////////
            // instance makers
            /* are never duplicated */

            ////////////////////////////////////////////////////////////////////////////////////////
            // Card types
            if (params.isWithCardTypes()) {
                List<AbstractCardType> cardTypes = originalProject.getElementsToBeDefined();
                cardTypes.sort(ID_COMPARATOR);

                for (AbstractCardType original : cardTypes) {
                    AbstractCardType newCardType = duplicateCardType(original);

                    newCardType.setProject(newProject);
                    newProject.getElementsToBeDefined().add(newCardType);
                }
            }

            ////////////////////////////////////////////////////////////////////////////////////////
            // Cards
            if (params.isWithCardsStructure()) {
                Card original = originalProject.getRootCard();
                if (original != null) {
                    Card newRootCard = duplicateCard(original);

                    newRootCard.setRootCardProject(newProject);
                    newProject.setRootCard(newRootCard);
                }

            }

            ////////////////////////////////////////////////////////////////////////////////////////
            // sticky notes
            if (params.isWithStickyNotes()) {
                stickyNoteLinksToDuplicate.sort(ID_COMPARATOR);

                for (StickyNoteLink original : stickyNoteLinksToDuplicate) {
                    duplicateStickyNoteLink(original);
                }
            }

            ////////////////////////////////////////////////////////////////////////////////////////
            // activity flow
            if (params.isWithActivityFlow()) {
                activityFlowLinksToDuplicate.sort(ID_COMPARATOR);

                for (ActivityFlowLink original : activityFlowLinksToDuplicate) {
                    duplicateActivityFlowLink(original);
                }
            }

            return newProject;
        } catch (ColabMergeException e) {
            throw HttpErrorMessage.dataError(MessageI18nKey.DATA_INTEGRITY_FAILURE);
        }
        // TODO sandra work in progress : handle exceptions
    }

    private TeamRole duplicateTeamRole(TeamRole original) throws ColabMergeException {
        TeamRole newTeamRole = new TeamRole();
        newTeamRole.mergeToDuplicate(original);

        teamRoleMatching.put(original.getId(), newTeamRole);

        return newTeamRole;
    }

    private TeamMember duplicateTeamMember(TeamMember original) throws ColabMergeException {
        TeamMember newTeamMember = new TeamMember();
        newTeamMember.mergeToDuplicate(original);

        teamMemberMatching.put(original.getId(), newTeamMember);

        ////////////////////////////////////////////////////////////////////////////////////////////
        // Team members's user
        User user = original.getUser();
        if (user != null) {
            newTeamMember.setUser(user);
        }

        ////////////////////////////////////////////////////////////////////////////////////////////
        // Team members's roles
        if (params.isWithRoles()) {
            List<TeamRole> linkedRoles = original.getRoles();
            linkedRoles.sort(ID_COMPARATOR);

            for (TeamRole linkedRole : linkedRoles) {
                TeamRole newRole = teamRoleMatching.get(linkedRole.getId());

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

                newRole.getMembers().add(newTeamMember);
                newTeamMember.getRoles().add(newRole);
            }
        }

        return newTeamMember;
    }

    private AbstractCardType duplicateCardType(AbstractCardType original)
            throws ColabMergeException {

        if (params.isMakeOnlyCardTypeReferences()) {
            CardTypeRef newCardTypeRef = new CardTypeRef();
            newCardTypeRef.setTarget(original);
            newCardTypeRef.setDeprecated(original.isDeprecated());
            newCardTypeRef.setPublished(false);

            cardTypeMatching.put(original.getId(), newCardTypeRef);

            if (resourceSpreader == null) {
                throw new IllegalStateException(
                        "Dear developer, please define the resource spreader");
            }

            List<ResourceRef> createdRefs = resourceSpreader
                    .extractReferencesFromUp(newCardTypeRef);
            createdRefs.stream().forEach(ref -> resourceMatching.put(ref.getTarget().getId(), ref));

            return newCardTypeRef;
        } else {
            AbstractCardType newAbstractCardType;
            if (original instanceof CardType) {
                CardType originalCardType = (CardType) original;

                CardType newCardType = new CardType();
                newCardType.mergeToDuplicate(originalCardType);

                cardTypeMatching.put(originalCardType.getId(), newCardType);

                TextDataBlock purpose = originalCardType.getPurpose();
                if (purpose != null) {
                    TextDataBlock newPurpose = (TextDataBlock) duplicateDocument(purpose);
                    newPurpose.setPurposingCardType(newCardType);
                    newCardType.setPurpose(newPurpose);
                }

                newAbstractCardType = newCardType;
            } else if (original instanceof CardTypeRef) {
                CardTypeRef originalCardTypeRef = (CardTypeRef) original;

                CardTypeRef newCardTypeRef = new CardTypeRef();
                newCardTypeRef.mergeToDuplicate(originalCardTypeRef);

                cardTypeMatching.put(originalCardTypeRef.getId(), newCardTypeRef);

                AbstractCardType originalTarget = originalCardTypeRef.getTarget();
                if (originalTarget != null) {
                    if (originalTarget.getProjectId() != originalCardTypeRef.getProjectId()) {
                        newCardTypeRef.setTarget(originalTarget);
                    } else {
                        throw new IllegalStateException(
                                "the target of a card type reference must be outside the project");
                        // Note for an hypothetical future evolution :
                        // if we break the condition that, in a project,
                        // there is only one reference per target type outside the project
                        // we must process the card types in the appropriate order
                    }
                }

                newAbstractCardType = newCardTypeRef;
            } else {
                throw new IllegalStateException("abstract card type implementation not handled");
            }

            if (params.isWithResources()) {
                List<AbstractResource> originalResources = original.getDirectAbstractResources();
                originalResources.sort(ID_COMPARATOR);

                for (AbstractResource originalResource : originalResources) {
                    AbstractResource newResource = duplicateResource(originalResource);

                    newResource.setAbstractCardType(newAbstractCardType);
                    newAbstractCardType.getDirectAbstractResources().add(newResource);
                }
            } else {
                logger.info("param do not duplicate project's resources");
            }

            return newAbstractCardType;
        }
    }

    private Card duplicateCard(Card original) throws ColabMergeException {
        Card newCard = new Card();
        newCard.mergeToDuplicate(original);

        cardMatching.put(original.getId(), newCard);

        AbstractCardType originalCardType = original.getCardType();
        if (originalCardType != null) {
            AbstractCardType newCardType = cardTypeMatching.get(originalCardType.getId());

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

            newCard.setCardType(newCardType);
        }

        if (params.isWithResources()) {
            List<AbstractResource> originalResources = original.getDirectAbstractResources();
            originalResources.sort(ID_COMPARATOR);

            for (AbstractResource originalResource : originalResources) {
                AbstractResource newResource = duplicateResource(originalResource);

                newResource.setCard(newCard);
                newCard.getDirectAbstractResources().add(newResource);
            }
        } else {
            logger.info("param do not duplicate project's resources");
        }

        List<CardContent> originalContents = original.getContentVariants();
        originalContents.sort(ID_COMPARATOR);

        for (CardContent originalCardContent : originalContents) {
            CardContent newCardContent = duplicateCardContent(originalCardContent);

            newCardContent.setCard(newCard);
            newCard.getContentVariants().add(newCardContent);
        }

        ////////////////////////////////////////////////////////////////////////////////////////////
        // Assignments

        List<Assignment> originalAssignments = original.getAssignments();
        originalAssignments.sort(ID_COMPARATOR);

        for (Assignment originalAssignment : originalAssignments) {
            if ((params.isWithTeamMembers() || originalAssignment.getMember() == null)
                    && (params.isWithRoles() || originalAssignment.getRole() == null)) {

                Assignment newAssignment = duplicateAssignment(originalAssignment);

                if (newAssignment != null) { // if we got a null, no need to duplicate
                    newAssignment.setCard(newCard);
                    newCard.getAssignments().add(newAssignment);
                }
            }

        }

        ////////////////////////////////////////////////////////////////////////////////////////
        // sticky notes
        if (params.isWithStickyNotes()) {
            stickyNoteLinksToDuplicate.addAll(original.getStickyNoteLinksAsDest());
        }

        ////////////////////////////////////////////////////////////////////////////////////////
        // activity flow
        if (params.isWithActivityFlow()) {
            activityFlowLinksToDuplicate.addAll(original.getActivityFlowLinksAsPrevious());
        }

        return newCard;
    }

    private CardContent duplicateCardContent(CardContent original) throws ColabMergeException {
        CardContent newCardContent = new CardContent();
        newCardContent.mergeToDuplicate(original);

        if (params.isResetProgressionData()) {
            cardContentManager.resetProgression(newCardContent);
        }

        cardContentMatching.put(original.getId(), newCardContent);

        if (params.isWithDeliverables()) {
            List<Document> originalDeliverables = original.getDeliverables();
            originalDeliverables.sort(ID_COMPARATOR);

            for (Document originalDoc : originalDeliverables) {
                Document newDoc = duplicateDocument(originalDoc);

                newDoc.setOwningCardContent(newCardContent);
                newCardContent.getDeliverables().add(newDoc);
            }
        } else {
            logger.info("param do not duplicate project's deliverables");
        }

        if (params.isWithResources()) {
            List<AbstractResource> originalResources = original.getDirectAbstractResources();
            originalResources.sort(ID_COMPARATOR);

            for (AbstractResource originalResource : originalResources) {
                AbstractResource newResource = duplicateResource(originalResource);

                newResource.setCardContent(newCardContent);
                newCardContent.getDirectAbstractResources().add(newResource);
            }
        } else {
            logger.info("param do not duplicate project's resources");
        }

        List<Card> originalSubCards = original.getSubCards();
        originalSubCards.sort(ID_COMPARATOR);

        for (Card originalSubCard : originalSubCards) {
            Card newSubCard = duplicateCard(originalSubCard);

            newSubCard.setParent(newCardContent);
            newCardContent.getSubCards().add(newSubCard);
        }

        return newCardContent;
    }

    private Document duplicateDocument(Document original) throws ColabMergeException {
        if (original instanceof DocumentFile) {
            DocumentFile originalDocumentFile = (DocumentFile) original;

            DocumentFile newDocumentFile = duplicateDocumentFile(originalDocumentFile);

            documentMatching.put(originalDocumentFile.getId(), newDocumentFile);

            return newDocumentFile;
        } else if (original instanceof ExternalLink) {
            ExternalLink originalExternalLink = (ExternalLink) original;

            ExternalLink newExternalLink = new ExternalLink();
            newExternalLink.mergeToDuplicate(originalExternalLink);

            documentMatching.put(originalExternalLink.getId(), newExternalLink);

            return newExternalLink;
        } else if (original instanceof TextDataBlock) {
            TextDataBlock originalTextDataBlock = (TextDataBlock) original;

            TextDataBlock newTextDataBlock = new TextDataBlock();
            newTextDataBlock.mergeToDuplicate(originalTextDataBlock);

            documentMatching.put(originalTextDataBlock.getId(), newTextDataBlock);

            return newTextDataBlock;
        } else {
            throw new IllegalStateException("abstract card type implementation not handled");
        }
    }

    private DocumentFile duplicateDocumentFile(DocumentFile original) throws ColabMergeException {
        DocumentFile newDocumentFile = new DocumentFile();
        newDocumentFile.mergeToDuplicate(original);

        documentFilesToProcessOnceIds.put(original.getId(), newDocumentFile);

        return newDocumentFile;
    }

    /**
     * Duplicate the given resource. No database action is provided.
     *
     * @param original the resource to duplicate
     *
     * @return the duplicated resource
     *
     * @throws ColabMergeException if merging is not possible
     */
    public AbstractResource duplicateResource(AbstractResource original)
            throws ColabMergeException {
        if (original instanceof Resource) {
            Resource originalResource = (Resource) original;

            Resource newResource = new Resource();
            newResource.mergeToDuplicate(originalResource);

            resourceMatching.put(originalResource.getId(), newResource);

            List<Document> originalDocuments = originalResource.getDocuments();
            originalDocuments.sort(ID_COMPARATOR);

            for (Document originalDocument : originalDocuments) {
                Document newDocument = duplicateDocument(originalDocument);

                newDocument.setOwningResource(newResource);
                newResource.getDocuments().add(newDocument);
            }

            TextDataBlock teaser = originalResource.getTeaser();
            if (teaser != null) {
                TextDataBlock newTeaser = (TextDataBlock) duplicateDocument(teaser);

                newTeaser.setTeasingResource(newResource);
                newResource.setTeaser(newTeaser);
            }

            return newResource;
        } else if (original instanceof ResourceRef) {
            ResourceRef originalResourceRef = (ResourceRef) original;

            ResourceRef newResourceRef = new ResourceRef();
            newResourceRef.mergeToDuplicate(originalResourceRef);

            resourceMatching.put(originalResourceRef.getId(), newResourceRef);

            AbstractResource originalTarget = originalResourceRef.getTarget();
            if (originalTarget != null) {
                AbstractResource newTarget;
                if (resourceMatching.containsKey(originalTarget.getId())) {
                    newTarget = resourceMatching.get(originalTarget.getId());
                } else {
                    newTarget = originalTarget;
                }
                newResourceRef.setTarget(newTarget);
            }

            return newResourceRef;

        } else {
            throw new IllegalStateException("abstract card type implementation not handled");
        }
    }

    private Assignment duplicateAssignment(Assignment original)
            throws ColabMergeException {
        Assignment newAssignment = new Assignment();
        newAssignment.mergeToDuplicate(original);

        if (original.getMember() != null) {
            TeamMember member = teamMemberMatching.get(original.getMember().getId());

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

            member.getAssignments().add(newAssignment);
            newAssignment.setMember(member);
        }

        if (original.getRole() != null) {
            TeamRole role = teamRoleMatching.get(original.getRole().getId());

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

            role.getAssignments().add(newAssignment);
            newAssignment.setRole(role);
        }

        return newAssignment;
    }

    private StickyNoteLink duplicateStickyNoteLink(StickyNoteLink original)
            throws ColabMergeException {
        StickyNoteLink newLink = new StickyNoteLink();
        newLink.mergeToDuplicate(original);

        TextDataBlock explanation = original.getExplanation();
        if (explanation != null) {
            TextDataBlock newExplanation = (TextDataBlock) duplicateDocument(explanation);
            newExplanation.setExplainingStickyNoteLink(newLink);
            newLink.setExplanation(newExplanation);
        }

        if (original.getDestinationCard() != null) {
            Card destinationCard = cardMatching.get(original.getDestinationCard().getId());

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

            destinationCard.getStickyNoteLinksAsDest().add(newLink);
            newLink.setDestinationCard(destinationCard);
        }

        if (original.getSrcCard() != null) {
            Card srcCard = cardMatching.get(original.getSrcCard().getId());

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

            srcCard.getStickyNoteLinksAsSrc().add(newLink);
            newLink.setSrcCard(srcCard);
        }

        if (original.getSrcCardContent() != null) {
            CardContent srcCardContent = cardContentMatching
                    .get(original.getSrcCardContent().getId());

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

            srcCardContent.getStickyNoteLinksAsSrc().add(newLink);
            newLink.setSrcCardContent(srcCardContent);
        }

        if (original.getSrcResourceOrRef() != null) {
            AbstractResource srcResourceOrRef = resourceMatching
                    .get(original.getSrcResourceOrRef().getId());

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

            srcResourceOrRef.getStickyNoteLinksAsSrc().add(newLink);
            newLink.setSrcResourceOrRef(srcResourceOrRef);
        }

        if (original.getSrcDocument() != null) {
            Document srcDocument = documentMatching.get(original.getSrcDocument().getId());

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

            srcDocument.getStickyNoteLinksAsSrc().add(newLink);
            newLink.setSrcDocument(srcDocument);
        }

        return newLink;
    }

    private ActivityFlowLink duplicateActivityFlowLink(ActivityFlowLink original)
            throws ColabMergeException {
        ActivityFlowLink newLink = new ActivityFlowLink();
        newLink.mergeToDuplicate(newLink);

        if (original.getPreviousCard() != null) {
            Card previousCard = cardMatching.get(original.getPreviousCard().getId());

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

            previousCard.getActivityFlowLinksAsPrevious().add(newLink);
            newLink.setPreviousCard(previousCard);
        }

        if (original.getNextCard() != null) {
            Card nextCard = cardMatching.get(original.getNextCard().getId());

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

            nextCard.getActivityFlowLinksAsNext().add(newLink);
            newLink.setNextCard(nextCard);
        }

        return newLink;
    }

    /**
     * Duplicate the data in the JCR. It must happen after creating the data in JPA
     * as long as we
     * need the ids.
     */
    public void duplicateDataIntoJCR() {
        try {
            for (Entry<Long, DocumentFile> data : documentFilesToProcessOnceIds.entrySet()) {
                duplicateFileDocumentIntoJCR(data.getKey(), data.getValue());
            }
        } catch (RepositoryException e) {
            throw HttpErrorMessage.duplicationError();
        }
    }

    /**
     * Duplicate the data in the Lexical YJS service.
     * <p>
     * That is the text data of the card contents and resources
     */
    public void duplicateLexicalData() {
        if (params.isWithDeliverables()) {
            for (Entry<Long, CardContent> data : cardContentMatching.entrySet()) {

                callDuplicateYjsService(data.getKey(), LexicalDataOwnershipKind.CARD_CONTENT,
                        data.getValue().getId(), LexicalDataOwnershipKind.CARD_CONTENT);
            }
        }

        if (params.isWithResources()) {
            for (Entry<Long, AbstractResource> data : resourceMatching.entrySet()) {
                if (data.getValue() instanceof Resource) {
                    callDuplicateYjsService(data.getKey(), LexicalDataOwnershipKind.RESOURCE,
                            data.getValue().getId(), LexicalDataOwnershipKind.RESOURCE);
                }
            }
        }
    }

    /**
     * Duplicate lexical data for a specific owner
     *
     * @param srcOwnerId    the original owner id
     * @param srcOwnerKind  the original kind of owner
     * @param destOwnerId   the new owner id
     * @param destOwnerKind the new kind of owner
     */
    private void callDuplicateYjsService(Long srcOwnerId, LexicalDataOwnershipKind srcOwnerKind,
            Long destOwnerId, LexicalDataOwnershipKind destOwnerKind) {
        try {
            // yjsLexicalCaller must be instanced here, else only 6 texts can be duplicated
            // please, make it stronger and consistent if you can
            yjsLexicalCaller = new YjsLexicalCaller();

            yjsLexicalCaller.sendDuplicationRequest(
                    srcOwnerId, srcOwnerKind, destOwnerId, destOwnerKind);
        } catch (YjsException e) {
            throw HttpErrorMessage.duplicationError();
        }
    }

    /**
     * Duplicate the file document data into JCR
     *
     * @param srcDocId
     * @param newDocFile
     */
    private void duplicateFileDocumentIntoJCR(Long srcDocId, DocumentFile newDocFile)
            throws RepositoryException {
        if (fileManager == null) {
            throw new IllegalStateException("Dear developer, you must have defined a file manager");
        }

        if (fileManager.hasFile(srcDocId)) {
            InputStream fileStream = null;
            try {
                fileStream = fileManager.getFileStream(srcDocId);
                fileManager.updateOrCreateFile(newDocFile.getId(), fileStream);
            } finally {
                if (fileStream != null) {
                    try {
                        fileStream.close();
                    } catch (IOException e) {
                        // not really a problem
                        // silent ex
                        logger.warn("Could not close stream", e);
                    }
                }
            }
        }
    }

    /**
     * Clear processed data
     */
    public void clear() {
        teamRoleMatching.clear();
        teamMemberMatching.clear();
        cardTypeMatching.clear();
        cardMatching.clear();
        cardContentMatching.clear();
        resourceMatching.clear();
        documentMatching.clear();
        stickyNoteLinksToDuplicate.clear();
        activityFlowLinksToDuplicate.clear();
        documentFilesToProcessOnceIds.clear();
    }

}