ResourceRestEndpoint.java

/*
 * The coLAB project
 * Copyright (C) 2021-2023 AlbaSim, MEI, HEIG-VD, HES-SO
 *
 * Licensed under the MIT License
 */
package ch.colabproject.colab.api.rest.document;

import ch.colabproject.colab.api.controller.document.RelatedPosition;
import ch.colabproject.colab.api.controller.document.ResourceCategoryHelper;
import ch.colabproject.colab.api.controller.document.ResourceManager;
import ch.colabproject.colab.api.exceptions.ColabMergeException;
import ch.colabproject.colab.api.model.DuplicationParam;
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.link.StickyNoteLink;
import ch.colabproject.colab.api.persistence.jpa.document.ResourceDao;
import ch.colabproject.colab.api.rest.document.bean.ResourceCreationData;
import ch.colabproject.colab.api.rest.document.bean.ResourceExternalReference;
import ch.colabproject.colab.generator.model.annotations.AuthenticationRequired;
import java.util.List;
import javax.inject.Inject;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import org.apache.commons.collections4.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * REST resource controller
 *
 * @author sandra
 */
@Path("resources")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@AuthenticationRequired
public class ResourceRestEndpoint {

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

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

    /**
     * The resource / resource reference persistence manager
     */
    @Inject
    private ResourceDao resourceDao;

    /**
     * The resource and resource reference related logic
     */
    @Inject
    private ResourceManager resourceManager;

    /**
     * Category specific logic handling
     */
    @Inject
    private ResourceCategoryHelper resourceCategoryHelper;

    // *********************************************************************************************
    // read
    // *********************************************************************************************

    /**
     * Get the resource or resource reference identified by the given id
     *
     * @param id the id of the resource or resource reference to fetch
     *
     * @return the resource or resource reference or null
     */
    @GET
    @Path("{id: [0-9]+}")
    public AbstractResource getAbstractResource(@PathParam("id") Long id) {
        logger.debug("get abstract resource #{}", id);
        return resourceDao.findResourceOrRef(id);
    }

    /**
     * Check the document identified by the given id
     *
     * @param id id of the document to check
     *
     */
    @GET
    @Path("{id}/assertReadWrite")
    public void assertReadWrite(@PathParam("id") Long id) {
        logger.debug("assert read/write card content #{}", id);
        resourceManager.assertResourceReadWrite(id);
    }

    /**
     * Get the resources linked to the card type or reference. With a list of resource references to
     * retrieve each resource.
     *
     * @param cardTypeOrRefId the id of the abstract card type for which we look for the linked
     *                        resources
     *
     * @return The targeted resource and a list of the references to get it
     */
    @GET
    @Path("fromCardType/{cardTypeOrRefId: [0-9]+}")
    public List<List<AbstractResource>> getResourceChainForAbstractCardType(
        @PathParam("cardTypeOrRefId") Long cardTypeOrRefId) {
        logger.debug("get resource chain for card content #{}", cardTypeOrRefId);
        return resourceManager.getExpandedResourcesForAbstractCardType(cardTypeOrRefId);
    }

    /**
     * Get the resources linked to the card. With a list of resource references to retrieve each
     * resource.
     *
     * @param cardId the id of the card for which we look for the linked resources
     *
     * @return The targeted resource and a list of the references to get it
     */
    @GET
    @Path("fromCard/{cardId: [0-9]+}")
    public List<List<AbstractResource>> getResourceChainForCard(
        @PathParam("cardId") Long cardId) {
        logger.debug("get resource chain for card content #{}", cardId);
        return resourceManager.getExpandedResourcesForCard(cardId);
    }

    /**
     * Get the resources linked to the card content. With a list of resource references to retrieve
     * each resource.
     *
     * @param cardContentId the id of the card content for which we look for the linked resources
     *
     * @return The targeted resources and a list of the references to get them
     */
    @GET
    @Path("fromCardContent/{cardContentId: [0-9]+}")
    public List<List<AbstractResource>> getResourceChainForCardContent(
        @PathParam("cardContentId") Long cardContentId) {
        logger.debug("get resource chain for card content #{}", cardContentId);
        return resourceManager.getExpandedResourcesForCardContent(cardContentId);
    }

    /**
     * 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
     */
    @GET
    @Path("directOfProject/{projectId: [0-9]+}")
    public List<AbstractResource> getDirectAbstractResourcesOfProject(
        @PathParam("projectId") Long projectId) {
        logger.debug("get all resources of the project #{}", projectId);
        return resourceManager.getDirectAbstractResourcesOfProject(projectId);
    }

    /**
     * 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
     */
    @GET
    @Path("externalReference/{abstractResourceId: [0-9]+}")
    public List<ResourceExternalReference> getResourceExternalReferences(
        @PathParam("abstractResourceId") Long abstractResourceId) {
        return resourceManager.getResourceExternalReferences(abstractResourceId);
    }

    // *********************************************************************************************
    // update
    // *********************************************************************************************

    /**
     * Save changes to database. Only fields which are editable by users will be impacted.
     *
     * @param resource the resource to update
     *
     * @throws ColabMergeException if the merge is impossible
     */
    @PUT
    public void updateResource(Resource resource) throws ColabMergeException {
        logger.debug("update resource {}", resource);
        resourceDao.updateResourceOrRef(resource);
    }

    /**
     * Save changes to database. Only fields which are editable by users will be impacted.
     *
     * @param resourceRef the resource reference to update
     *
     * @throws ColabMergeException if the merge is impossible
     */
    @PUT
    @Path("ref")
    public void updateResourceRef(ResourceRef resourceRef) throws ColabMergeException {
        logger.debug("update resource reference {}", resourceRef);
        resourceDao.updateResourceOrRef(resourceRef);
    }

    /**
     * Set the lexical conversion status.
     *
     * @param id the id of the resource
     * @param status the new lexical conversion status to set
     */
    @PUT
    @Path("changeLexiConv/{id: [0-9]+}")
    public void changeResourceLexicalConversionStatus(@PathParam("id") Long id, ConversionStatus status) {
        logger.debug("change lexical conversion status to {} for resource #{}", status, id);
        resourceManager.changeResourceLexicalConversionStatus(id, status);
    }

    /**
     * Duplicate the given resource
     *
     * @param resourceId the id of the resource we want to duplicate
     * @param parentType the new owner
     * @param parentId   if of the new owner
     *
     * @return the id of the duplicated resource
     */
    @PUT
    @Path("copyResource1/{resourceId: [0-9]+}/to/{parentType: (Card|CardContent|CardType)}/{parentId: [0-9]+}")
    public Long damr1(
        @PathParam("resourceId") Long resourceId,
        @PathParam("parentType") String parentType,
        @PathParam("parentId") Long parentId) {
        logger.debug("duplicate the resource #{} to {}#{}", resourceId, parentType,
            parentId);

        DuplicationParam effectiveParams = DuplicationParam.buildDefaultForCopyOfResource();

        Resource newResource = resourceManager.copyResourceTo(resourceId, effectiveParams,
            parentType, parentId, false);

        return newResource.getId();
    }

    // *********************************************************************************************
    // change state of a resource
    // *********************************************************************************************

    /**
     * Make a resource (or reference) not active = not visible anymore
     *
     * @param resourceOrRefId the resource (or reference) to discard
     *
     * @throws ColabMergeException if the merge is impossible
     */
    @PUT
    @Path("discard")
    public void discardResourceOrRef(Long resourceOrRefId)
        throws ColabMergeException {
        logger.debug("discard resource or ref #{}", resourceOrRefId);
        resourceManager.discardResourceOrRef(resourceOrRefId);
    }

    /**
     * Make a resource (or reference) active again
     *
     * @param resourceOrRefId the resource (or reference) to restore
     *
     * @throws ColabMergeException if the merge is impossible
     */
    @PUT
    @Path("restore")
    public void restoreResourceOrRef(Long resourceOrRefId)
        throws ColabMergeException {
        logger.debug("restore resource or ref #{} ", resourceOrRefId);
        resourceManager.restoreResourceOrRef(resourceOrRefId);
    }

    /**
     * Publish a resource.
     *
     * @param resourceId the id of the resource
     */
    @PUT
    @Path("publish")
    public void publishResource(Long resourceId) {
        logger.debug("Publish resource #{}", resourceId);
        resourceManager.changeResourcePublication(resourceId, true);
    }

    /**
     * Un-publish a resource.
     *
     * @param resourceId the id of the resource
     */
    @PUT
    @Path("unpublish")
    public void unpublishResource(Long resourceId) {
        logger.debug("Unpublish resource #{}", resourceId);
        resourceManager.changeResourcePublication(resourceId, false);
    }

    // *********************************************************************************************
    // move a resource / document
    // *********************************************************************************************

    /**
     * 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
     */
    @PUT
    @Path("move/{resourceId: [0-9]+}/to/{parentType: (Card|CardContent|CardType)}/{parentId: [0-9]+}/{published}")
    public void moveResource(
        @PathParam("resourceId") Long resourceId,
        @PathParam("parentType") String parentType,
        @PathParam("parentId") Long parentId,
        @PathParam("published") Boolean published) {
        logger.debug("Move resource #{} to {}#{}; published={}",
            resourceId, parentType, parentId, published);

        resourceManager.moveResource(resourceId, parentType, parentId, published);
    }

    // *********************************************************************************************
    // create a resource / document
    // *********************************************************************************************

    /**
     * Create a resource
     *
     * @param resourceCreationData Everything needed to create a resource
     *
     * @return the brand new resource id
     */
    @POST
    @Path("create")
    public Long createResource(ResourceCreationData resourceCreationData) {
        logger.debug("create resource {}", resourceCreationData);

        Resource resource = new Resource();
        resource.setTitle(resourceCreationData.getTitle());
        resource.setTeaser(resourceCreationData.getTeaser());
        if (resource.getTeaser() != null) {
            resource.getTeaser().setTeasingResource(resource);
        }
        resource.setCategory(resourceCreationData.getCategory());
        resource.setAbstractCardTypeId(resourceCreationData.getAbstractCardTypeId());
        resource.setCardId(resourceCreationData.getCardId());
        resource.setCardContentId(resourceCreationData.getCardContentId());
        resource.setPublished(resourceCreationData.isPublished());

        Resource newResource = resourceManager.createResource(resource);

        if (CollectionUtils.isNotEmpty(resourceCreationData.getDocuments())) {
            for (Document document : resourceCreationData.getDocuments()) {
                resourceManager.addDocument(newResource.getId(), document);
            }
        }

        return newResource.getId();
    }

    /**
     * Add the document at the beginning 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 document newly created
     */
    @POST
    @Path("{id: [0-9]+}/addDocumentAtBeginning")
    public Document addDocumentAtBeginning(@PathParam("id") Long resourceId, Document document) {
        logger.debug("add the document {} at the beginning of the resource #{}", document,
            resourceId);
        return resourceManager.addDocument(resourceId, document,
            RelatedPosition.AT_BEGINNING, null);
    }

    /**
     * Add the document at 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 document newly created
     */
    @POST
    @Path("{id: [0-9]+}/addDocumentAtEnd")
    public Document addDocumentAtEnd(@PathParam("id") Long resourceId, Document document) {
        logger.debug("add the document {} at the end of the resource #{}", document, resourceId);
        return resourceManager.addDocument(resourceId, document,
            RelatedPosition.AT_END, null);
    }

    /**
     * Add the document to the resource just before the given document.
     *
     * @param resourceId     the id of the resource
     * @param neighbourDocId the id of the document which will be just after the new document
     * @param document       the document to use in the resource. It must be a new document
     *
     * @return the document newly created
     */
    @POST
    @Path("{id: [0-9]+}/addDocumentBefore/{neighbourDocId: [0-9]+}")
    public Document addDocumentBefore(@PathParam("id") Long resourceId,
        @PathParam("neighbourDocId") Long neighbourDocId, Document document) {
        logger.debug("add the document {} before #{} in the resource #{}", document,
            neighbourDocId, resourceId);
        return resourceManager.addDocument(resourceId, document, RelatedPosition.BEFORE,
            neighbourDocId);
    }

    /**
     * Add the document to the resource just after the given document.
     *
     * @param resourceId     the id of the resource
     * @param neighbourDocId the id of the document which will be just before the new document
     * @param document       the document to use in the resource. It must be a new document
     *
     * @return the document newly created
     */
    @POST
    @Path("{id: [0-9]+}/addDocumentAfter/{neighbourDocId: [0-9]+}")
    public Document addDocumentAfter(@PathParam("id") Long resourceId,
        @PathParam("neighbourDocId") Long neighbourDocId, Document document) {
        logger.debug("add the document {} after #{} in the resource #{}", document,
            neighbourDocId, resourceId);
        return resourceManager.addDocument(resourceId, document, RelatedPosition.AFTER,
            neighbourDocId);
    }

    /**
     * Remove the document of the resource.
     *
     * @param resourceId the id of the resource
     * @param documentId the id of the document to remove from the resource
     */
    @POST
    @Path("{id: [0-9]+}/removeDocument")
    public void removeDocument(@PathParam("id") Long resourceId, Long documentId) {
        logger.debug("add the document {} for the resource #{}", documentId, resourceId);

        resourceManager.removeDocument(resourceId, documentId);
    }

    // *********************************************************************************************
    // deletion
    // *********************************************************************************************

    /**
     * Permanently delete a resource
     *
     * @param id the id of the resource to delete
     */
    @DELETE
    @Path("{id: [0-9]+}")
    public void deleteResource(@PathParam("id") Long id) {
        logger.debug("delete resource #{}", id);
        resourceManager.deleteResource(id);
    }

    // *********************************************************************************************
    // category
    // *********************************************************************************************

    /**
     * Set the category of the resource
     *
     * @param resourceOrRefId the id of the resource / resource reference
     * @param categoryName    the name of the category that apply to the resource / resource
     *                        reference
     */
    @PUT
    @Path("changeCategory/{resourceOrRefId: [0-9]+}/{category : .*}")
    public void changeCategory(@PathParam("resourceOrRefId") Long resourceOrRefId,
        @PathParam("category") String categoryName) {
        logger.debug("add resource/ref #{} to category {}", resourceOrRefId, categoryName);
        resourceCategoryHelper.changeCategory(resourceOrRefId, categoryName);
    }

    /**
     * Set the category of a list of resources
     *
     * @param resourceOrRefIds the id of the resources / resource references
     * @param categoryName     the name of the category that apply to the resource / resource
     *                         reference
     */
    @PUT
    @Path("changeCategory/list/{newName : .*}")
    public void changeCategoryForList(@PathParam("newName") String categoryName,
        List<Long> resourceOrRefIds) {
        logger.debug("add resource/ref #{} to category {}", resourceOrRefIds, categoryName);
        resourceCategoryHelper.changeCategory(resourceOrRefIds, categoryName);
    }

    /**
     * Remove the category of the resource / resource reference
     *
     * @param resourceOrRefId the id of the resource / resource reference
     */
    @PUT
    @Path("removeCategory/{resourceOrRefId: [0-9]+}")
    public void removeCategory(@PathParam("resourceOrRefId") Long resourceOrRefId) {
        logger.debug("remove category from resource/ref #{}", resourceOrRefId);
        resourceCategoryHelper.changeCategory(resourceOrRefId, null);
    }

    /**
     * Remove the category of a list of resources / resource references
     *
     * @param resourceOrRefIds the id of the resources / resource references
     */
    @PUT
    @Path("removeCategory/list")
    public void removeCategoryForList(List<Long> resourceOrRefIds) {
        logger.debug("remove category from resource/ref #{}", resourceOrRefIds);
        resourceCategoryHelper.changeCategory(resourceOrRefIds, null);
    }

    /**
     * Rename the category in a card type / card type reference
     *
     * @param cardTypeOrRefId the id of the card type / card type reference (scope of the renaming)
     * @param oldName         the old name of the category
     * @param newName         the new name of the category
     */
    @PUT
    @Path("renameCategory/cardType/{cardTypeId: [0-9]+}/{oldName : .*}/{newName : .*}")
    public void renameCategoryForCardType(
        @PathParam("cardTypeId") Long cardTypeOrRefId, @PathParam("oldName") String oldName,
        @PathParam("newName") String newName) {
        logger.debug("rename category {} to {} for card type #{}", oldName,
            newName, cardTypeOrRefId);
        resourceCategoryHelper.renameCategoryInCardType(cardTypeOrRefId, oldName, newName);
    }

    /**
     * Rename the category in a card
     *
     * @param cardId  the id of the card
     * @param oldName the old name of the category
     * @param newName the new name of the category
     */
    @PUT
    @Path("renameCategory/card/{cardId: [0-9]+}/{oldName : .*}/{newName : .*}")
    public void renameCategoryForCard(@PathParam("cardId") Long cardId,
        @PathParam("oldName") String oldName, @PathParam("newName") String newName) {
        logger.debug("rename category {} to {} for card #{}", oldName, newName, cardId);
        resourceCategoryHelper.renameCategoryInCard(cardId, oldName, newName);
    }

    /**
     * Rename the category in a card content
     *
     * @param cardContentId the id of the card content
     * @param oldName       the old name of the category
     * @param newName       the new name of the category
     */
    @PUT
    @Path("renameCategory/cardContent/{cardContentId: [0-9]+}/{oldName : .*}/{newName : .*}")
    public void renameCategoryForCardContent(@PathParam("cardContentId") Long cardContentId,
        @PathParam("oldName") String oldName, @PathParam("newName") String newName) {
        logger.debug("rename category {} to {} for card content #{}", oldName, newName,
            cardContentId);
        resourceCategoryHelper.renameCategoryInCardContent(cardContentId, oldName, newName);
    }

    // *********************************************************************************************
    // links
    // *********************************************************************************************

    /**
     * Get the documents of the resource
     *
     * @param resourceId the id of the resource
     *
     * @return the documents linked to the resource
     */
    @GET
    @Path("{id: [0-9]+}/Documents")
    public List<Document> getDocumentsOfResource(@PathParam("id") Long resourceId) {
        logger.debug("Get the documents of the resource #{}", resourceId);
        return resourceManager.getDocumentsOfResource(resourceId);
    }

    /**
     * Get all sticky note links where the resource / resource reference is the source
     *
     * @param resourceOrRefId the id of the resource / resource reference
     *
     * @return list of links
     */
    @GET
    @Path("{id: [0-9]+}/StickyNoteLinks")
    public List<StickyNoteLink> getStickyNoteLinksAsSrc(@PathParam("id") Long resourceOrRefId) {
        logger.debug("Get sticky note links to abstract resource #{} as source", resourceOrRefId);
        return resourceManager.getStickyNoteLinkAsSrc(resourceOrRefId);
    }

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

}