AbstractResource.java

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

import static ch.colabproject.colab.api.model.document.Document.DOCUMENT_SEQUENCE_NAME;
import ch.colabproject.colab.api.exceptions.ColabMergeException;
import ch.colabproject.colab.api.model.ColabEntity;
import ch.colabproject.colab.api.model.WithWebsocketChannels;
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.DeletionStatus;
import ch.colabproject.colab.api.model.common.Tracking;
import ch.colabproject.colab.api.model.link.StickyNoteLink;
import ch.colabproject.colab.api.model.link.StickyNoteSourceable;
import ch.colabproject.colab.api.model.project.Project;
import ch.colabproject.colab.api.model.tools.EntityHelper;
import ch.colabproject.colab.api.security.permissions.Conditions;
import ch.colabproject.colab.api.ws.channel.tool.ChannelsBuilders.ChannelsBuilder;
import ch.colabproject.colab.api.ws.channel.tool.ChannelsBuilders.EmptyChannelBuilder;
import ch.colabproject.colab.generator.model.exceptions.HttpErrorMessage;
import ch.colabproject.colab.generator.model.exceptions.MessageI18nKey;
import ch.colabproject.colab.generator.model.tools.PolymorphicDeserializer;
import java.util.ArrayList;
import java.util.List;
import javax.json.bind.annotation.JsonbTransient;
import javax.json.bind.annotation.JsonbTypeDeserializer;
import javax.persistence.CascadeType;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Index;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.validation.constraints.Size;

/**
 * A resource is a document provided to help the users to fulfill their goals.
 * <p>
 * An abstract resource is either a resource directly representing a document or a reference to
 * another abstract resource.
 * <p>
 * An abstract resource can be owned either by a card type / card type reference, or by a card by to
 * a card content.
 *
 * @author sandra
 */
@Entity
@Table(
    indexes = {
        @Index(columnList = "abstractcardtype_id"),
        @Index(columnList = "card_id"),
        @Index(columnList = "cardcontent_id"),
    }
)
@Inheritance(strategy = InheritanceType.JOINED)
@JsonbTypeDeserializer(PolymorphicDeserializer.class)
public abstract class AbstractResource
    implements ColabEntity, WithWebsocketChannels, StickyNoteSourceable {

    private static final long serialVersionUID = 1L;

    // ---------------------------------------------------------------------------------------------
    // fields
    // ---------------------------------------------------------------------------------------------
    /**
     * Abstract resource ID
     */
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = DOCUMENT_SEQUENCE_NAME)
    protected Long id;

    /**
     * creation + modification + erasure tracking data
     */
    @Embedded
    private Tracking trackingData;

    /**
     * Is it in a bin or ready to be definitely deleted. Null means active.
     */
    @Enumerated(EnumType.STRING)
    private DeletionStatus deletionStatus;

    /**
     * The category to classify the resource
     */
    @Size(max = 255)
    private String category;

    /**
     * The card type / card type reference to which the abstract resource is linked
     */
    @ManyToOne(fetch = FetchType.LAZY)
    @JsonbTransient
    private AbstractCardType abstractCardType;

    /**
     * The card type id / card type reference id (serialization sugar)
     */
    @Transient
    private Long abstractCardTypeId;

    /**
     * The card to which the abstract resource is linked
     */
    @ManyToOne(fetch = FetchType.LAZY)
    @JsonbTransient
    private Card card;

    /**
     * The card id (serialization sugar)
     */
    @Transient
    private Long cardId;

    /**
     * The card content to which the abstract resource is linked
     */
    @ManyToOne(fetch = FetchType.LAZY)
    @JsonbTransient
    private CardContent cardContent;

    /**
     * The card content id (serialization sugar)
     */
    @Transient
    private Long cardContentId;

    /**
     * The list of sticky note links of which the resource is the source
     */
    @OneToMany(mappedBy = "srcResourceOrRef", cascade = CascadeType.ALL)
    @JsonbTransient
    private List<StickyNoteLink> stickyNoteLinksAsSrc = new ArrayList<>();

    // Note : the List<ResourceRef> of direct references must be retrieved with a DAO
    // because the abstract resource must not be seen as changed when a reference is added or
    // removed

    // ---------------------------------------------------------------------------------------------
    // getters and setters
    // ---------------------------------------------------------------------------------------------
    /**
     * @return the abstract resource id
     */
    @Override
    public Long getId() {
        return id;
    }

    /**
     * @param id the abstract resource id
     */
    public void setId(Long id) {
        this.id = id;
    }

    /**
     * Get the tracking data
     *
     * @return tracking data
     */
    @Override
    public Tracking getTrackingData() {
        return trackingData;
    }

    /**
     * Set tracking data
     *
     * @param trackingData new tracking data
     */
    @Override
    public void setTrackingData(Tracking trackingData) {
        this.trackingData = trackingData;
    }

    @Override
    public DeletionStatus getDeletionStatus() {
        return deletionStatus;
    }

    @Override
    public void setDeletionStatus(DeletionStatus status) {
        this.deletionStatus = status;
    }

    /**
     * @return the category to classify the resource
     */
    public String getCategory() {
        return category;
    }

    /**
     * @param category the category to classify the resource
     */
    public void setCategory(String category) {
        this.category = category;
    }

    /**
     * @return the card type / card type reference to which an abstract resource is linked
     */
    public AbstractCardType getAbstractCardType() {
        return abstractCardType;
    }

    /**
     * @param abstractCardType the card type / card type reference to which an abstract resource is
     *                         linked
     */
    public void setAbstractCardType(AbstractCardType abstractCardType) {
        this.abstractCardType = abstractCardType;
    }

    /**
     * get the id of the card type / card type reference to which an abstract resource is linked. To
     * be sent to client.
     *
     * @return the ID of the abstract card type
     */
    public Long getAbstractCardTypeId() {
        if (this.abstractCardType != null) {
            return abstractCardType.getId();
        } else {
            return abstractCardTypeId;
        }
    }

    /**
     * set the id of the card type / card type reference to which an abstract resource is linked.
     * For serialization only.
     *
     * @param abstractCardTypeId the ID of the abstract card type
     */
    public void setAbstractCardTypeId(Long abstractCardTypeId) {
        this.abstractCardTypeId = abstractCardTypeId;
    }

    /**
     * @return True if there is a linked abstract card type
     */
    public boolean hasAbstractCardType() {
        return abstractCardType != null || abstractCardTypeId != null;
    }

    /**
     * @return the card to which an abstract resource is linked
     */
    public Card getCard() {
        return card;
    }

    /**
     * @param card the card to which an abstract resource is linked
     */
    public void setCard(Card card) {
        this.card = card;
    }

    /**
     * get the id of the card to which an abstract resource is linked. To be sent to client.
     *
     * @return the ID of the card
     */
    public Long getCardId() {
        if (this.card != null) {
            return card.getId();
        } else {
            return cardId;
        }
    }

    /**
     * set the id of the card to which an abstract resource is linked. For serialization only.
     *
     * @param cardId the ID of the card
     */
    public void setCardId(Long cardId) {
        this.cardId = cardId;
    }

    /**
     * @return True if there is a linked card
     */
    public boolean hasCard() {
        return card != null || cardId != null;
    }

    /**
     * @return the card content to which an abstract resource is linked
     */
    public CardContent getCardContent() {
        return cardContent;
    }

    /**
     * @param cardContent the card content to which an abstract resource is linked
     */
    public void setCardContent(CardContent cardContent) {
        this.cardContent = cardContent;
    }

    /**
     * get the id of the card content to which an abstract resource is linked. To be sent to client.
     *
     * @return the ID of the card content
     */
    public Long getCardContentId() {
        if (this.cardContent != null) {
            return cardContent.getId();
        } else {
            return cardContentId;
        }
    }

    /**
     * set the id of the card content to which an abstract resource is linked. For serialization
     * only.
     *
     * @param cardContentId the ID of the card content
     */
    public void setCardContentId(Long cardContentId) {
        this.cardContentId = cardContentId;
    }

    /**
     * @return True if there is a linked card content
     */
    public boolean hasCardContent() {
        return cardContent != null || cardContentId != null;
    }

    /**
     * @return the list of sticky note links of which the resource is the source
     */
    @Override
    public List<StickyNoteLink> getStickyNoteLinksAsSrc() {
        return stickyNoteLinksAsSrc;
    }

    /**
     * @param stickyNoteLinksAsSrc the list of sticky note links of which the resource is the source
     */
    public void setStickyNoteLinksAsSrc(List<StickyNoteLink> stickyNoteLinksAsSrc) {
        this.stickyNoteLinksAsSrc = stickyNoteLinksAsSrc;
    }

    // ---------------------------------------------------------------------------------------------
    // helpers
    // ---------------------------------------------------------------------------------------------

    /**
     * @return the owner of the resource
     */
    @JsonbTransient
    public Resourceable getOwner() {
        if (this.card != null) {
            return this.card;
        }
        if (this.cardContent != null) {
            return this.cardContent;
        }
        if (this.abstractCardType != null) {
            return this.abstractCardType;
        }
        throw HttpErrorMessage.dataError(MessageI18nKey.DATA_INTEGRITY_FAILURE);
    }

    /**
     * @param owner the owner of the resource
     */
    public void setOwner(Resourceable owner) {
        if (owner == null) {
            resetOwner();
        } else if (owner instanceof Card) {
            resetOwner();
            setCard((Card) owner);
        } else if (owner instanceof CardContent) {
            resetOwner();
            setCardContent((CardContent) owner);
        } else if (owner instanceof AbstractCardType) {
            resetOwner();
            setAbstractCardType((AbstractCardType) owner);
        } else {
            throw HttpErrorMessage.dataError(MessageI18nKey.DATA_INTEGRITY_FAILURE);
        }
    }

    /**
     * Set every owner to null
     */
    private void resetOwner() {
        setCard(null);
        setCardId(null);
        setCardContent(null);
        setCardContentId(null);
        setAbstractCardType(null);
        setAbstractCardTypeId(null);
    }

    // ---------------------------------------------------------------------------------------------
    // helpers
    // ---------------------------------------------------------------------------------------------

    /**
     * Resolve to concrete Resource
     *
     * @return the effective resource
     */
    public abstract Resource resolve();

    /**
     * Resolve to concrete resource and return all transitive references too.
     *
     * @return concrete resource and transitive references
     */
    public abstract List<AbstractResource> expand();

    // ---------------------------------------------------------------------------------------------
    // concerning the whole class
    // ---------------------------------------------------------------------------------------------

    @Override
    public void mergeToUpdate(ColabEntity other) throws ColabMergeException {
        if (other instanceof AbstractResource) {
            AbstractResource o = (AbstractResource) other;
            this.setDeletionStatus(o.getDeletionStatus());
            // category cannot be changed alone manually. It is handled by ResourceCategoryHelper
        } else {
            throw new ColabMergeException(this, other);
        }
    }

    @Override
    public void mergeToDuplicate(ColabEntity other) throws ColabMergeException {
        if (other instanceof AbstractResource) {
            AbstractResource o = (AbstractResource) other;
            this.setDeletionStatus(o.getDeletionStatus());
            this.setCategory(o.getCategory());
        } else {
            throw new ColabMergeException(this, other);
        }
    }

    /**
     * Get the project this content belongs to
     *
     * @return content owner
     */
    @JsonbTransient
    public Project getProject() {
        if (this.abstractCardType != null) {
            // the abstract resource is linked to a card type / card type reference
            return this.abstractCardType.getProject();
        } else if (this.card != null) {
            // the abstract resource is linked to a card
            return this.card.getProject();
        } else if (this.cardContent != null) {
            // the abstract resource is linked to a card content
            return this.cardContent.getProject();
        } else {
            // such an orphan shouldn't exist...
            return null;
        }
    }

    @Override
    public ChannelsBuilder getChannelsBuilder() {
        if (this.abstractCardType != null) {
            // the abstract resource is linked to a card type / card type reference
            return this.abstractCardType.getChannelsBuilder();
        } else if (this.card != null) {
            // the abstract resource is linked to a card
            return this.card.getChannelsBuilder();
        } else if (this.cardContent != null) {
            // the abstract resource is linked to a card content
            return this.cardContent.getChannelsBuilder();
        } else {
            // such an orphan shouldn't exist...
            return new EmptyChannelBuilder();
        }
    }

    @Override
    @JsonbTransient
    public Conditions.Condition getReadCondition() {
        if (this.abstractCardType != null) {
            // the abstract resource is linked to a card type / card type reference
            return this.abstractCardType.getReadCondition();
        } else if (this.card != null) {
            // the abstract resource is linked to a card
            return new Conditions.HasCardReadRight(this.card);
        } else if (this.cardContent != null) {
            // the abstract resource is linked to a card content
            return this.cardContent.getReadCondition();
        } else {
            // such an orphan shouldn't exist...
            return Conditions.defaultForOrphan;
        }
    }

    @Override
    @JsonbTransient
    public Conditions.Condition getUpdateCondition() {
        if (this.abstractCardType != null) {
            // the abstract resource is linked to a card type / card type reference
            return this.abstractCardType.getUpdateCondition();
        } else if (this.card != null) {
            // the abstract resource is linked to a card
            return this.card.getUpdateCondition();
        } else if (this.cardContent != null) {
            // the abstract resource is linked to a card content
            return this.cardContent.getUpdateCondition();
        } else {
            // such an orphan shouldn't exist...
            return Conditions.defaultForOrphan;
        }
    }

    @Override
    public int hashCode() {
        return EntityHelper.hashCode(this);
    }

    @Override
    @SuppressWarnings("EqualsWhichDoesntCheckParameterClass")
    public boolean equals(Object obj) {
        return EntityHelper.equals(this, obj);
    }

    /**
     * @return string representation of its fields
     */
    protected String toPartialString() {
        return "id=" + id + ", deletion=" + getDeletionStatus()
            + ", abstractCardTypeId=" + abstractCardTypeId + ", cardId=" + cardId
            + ", cardContentId=" + cardContentId + ", category=" + category;
    }

}