TeamMember.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.team;

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.project.Project;
import ch.colabproject.colab.api.model.team.acl.Assignment;
import ch.colabproject.colab.api.model.team.acl.HierarchicalPosition;
import ch.colabproject.colab.api.model.tools.EntityHelper;
import ch.colabproject.colab.api.model.user.User;
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 java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
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.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.NamedQuery;
import javax.persistence.OneToMany;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import org.apache.commons.collections4.CollectionUtils;

/**
 * A member is a {@link User user} which work on a {@link Project project}
 *
 * @author maxence
 */
@Entity
@Table(
    indexes = {
        @Index(columnList = "project_id,user_id", unique = true),
        @Index(columnList = "project_id"),
        @Index(columnList = "user_id")
    }
)
@NamedQuery(
    name = "TeamMember.areUserTeammate",
    // SELECT true FROM TeamMember a, TeamMember b WHERE ...
    query = "SELECT true FROM TeamMember a "
        + "JOIN TeamMember b ON a.project.id = b.project.id "
        + "WHERE a.user.id = :aUserId AND b.user.id = :bUserId")
@NamedQuery(
    name = "TeamMember.findByProjectAndUser",
    query = "SELECT m FROM TeamMember m "
        + "WHERE m.project.id = :projectId "
        + "AND m.user IS NOT NULL AND m.user.id = :userId"
)
@NamedQuery(
    name = "TeamMember.findByUser",
    query = "SELECT m FROM TeamMember m "
        + "WHERE m.user IS NOT NULL AND m.user.id = :userId")
public class TeamMember implements ColabEntity, WithWebsocketChannels {

    private static final long serialVersionUID = 1L;

    /** project team sequence name */
    public static final String TEAM_SEQUENCE_NAME = "team_seq";

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

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

    /**
     * Optional display name. Such a name will hide user.commonName.
     */
    @Size(max = 255)
    private String displayName;

    /**
     * Hierarchical position of the member
     */
    @NotNull
    @Enumerated(value = EnumType.STRING)
    private HierarchicalPosition position = HierarchicalPosition.INTERNAL;

    /**
     * The user
     */
    @ManyToOne(fetch = FetchType.LAZY)
    @JsonbTransient
    private User user;

    /**
     * The user ID (serialization sugar)
     */
    @Transient
    private Long userId;

    /**
     * The project
     */
    @ManyToOne(fetch = FetchType.LAZY)
    @NotNull
    @JsonbTransient
    private Project project;

    /**
     * The project ID (serialization sugar)
     */
    @Transient
    private Long projectId;

    /**
     * The roles
     */
    @ManyToMany
    @JoinTable(indexes = {
        @Index(columnList = "members_id"),
        @Index(columnList = "roles_id"),
    })
    @JsonbTransient
    private List<TeamRole> roles = new ArrayList<>();

    /**
     * Id of the roles. For deserialization only
     */
    @NotNull
    @Transient
    private List<Long> roleIds = new ArrayList<>();

    /**
     * List of assignments relative to this member
     */
    @OneToMany(mappedBy = "member", cascade = CascadeType.ALL)
    @JsonbTransient
    private List<Assignment> assignments = new ArrayList<>();

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

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

    /**
     * @param id the member 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 displayName
     *
     * @return the value of displayName
     */
    public String getDisplayName() {
        return displayName;
    }

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

    /**
     * Get the hierarchical position of the member
     *
     * @return member's position
     */
    public HierarchicalPosition getPosition() {
        return position;
    }

    /**
     * Set hierarchical position of member
     *
     * @param position new position
     */
    public void setPosition(HierarchicalPosition position) {
        this.position = position;
    }

    /**
     * @return the user
     */
    public User getUser() {
        return user;
    }

    /**
     * @param user the user
     */
    public void setUser(User user) {
        this.user = user;
    }

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

    /**
     * set the user id. For serialization only
     *
     * @param id the id of the user
     */
    public void setUserId(Long id) {
        this.userId = id;
    }

    /**
     * @return the project
     */
    public Project getProject() {
        return project;
    }

    /**
     * @param project the project
     */
    public void setProject(Project project) {
        this.project = project;
    }

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

    /**
     * set the project id. For serialization only
     *
     * @param id the id of the project
     */
    public void setProjectId(Long id) {
        this.projectId = id;
    }

    /**
     * Get roles
     *
     * @return roles
     */
    public List<TeamRole> getRoles() {
        return roles;
    }

    /**
     * Set the list of roles
     *
     * @param roles list of roles
     */
    public void setRoles(List<TeamRole> roles) {
        this.roles = roles;
    }

    /**
     * Get ids of the roles.
     *
     * @return list of ids
     */
    public List<Long> getRoleIds() {
        if (!CollectionUtils.isEmpty(this.roles)) {
            return roles.stream()
                .map(role -> role.getId())
                .collect(Collectors.toList());
        }
        return roleIds;
    }

    /**
     * The the list of roleId
     *
     * @param roleIds id of roles
     */
    public void setRoleIds(List<Long> roleIds) {
        this.roleIds = roleIds;
    }

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

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

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

    @Override
    public void mergeToUpdate(ColabEntity other) throws ColabMergeException {
        if (other instanceof TeamMember) {
            TeamMember o = (TeamMember) other;
            this.setDeletionStatus(o.getDeletionStatus());
            this.setDisplayName(o.getDisplayName());
        } else {
            throw new ColabMergeException(this, other);
        }
    }

    @Override
    public void mergeToDuplicate(ColabEntity other) throws ColabMergeException {
        if (other instanceof TeamMember) {
            TeamMember o = (TeamMember) other;
            this.setDeletionStatus(o.getDeletionStatus());
            this.setDisplayName(o.getDisplayName());
            this.setPosition(o.getPosition());
        } else {
            throw new ColabMergeException(this, other);
        }
    }

    @Override
    public ChannelsBuilder getChannelsBuilder() {
        if (this.project != null) {
            return this.project.getChannelsBuilder();
        } else {
            // such an orphan shouldn't exist...
            return new EmptyChannelBuilder();
        }
    }

    @Override
    @JsonbTransient
    public Conditions.Condition getReadCondition() {
        if (this.user != null && this.project != null) {
            return new Conditions.IsCurrentUserMemberOfProject(project);
        } else {
            // anyone can read a pending invitation
            return Conditions.alwaysTrue;
        }
    }

    @Override
    @JsonbTransient
    public Conditions.Condition getUpdateCondition() {
        if (this.user != null && this.project != null) {
            return new Conditions.IsCurrentUserInternalToProject(project);
        } else {
            // anyone can read a pending invitation
            return Conditions.alwaysTrue;
        }
    }

    @Override
    @JsonbTransient
    public Conditions.Condition getCreateCondition() {
        if (this.project != null) {
            // any "internal" may invite somebody
            return new Conditions.IsCurrentUserInternalToProject(project);
        } else {
            return Conditions.alwaysFalse;
        }
    }

    @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() {
        if (user == null) {
            return "TeamMember{pending}";
        } else {
            return "TeamMember{" + "id=" + id + ", deletion=" + getDeletionStatus()
                + ", userId=" + userId + ", projectId=" + projectId + "}";
        }
    }

}