ResourceReferenceSpreadingHelper.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.RequestManager;
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.card.CardTypeRef;
import ch.colabproject.colab.api.model.document.AbstractResource;
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.persistence.jpa.card.CardTypeDao;
import ch.colabproject.colab.api.persistence.jpa.document.ResourceDao;
import ch.colabproject.colab.generator.model.exceptions.HttpErrorMessage;
import ch.colabproject.colab.generator.model.exceptions.MessageI18nKey;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import javax.inject.Inject;

/**
 * Resource and resource reference spread specific logic
 *
 * @author sandra
 */
public class ResourceReferenceSpreadingHelper {

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

    /**
     * Resource persistence handler
     */
    @Inject
    private ResourceDao resourceDao;

    /**
     * Card type persistence handler
     */
    @Inject
    private CardTypeDao cardTypeDao;

    /**
     * TO sudo
     */
    @Inject
    private RequestManager requestManager;

    // *********************************************************************************************
    // when a resource / resource reference is added, spread it down stream with references
    // *********************************************************************************************

    /**
     * Each child of the resource owner acquires a reference to the resource.
     *
     * @param resourceOrRef the resource to reference
     */
    public void spreadAvailableResourceDown(AbstractResource resourceOrRef) {
        if (resourceOrRef.getAbstractCardType() != null) {
            requestManager.sudo(() -> {
                AbstractCardType resourceOwner = resourceOrRef.getAbstractCardType();

                for (AbstractCardType cardTypeRef : cardTypeDao
                    .findDirectReferences(resourceOwner)) {
                    makeActiveReference(cardTypeRef, resourceOrRef);
                }

                for (Card implementingCard : resourceOwner.getImplementingCards()) {
                    makeActiveReference(implementingCard, resourceOrRef);
                }
            });
        }

        if (resourceOrRef.getCard() != null) {
            requestManager.sudo(() -> {
                Card resourceOwner = resourceOrRef.getCard();

                for (CardContent variant : resourceOwner.getContentVariants()) {
                    makeActiveReference(variant, resourceOrRef);
                }
            });
        }

        if (resourceOrRef.getCardContent() != null) {
            requestManager.sudo(() -> {
                CardContent resourceOwner = resourceOrRef.getCardContent();

                for (Card subCard : resourceOwner.getSubCards()) {
                    makeActiveReference(subCard, resourceOrRef);
                }
            });
        }
    }

    // *********************************************************************************************
    // when a card type reference / card / card content is added,
    // initialize the references from parent's resources / resource references
    // *********************************************************************************************

    /**
     * Create a resource reference for each resource / resource reference of the parent.
     *
     * @param cardTypeRefToFill A card type reference that need references to the up stream
     *                          resources
     *
     * @return the resource references that have been created or revived (or let as it is if nothing
     *         is needed)
     */
    public List<ResourceRef> extractReferencesFromUp(CardTypeRef cardTypeRefToFill) {
        AbstractCardType targetType = cardTypeRefToFill.getTarget();
        List<ResourceRef> refs = new ArrayList<>();

        for (AbstractResource targetResourceOrRef : targetType.getDirectAbstractResources()) {
            ResourceRef ref = makeActiveReference(cardTypeRefToFill, targetResourceOrRef);
            if (ref != null) {
                refs.add(ref);
            }
        }

        return refs;
    }

    /**
     * Create a resource reference for each resource / resource reference of the parent.
     *
     * @param cardToFill A card that need references to the up stream resources
     */
    public void extractReferencesFromUp(Card cardToFill) {
        CardContent parent = cardToFill.getParent();

        for (AbstractResource parentResourceOrRef : parent.getDirectAbstractResources()) {
            makeActiveReference(cardToFill, parentResourceOrRef);
        }

        if (cardToFill.hasCardType()) {
            AbstractCardType type = cardToFill.getCardType();

            for (AbstractResource typeResourceOrRef : type.getDirectAbstractResources()) {
                makeActiveReference(cardToFill, typeResourceOrRef);
            }
        }
    }

    /**
     * Create a resource reference for each resource / resource reference of the parent.
     *
     * @param cardContentToFill A card content that need references to the up stream resources
     */
    public void extractReferencesFromUp(CardContent cardContentToFill) {
        Card parent = cardContentToFill.getCard();

        for (AbstractResource parentResourceOrRef : parent.getDirectAbstractResources()) {
            makeActiveReference(cardContentToFill, parentResourceOrRef);
        }
    }

    // *********************************************************************************************
    // do it
    // *********************************************************************************************

    /**
     * If the given target resource (or reference) can have references, ensure that there is an
     * active reference to the given target resource (or reference) for the given owner.
     * <p>
     * For that either be sure the already existing reference for the owner and targeting the same
     * final resource is active or make a new reference.
     *
     * @param owner               the owner of the wanted reference
     * @param targetResourceOrRef the target of the wanted reference
     *
     * @return the resource reference that has been created or revived (or let as it is if nothing
     *         is needed)
     */
    private ResourceRef makeActiveReference(Resourceable owner,
        AbstractResource targetResourceOrRef) {

        if (mustHaveReferences(targetResourceOrRef)) {

            ResourceRef existingMatchingReference = findMatchingResourceRef(owner,
                targetResourceOrRef);

            if (existingMatchingReference != null) {
                ResourceRef aimedResourceRef = reviveAndRetarget(existingMatchingReference,
                    targetResourceOrRef);

                spreadAvailableResourceDown(aimedResourceRef);

                return aimedResourceRef;

            } else {
                ResourceRef aimedResourceRef = initNewReference(owner, targetResourceOrRef);

                spreadAvailableResourceDown(aimedResourceRef);

                return aimedResourceRef;
            }
        }

        return null;
    }

    /**
     * Ascertain if there must be resource references down stream for the given target resource (or
     * reference).
     *
     * @param targetResourceOrRef Resource / resource reference
     *
     * @return true iff the resource can have references
     */
    private boolean mustHaveReferences(AbstractResource targetResourceOrRef) {
        Resource concreteTargetResource = targetResourceOrRef.resolve();

        // do not spread unpublished resource
        if (concreteTargetResource != null && !concreteTargetResource.isPublished()) {
            // unless between a card and its card contents
            boolean isResourceLinkedToACard = (targetResourceOrRef instanceof Resource)
                && targetResourceOrRef.getCard() != null;
            if (!isResourceLinkedToACard) {
                return false;
            }
        }

        // do not spread references of a type from a card content to its sub cards
        boolean isResourceOrRefLinkedToACardContent = targetResourceOrRef.getCardContent() != null;
        if (isResourceOrRefLinkedToACardContent) {

            boolean isConcreteResourceLinkedToACardType = concreteTargetResource != null
                && concreteTargetResource.getAbstractCardType() != null;

            return !isConcreteResourceLinkedToACardType;
        }

        // in all other cases, spread
        return true;
    }

    /**
     * Search for an existing resource reference owned by the given owner and targeting the same
     * final resource as the given target.
     * <p>
     * It ensures that the matching reference is unique.
     *
     * @param owner               the owner of the wanted reference
     * @param targetResourceOrRef the target of the wanted reference
     *
     * @return the matching resource reference
     */
    private ResourceRef findMatchingResourceRef(Resourceable owner,
        AbstractResource targetResourceOrRef) {
        List<ResourceRef> refsOfOwnerWithSameFinalTarget = owner.getDirectAbstractResources()
            .stream()
            .filter(resOrRef -> resOrRef instanceof ResourceRef)
            .map(resOrRef -> (ResourceRef) resOrRef)
            .filter(ref -> Objects.equals(ref.resolve(), targetResourceOrRef.resolve()))
            .collect(Collectors.toList());

        if (refsOfOwnerWithSameFinalTarget.size() == 1) {
            return refsOfOwnerWithSameFinalTarget.get(0);
        }

        if (refsOfOwnerWithSameFinalTarget.size() > 1) {
            throw HttpErrorMessage.dataError(MessageI18nKey.DATA_INTEGRITY_FAILURE);
        }

        return null;
    }

    /**
     * Update the given resource reference so that it is not set as residual, make the same for its
     * descendants. Make the given resource reference target the given target.
     * <p>
     * It can be done only if the resource reference and the new target have the same final concrete
     * resource.
     *
     * @param resourceReference the resource reference to update
     * @param newDirectTarget   the new target of the resource reference
     *
     * @return the resource reference that has been revived (or let as it is if nothing is needed)
     */
    private ResourceRef reviveAndRetarget(ResourceRef resourceReference,
        AbstractResource newDirectTarget) {
        if (!Objects.equals(resourceReference.resolve(), newDirectTarget.resolve())) {
            throw HttpErrorMessage.dataError(MessageI18nKey.DATA_INTEGRITY_FAILURE);
        }

        // revive
        resourceReference.setResidual(false);

        // retarget
        AbstractResource olderTarget = resourceReference.getTarget();

        if (!Objects.equals(olderTarget, newDirectTarget)) {
            resourceReference.setTarget(newDirectTarget);
        }

        return resourceReference;
    }

    /**
     * Make a new resource reference to link to the given owner, targeting the given resource (or
     * reference).
     *
     * @param owner         the entity the new resource reference will be linked to
     * @param resourceOrRef the resource (or reference) target of the new resource reference
     *
     * @return the resource reference that has been created
     */
    private ResourceRef initNewReference(Resourceable owner, AbstractResource targetResourceOrRef) {
        ResourceRef newRef = initNewReferenceFrom(targetResourceOrRef);

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

        newRef.setTarget(targetResourceOrRef);

        return newRef;

        // no need to persist, it will be done all at once
    }

    /**
     * Initialize a new reference which will have the given resource (or reference) as target.
     *
     * @param targetResourceOrRef The target of the new resource reference
     *
     * @return the new resource reference
     */
    private ResourceRef initNewReferenceFrom(AbstractResource targetResourceOrRef) {
        ResourceRef newRef = new ResourceRef();

        newRef.setCategory(targetResourceOrRef.getCategory());

        if (targetResourceOrRef instanceof ResourceRef) {
            ResourceRef targetResourceRef = (ResourceRef) targetResourceOrRef;

            newRef.setRefused(targetResourceRef.isRefused());
            newRef.setResidual(targetResourceRef.isResidual());
        }

        return newRef;
    }

    // *********************************************************************************************
    // when something is moved, mark the former ancestors resource references as residual
    // when something is un-published, mark the resource references as residual
    // *********************************************************************************************

    /**
     * Each resource reference of the given owner that is linked to a resource of the given former
     * related is marked as residual. As well as all its descendants.
     *
     * @param owner         the owner of the resource references to mark
     * @param formerRelated the not-any-more-related target of the references
     */
    public void spreadDisableResourceDown(Resourceable owner, Resourceable formerRelated) {
        owner.getDirectAbstractResources().stream()
            .filter(resOrRef -> (resOrRef instanceof ResourceRef))
            .map(resOrRef -> ((ResourceRef) resOrRef))
            .filter(ref -> Objects.equals(ref.getTarget().getOwner(), formerRelated))
            .forEach(ref -> markAsResidualRecursively(ref, true));
    }

    /**
     * Disable = mark as residual.
     * <p>
     * Mark the resource's references as residual. Do it as well for all descendants.
     *
     * @param resource the resource
     */
    public void spreadDisableResourceDown(Resource resource) {
        requestManager.sudo(() -> {
            for (ResourceRef childRef : resourceDao.findDirectReferences(resource)) {
                markAsResidualRecursively(childRef, false);
            }
        });
    }

    /**
     * Disable = mark as residual.
     * <p>
     * Mark the resource's references as residual. Do it as well for all descendants.
     *
     * @param resource   the resource
     * @param alwaysMark if the reference must for sure be marked as residual
     */
    public void spreadDisableResourceDown(Resource resource, boolean alwaysMark) {
        requestManager.sudo(() -> {
            for (ResourceRef childRef : resourceDao.findDirectReferences(resource)) {
                markAsResidualRecursively(childRef, alwaysMark);
            }
        });
    }

    /**
     * Mark the resource reference as residual. Do it as well for all its descendants.
     *
     * @param resourceReference the reference to update
     * @param alwaysMark        if the reference must for sure be marked as residual
     */
    private void markAsResidualRecursively(ResourceRef resourceReference, boolean alwaysMark) {
        if (alwaysMark || resourceReference.getTarget() == null
            || !mustHaveReferences(resourceReference.getTarget())) {
            resourceReference.setResidual(true);
        }

        requestManager.sudo(() -> {
            for (ResourceRef childRef : resourceDao.findDirectReferences(resourceReference)) {
                markAsResidualRecursively(childRef, alwaysMark);
            }
        });
    }

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

    /**
     * Mark the resource reference as refused. Do it as well for all its descendants.
     *
     * @param resourceReference the reference to update
     */
    public void refuseRecursively(ResourceRef resourceReference) {
        resourceReference.setRefused(true);

        requestManager.sudo(() -> {
            for (ResourceRef childRef : resourceDao.findDirectReferences(resourceReference)) {
                refuseRecursively(childRef);
            }
        });
    }

    /**
     * Mark the resource reference as not refused. Do it as well for all its descendants.
     *
     * @param resourceReference the reference to update
     */
    public void unRefuseRecursively(ResourceRef resourceReference) {
        resourceReference.setRefused(false);

        requestManager.sudo(() -> {
            for (ResourceRef childRef : resourceDao.findDirectReferences(resourceReference)) {
                unRefuseRecursively(childRef);
            }
        });
    }

    /**
     * Mark the resource reference as not residual. Do it as well for all its descendants.
     *
     * @param resourceReference the reference to update
     */
    public void reviveRecursively(ResourceRef resourceReference) {
        if (resourceReference.getTarget() != null
            && mustHaveReferences(resourceReference.getTarget())) {
            resourceReference.setResidual(false);
        }

        requestManager.sudo(() -> {
            for (ResourceRef childRef : resourceDao.findDirectReferences(resourceReference)) {
                reviveRecursively(childRef);
            }
        });
    }

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

}