Task.java

/*
 * BSD 2-Clause License
 * 
 * Copyright (c) 2022, [Aleksandra Serba, Marcin Czerniak, Bartosz Wawrzyniak, Adrian Antkowiak]
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 * 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package dev.vernite.vernite.task;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OrderBy;
import jakarta.persistence.PrePersist;
import jakarta.persistence.PreUpdate;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.PositiveOrZero;

import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonProperty.Access;

import dev.vernite.vernite.integration.git.Issue;
import dev.vernite.vernite.integration.git.PullRequest;
import dev.vernite.vernite.integration.git.github.model.TaskIntegration;
import dev.vernite.vernite.release.Release;
import dev.vernite.vernite.sprint.Sprint;
import dev.vernite.vernite.status.Status;
import dev.vernite.vernite.task.comment.Comment;
import dev.vernite.vernite.task.time.TimeTrack;
import dev.vernite.vernite.user.User;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Data
@Entity
@NoArgsConstructor
@JsonInclude(Include.NON_NULL)
@EntityListeners(TaskListener.class)
public class Task {

    public enum Type {
        TASK, USER_STORY, ISSUE, EPIC, SUBTASK;

        public boolean isValidParent(Type parent) {
            if (this == parent) {
                return false;
            }
            switch (this) {
                case EPIC:
                    return false;
                case TASK:
                    return parent == EPIC;
                case USER_STORY:
                    return parent == EPIC;
                case ISSUE:
                    return parent == EPIC || parent == TASK;
                case SUBTASK:
                    return parent != EPIC;
                default:
                    return false;
            }
        }
    }

    @Id
    @JsonIgnore
    @PositiveOrZero
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @Positive
    @JsonProperty("id")
    @Column(nullable = false)
    private long number;

    @NotBlank
    @Column(nullable = false, length = 100)
    private String name;

    @ManyToOne
    @JsonIgnore
    @ToString.Exclude
    @EqualsAndHashCode.Exclude
    private Sprint sprint;

    @NotNull
    @JsonIgnore
    @ToString.Exclude
    @EqualsAndHashCode.Exclude
    @ManyToMany(cascade = CascadeType.MERGE)
    @OnDelete(action = OnDeleteAction.CASCADE)
    private Set<Sprint> archiveSprints = new HashSet<>();

    @NotNull
    @Column(nullable = false, length = 1000)
    private String description;

    @NotNull
    @Column(nullable = false)
    private Date createdAt;

    @NotNull
    @JsonIgnore
    @ManyToOne
    @OnDelete(action = OnDeleteAction.CASCADE)
    @JoinColumn(nullable = false)
    @ToString.Exclude
    @EqualsAndHashCode.Exclude
    private Status status;

    @NotNull
    @JsonIgnore
    @ManyToOne
    @ToString.Exclude
    @EqualsAndHashCode.Exclude
    @OnDelete(action = OnDeleteAction.CASCADE)
    @JoinColumn(name = "created_by", nullable = false)
    private User user;

    @JsonIgnore
    @ManyToOne
    @ToString.Exclude
    @EqualsAndHashCode.Exclude
    @OnDelete(action = OnDeleteAction.CASCADE)
    @JoinColumn(nullable = true, name = "assignee")
    private User assignee;

    @Column(nullable = false)
    private int type;

    @JsonIgnore
    @ManyToOne
    @JoinColumn(nullable = true)
    @ToString.Exclude
    @EqualsAndHashCode.Exclude
    private Task parentTask;

    @NotNull
    @ToString.Exclude
    @EqualsAndHashCode.Exclude
    @OnDelete(action = OnDeleteAction.CASCADE)
    @OneToMany(mappedBy = "parentTask")
    @OrderBy("name, id")
    private List<Task> subTasks = new ArrayList<>();

    private Date deadline;
    private Date estimatedDate;

    @NotNull
    @Column(nullable = false)
    private String priority;

    @ToString.Exclude
    @EqualsAndHashCode.Exclude
    @NotNull
    @OneToMany(mappedBy = "task")
    @OnDelete(action = OnDeleteAction.CASCADE)
    private List<TimeTrack> timeTracks = new ArrayList<>();

    @Column(nullable = false)
    private long storyPoints;

    @ToString.Exclude
    @EqualsAndHashCode.Exclude
    @ManyToOne
    @JsonIgnore
    private Release release;

    @NotNull
    private Date lastUpdated;

    @NotNull
    @ToString.Exclude
    @EqualsAndHashCode.Exclude
    @JsonIgnore
    @OrderBy("createdAt DESC")
    @OneToMany(mappedBy = "task")
    @OnDelete(action = OnDeleteAction.CASCADE)
    private List<Comment> comments = new ArrayList<>();

    @NotNull
    @JsonIgnore
    @ToString.Exclude
    @EqualsAndHashCode.Exclude
    @OneToMany(mappedBy = "task")
    @OnDelete(action = OnDeleteAction.CASCADE)
    private List<TaskIntegration> gitHubTaskIntegrations = new ArrayList<>();

    /**
     * Default constructor for Task.
     * 
     * @param id          ID of the task
     * @param name        name of the task
     * @param description description of the task
     * @param status      status of the task
     * @param user        user who created the task
     * @param type        type of the task
     * @param priority    priority of the task
     */
    public Task(long id, String name, String description, Status status, User user, int type, String priority) {
        this.number = id;
        setName(name);
        setDescription(description);
        this.status = status;
        this.user = user;
        this.type = type;
        this.priority = priority;
        this.createdAt = new Date();
    }

    /**
     * Constructor for Task from request.
     * 
     * @param id     ID of the task
     * @param status status of the task
     * @param user   user who created the task
     * @param create request for creating task
     */
    public Task(long id, Status status, User user, CreateTask create) {
        this(id, create.getName(), create.getDescription(), status, user, create.getType(), create.getPriority());
        this.deadline = create.getDeadline();
        this.estimatedDate = create.getEstimatedDate();

        if (create.getStoryPoints() != null) {
            this.storyPoints = create.getStoryPoints();
        }
    }

    @Deprecated
    public Task(long id, String name, String description, Status status, User user, int type) {
        this(id, name, description, status, user, type, "low");
    }

    /**
     * Updates task with non-empty request fields.
     * 
     * @param update must not be {@literal null}. When fields are not present in
     *               request, they are not updated.
     */
    public void update(UpdateTask update) {
        if (update.getDeadline() != null) {
            this.setDeadline(update.getDeadline());
        }
        if (update.getEstimatedDate() != null) {
            this.setEstimatedDate(update.getEstimatedDate());
        }
        if (update.getPriority() != null) {
            this.setPriority(update.getPriority());
        }
        if (update.getName() != null) {
            this.setName(update.getName());
        }
        if (update.getDescription() != null) {
            this.setDescription(update.getDescription());
        }
        if (update.getType() != null) {
            this.setType(update.getType());
        }
        if (update.getStoryPoints() != null) {
            this.setStoryPoints(update.getStoryPoints());
        }
    }

    /**
     * Sets the name of the task. Trims the name.
     * 
     * @param name name of the task
     */
    public void setName(String name) {
        this.name = name.trim();
    }

    /**
     * Sets the description of the task. Trims the description.
     * 
     * @param description description of the task
     */
    public void setDescription(String description) {
        this.description = description.trim();
    }

    public long getStatusId() {
        return this.getStatus().getId();
    }

    public long getCreatedBy() {
        return this.getUser().getId();
    }

    public Long getAssigneeId() {
        return this.getAssignee() == null ? null : this.getAssignee().getId();
    }

    public void changeStatus(boolean isOpen) {
        if (isOpen && !getStatus().isBegin()) {
            for (Status newStatus : getStatus().getProject().getStatuses()) {
                if (newStatus.isBegin()) {
                    setStatus(newStatus);
                    break;
                }
            }
        } else if (!isOpen && !getStatus().isFinal()) {
            for (Status newStatus : getStatus().getProject().getStatuses()) {
                if (newStatus.isFinal()) {
                    setStatus(newStatus);
                    break;
                }
            }
        }
    }

    public List<Task> getSubTasks() {
        return type == Type.EPIC.ordinal() ? List.of() : subTasks;
    }

    public Long getParentTaskId() {
        return this.parentTask != null ? this.parentTask.getNumber() : null;
    }

    public Long getReleaseId() {
        return this.release != null ? this.release.getId() : null;
    }

    public Long getSprintId() {
        return this.getSprint() == null ? null : this.getSprint().getId();
    }

    @JsonProperty(access = Access.READ_ONLY)
    public List<Long> getArchivedSprintIds() {
        return this.getArchiveSprints().stream().map(Sprint::getId).toList();
    }

    @PreUpdate
    @PrePersist
    private void updateDate() {
        this.setLastUpdated(new Date());
    }

    public PullRequest getPull() {
        for (var integration : getGitHubTaskIntegrations()) {
            if (integration.getId().getType() == TaskIntegration.Type.PULL_REQUEST.ordinal()) {
                var pull = new PullRequest(integration.getIssueId(), integration.link(), getName(), getDescription(),
                        "github", integration.getBranch());
                if (integration.isMerged()) {
                    pull.setState("merged");
                }
                return pull;
            }
        }
        return null;
    }

    public Issue getIssue() {
        for (var integration : getGitHubTaskIntegrations()) {
            if (integration.getId().getType() == TaskIntegration.Type.ISSUE.ordinal()) {
                return integration.toIssue();
            }
        }
        return null;
    }

    public long getProjectId() {
        return this.getStatus().getProject().getId();
    }
}