Card.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.card;

import ch.colabproject.colab.api.controller.card.grid.GridCellWithId;
import ch.colabproject.colab.api.controller.card.grid.GridPosition;
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.common.DeletionStatus;
import ch.colabproject.colab.api.model.common.Tracking;
import ch.colabproject.colab.api.model.document.AbstractResource;
import ch.colabproject.colab.api.model.document.Resourceable;
import ch.colabproject.colab.api.model.link.ActivityFlowLink;
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.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.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.api.ws.channel.tool.ChannelsBuilders.ProjectContentChannelBuilder;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import javax.json.bind.annotation.JsonbTransient;
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.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

/**
 * Card
 * <p>
 * It is defined by a cardType. The content is stored in one or several CardContent.
 *
 * @author sandra
 */
@Entity
@Table(
    indexes = {
        @Index(columnList = "cardtype_id"),
        @Index(columnList = "parent_id"),
    }
)
public class Card
    implements ColabEntity, WithWebsocketChannels, Resourceable, StickyNoteSourceable,
    GridCellWithId {

    private static final long serialVersionUID = 1L;

    /** Name of the project structure sequence */
    public static final String STRUCTURE_SEQUENCE_NAME = "structure_seq";

    // ---------------------------------------------------------------------------------------------
    // fields
    // ---------------------------------------------------------------------------------------------

    /**
     * Card ID
     */
    @Id
    @SequenceGenerator(name = STRUCTURE_SEQUENCE_NAME, allocationSize = 20)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = STRUCTURE_SEQUENCE_NAME)
    private 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;

    /**
     * Card title
     */
    @Size(max = 255)
    private String title;

    /**
     * The color of the card
     */
    @Size(max = 255)
    private String color;

    /**
     * The x coordinate of the card within its parent
     */
    @NotNull
    private Integer x = 1;

    /**
     * The y coordinate of the card within its parent
     */
    @NotNull
    private Integer y = 1;

    /**
     * The width of the card within its parent
     */
    @NotNull
    private Integer width = 1;

    /**
     * The height of the card within its parent
     */
    @NotNull
    private Integer height = 1;

    /**
     * The card type defining what is it for
     */
    @ManyToOne(fetch = FetchType.LAZY)
    @JsonbTransient
    private AbstractCardType cardType;

    /**
     * The id of the card type (serialization sugar)
     */
    @Transient
    private Long cardTypeId;

    /**
     * The parent card content
     * <p>
     * A card can either be the root card of a project or be within a card content
     */
    @ManyToOne(fetch = FetchType.LAZY)
    @JsonbTransient
    private CardContent parent;

    /**
     * The id of the parent card content (serialization sugar)
     */
    @Transient
    private Long parentId;

    /**
     * The project this card is root of. may be null
     */
    @OneToOne(mappedBy = "rootCard", fetch = FetchType.LAZY)
    @JsonbTransient
    private Project rootCardProject;

    /**
     * The id of the project for root cards (serialization sugar)
     */
    @Transient
    private Long rootCardProjectId;

    /**
     * The list of content variants.
     * <p>
     * There can be several variants of content
     */
    @OneToMany(mappedBy = "card", cascade = CascadeType.ALL)
    @JsonbTransient
    private List<CardContent> contentVariants = new ArrayList<>();

    /**
     * Assignments
     */
    // NB : Fetched eagerly because else it throws a silly org.postgresql.xa.PGXAException
    // when a member (internal, not owner) of the project tries to update a card / card content.
    // No idea why.
    @OneToMany(mappedBy = "card", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    @JsonbTransient
    private List<Assignment> assignments = new ArrayList<>();

    /**
     * The list of abstract resources directly linked to this card
     */
    @OneToMany(mappedBy = "card", cascade = CascadeType.ALL)
    @JsonbTransient
    private List<AbstractResource> directAbstractResources = new ArrayList<>();

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

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

    /**
     * The list of activity flow links of which the card is the previous one
     */
    @OneToMany(mappedBy = "previousCard", cascade = CascadeType.ALL)
    @JsonbTransient
    private List<ActivityFlowLink> activityFlowLinksAsPrevious = new ArrayList<>();

    /**
     * The list of activity flow links of which the card is the next one
     */
    @OneToMany(mappedBy = "nextCard", cascade = CascadeType.ALL)
    @JsonbTransient
    private List<ActivityFlowLink> activityFlowLinksAsNext = new ArrayList<>();

    // ---------------------------------------------------------------------------------------------
    // getters and setters
    // ---------------------------------------------------------------------------------------------

    /**
     * @return the card id
     */
    @Override
    public Long getId() {
        return id;
    }

    /**
     * @param id the card 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;
    }

    /**
     * Get the value of title
     *
     * @return the value of title
     */
    public String getTitle() {
        return title;
    }

    /**
     * Set the value of title
     *
     * @param title new value of title
     */
    public void setTitle(String title) {
        this.title = title;
    }

    /**
     * @return the color of the card
     */
    public String getColor() {
        return color;
    }

    /**
     * @param color the new color of the card
     */
    public void setColor(String color) {
        this.color = color;
    }

    @Override
    public Integer getX() {
        return x;
    }

    @Override
    public void setX(Integer x) {
        this.x = x;
    }

    @Override
    public Integer getY() {
        return y;
    }

    @Override
    public void setY(Integer y) {
        this.y = y;
    }

    @Override
    public Integer getWidth() {
        return width;
    }

    @Override
    public void setWidth(Integer width) {
        this.width = width;
    }

    @Override
    public Integer getHeight() {
        return height;
    }

    @Override
    public void setHeight(Integer height) {
        this.height = height;
    }

    /**
     * Move the card to the given position
     *
     * @param position new position
     */
    public void moveTo(GridPosition position) {
        this.setX(position.getX());
        this.setY(position.getY());
        this.setWidth(position.getWidth());
        this.setHeight(position.getHeight());
    }

    /**
     * @return the card type defining what is it for
     */
    public AbstractCardType getCardType() {
        return cardType;
    }

    /**
     * @param cardType the card type defining what is it for
     */
    public void setCardType(AbstractCardType cardType) {
        this.cardType = cardType;
    }

    /**
     * get the id of the card type. To be sent to client
     *
     * @return the id of the card type
     */
    public Long getCardTypeId() {
        if (this.cardType != null) {
            return this.cardType.getId();
        } else {
            return cardTypeId;
        }
    }

    /**
     * @param cardTypeId the card type id to set
     */
    public void setCardTypeId(Long cardTypeId) {
        this.cardTypeId = cardTypeId;
    }

    /**
     * @return True if it has a card type
     */
    public boolean hasCardType() {
        return cardType != null || cardTypeId != null;
    }

    /**
     * @return the parent card content
     *         <p>
     *         A card can either be the root card of a project or be within a card content
     */
    public CardContent getParent() {
        return parent;
    }

    /**
     * @param parent the parent card content
     */
    public void setParent(CardContent parent) {
        this.parent = parent;
    }

    /**
     * get the id of the parent card content. To be sent to client
     *
     * @return the id of the parent card content
     */
    public Long getParentId() {
        if (this.parent != null) {
            return parent.getId();
        } else {
            return parentId;
        }
    }

    /**
     * set the id of the parent card content. For serialization only
     *
     * @param parentId the id of the parent card content
     */
    public void setParentId(Long parentId) {
        this.parentId = parentId;
    }

    /**
     * Get the project this card is root of.
     *
     * @return the project
     */
    public Project getRootCardProject() {
        return rootCardProject;
    }

    /**
     * Set the project this card is the root of
     *
     * @param rootCardProject the project
     */
    public void setRootCardProject(Project rootCardProject) {
        this.rootCardProject = rootCardProject;
    }

    /**
     * get the id of the rootCardProject. To be sent to client.
     *
     * @return the id of the rootCardProject
     */
    public Long getRootCardProjectId() {
        if (this.rootCardProject != null) {
            return rootCardProject.getId();
        } else {
            return rootCardProjectId;
        }
    }

    /**
     * set the id of the rootCard project. For serialization only.
     *
     * @param rootCardProjectId the id of the root-card project
     */
    public void setRootCardProjectId(Long rootCardProjectId) {
        this.rootCardProjectId = rootCardProjectId;
    }

    /**
     * @return True if there is a project whose root card is this one
     */
    public boolean hasRootCardProject() {
        return rootCardProject != null || rootCardProjectId != null;
    }

    /**
     * @return the list of variants of card content
     */
    public List<CardContent> getContentVariants() {
        return contentVariants;
    }

    /**
     * @param contentVariantList the list of variants of card content
     */
    public void setContentVariants(List<CardContent> contentVariantList) {
        this.contentVariants = contentVariantList;
    }

    /**
     * Get assignments list
     *
     * @return Assignments
     */
    public List<Assignment> getAssignments() {
        return assignments;
    }

    /**
     * Get the assignments which match the given member
     *
     * @param member the member
     *
     * @return the assignments which match the member or null
     */
    public Assignment getAssignmentByMember(TeamMember member) {
        if (member != null) {
            Optional<Assignment> optAssignment = this.getAssignments().stream()
                .filter(assignment -> member.equals(assignment.getMember())).findFirst();
            return optAssignment.isPresent() ? optAssignment.get() : null;
        } else {
            return null;
        }
    }

    /**
     * Get the assignments which match the given role
     *
     * @param role the role
     *
     * @return the assignments which match the role or null
     */
    public Assignment getAssignmentsByRole(TeamRole role) {
        if (role != null) {
            Optional<Assignment> optAssignment = this.getAssignments().stream()
                .filter(assignment -> role.equals(assignment.getRole())).findFirst();

            return optAssignment.isPresent() ? optAssignment.get() : null;
        } else {
            return null;
        }
    }

    /**
     * Set the assignments list
     *
     * @param assignments new list
     */
    public void setAssignments(List<Assignment> assignments) {
        this.assignments = assignments;
    }

    /**
     * @return the list of abstract resources directly linked to this card
     */
    @Override
    public List<AbstractResource> getDirectAbstractResources() {
        return directAbstractResources;
    }

    /**
     * @param abstractResources the list of abstract resources directly linked to this card
     */
    public void setDirectAbstractResources(List<AbstractResource> abstractResources) {
        this.directAbstractResources = abstractResources;
    }

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

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

    /**
     * @return the list of sticky note links of which the card is the destination
     */
    public List<StickyNoteLink> getStickyNoteLinksAsDest() {
        return stickyNoteLinksAsDest;
    }

    /**
     * @param links the list of sticky note links of which the card is the destination
     */
    public void setStickyNoteLinksAsDest(List<StickyNoteLink> links) {
        this.stickyNoteLinksAsDest = links;
    }

    /**
     * @return the list of activity flow links of which the card is the previous one
     */
    public List<ActivityFlowLink> getActivityFlowLinksAsPrevious() {
        return activityFlowLinksAsPrevious;
    }

    /**
     * @param links list of activity flow links of which the card is the previous one
     */
    public void setActivityFlowLinksAsPrevious(List<ActivityFlowLink> links) {
        this.activityFlowLinksAsPrevious = links;
    }

    /**
     * @return the list of activity flow links of which the card is the next one
     */
    public List<ActivityFlowLink> getActivityFlowLinksAsNext() {
        return activityFlowLinksAsNext;
    }

    /**
     * @param links the list of activity flow links of which the card is the next one
     */
    public void setActivityFlowLinksAsNext(List<ActivityFlowLink> links) {
        this.activityFlowLinksAsNext = links;
    }

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

    @Override
    public void mergeToUpdate(ColabEntity other) throws ColabMergeException {
        if (other instanceof Card) {
            Card o = (Card) other;
            this.setDeletionStatus(o.getDeletionStatus());
            this.setTitle(o.getTitle());
            this.setColor(o.getColor());
            // do not update position
        } else {
            throw new ColabMergeException(this, other);
        }
    }

    @Override
    public void mergeToDuplicate(ColabEntity other) throws ColabMergeException {
        // same as merge to update but copy position too
        this.mergeToUpdate(other);
        if (other instanceof Card) {
            Card o = (Card) other;
            this.setDeletionStatus(o.getDeletionStatus());
            this.setX(o.getX());
            this.setY(o.getY());
            this.setWidth(o.getWidth());
            this.setHeight(o.getHeight());
        }
    }

    /**
     * Get the project this card belongs to.
     *
     * @return card owner
     */
    @JsonbTransient
    public Project getProject() {
        if (this.rootCardProject != null) {
            // this card is a root card
            return this.rootCardProject;
        } else if (this.cardType != null) {
            // the card type has a direct access to project
            return this.cardType.getProject();
        } else if (this.parent != null) {
            // nothing easier, so get the one of its parent
            return this.parent.getProject();
        }
        return null;
    }

    @Override
    public ChannelsBuilder getChannelsBuilder() {
        if (this.rootCardProject != null) {
            // this card is a root card, propagate through the project content channel
            return new ProjectContentChannelBuilder(this.rootCardProject);
        } else if (this.parent != null) {
            // this card is a sub-card, propagate through its parent channels
            return this.parent.getChannelsBuilder();
        } else if (this.cardType != null) {
            // such a card shouldn't exist...
            // Lorem-ipsum cards for global cardTypes ???
            return this.cardType.getChannelsBuilder();
        } else {
            // such an orphan shouldn't exist...
            return new EmptyChannelBuilder();
        }
    }

    @Override
    @JsonbTransient
    public Conditions.Condition getReadCondition() {
        // genuine hack inside
        // any member can read any card of the project
        // if a member lacks the read right on a card, it will not be able to read the deliverable,
        // resources and so on, but it will still be able to view the card "from the outside"
        return new Conditions.IsCurrentUserMemberOfProject(getProject());
    }

    @Override
    @JsonbTransient
    public Conditions.Condition getUpdateCondition() {
        return new Conditions.HasCardWriteRight(this);
    }

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

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

    @Override
    public String toString() {
        return "Card{" + "id=" + id + ", deletion=" + getDeletionStatus()
            + ", xy=(" + x + "," + y + "), size=" + width + "x" + height + ", color=" + color
            + ", cardTypeId=" + cardTypeId + ", parentId=" + parentId + "}";
    }
}