User.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.user;

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.tools.EntityHelper;
import ch.colabproject.colab.api.security.permissions.Conditions;
import ch.colabproject.colab.api.ws.channel.tool.ChannelsBuilders.AboutUserChannelsBuilder;
import ch.colabproject.colab.api.ws.channel.tool.ChannelsBuilders.ChannelsBuilder;
import ch.colabproject.colab.generator.model.tools.DateSerDe;
import org.apache.commons.lang3.StringUtils;

import javax.json.bind.annotation.JsonbTransient;
import javax.json.bind.annotation.JsonbTypeDeserializer;
import javax.json.bind.annotation.JsonbTypeSerializer;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.List;

/**
 * Represents a registered user. A user may authenticate by several means (accounts).
 *
 * @author maxence
 */
@Entity
@Table(name = "users", indexes = {
    @Index(columnList = "username", unique = true), })
@NamedQuery(name = "User.findAll", query = "SELECT u from User u")
@NamedQuery(name = "User.findByUsername",
    query = "SELECT u from User u where u.username = :username")
@NamedQuery(name = "User.findAllAdmin",
    query = "SELECT u from User u where u.isAdmin = TRUE")
public class User implements ColabEntity, WithWebsocketChannels {

    private static final long serialVersionUID = 1L;

    /** user sequence name */
    public static final String USER_SEQUENCE_NAME = "user_seq";

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

    /**
     * User unique id
     */
    @Id
    @SequenceGenerator(name = USER_SEQUENCE_NAME, allocationSize = 20)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = USER_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;

    /**
     * is the user administrator ?
     */
    private boolean isAdmin;

    /**
     * persisted last-activity date
     */
    @JsonbTransient
    private OffsetDateTime lastSeenAt = null;

    /**
     * Last activity date
     */
    @Transient
    @JsonbTypeDeserializer(DateSerDe.class)
    @JsonbTypeSerializer(DateSerDe.class)
    private OffsetDateTime activityDate = null;

    /**
     * persisted terms of use and data policy agreement time
     */
    @JsonbTypeDeserializer(DateSerDe.class)
    @JsonbTypeSerializer(DateSerDe.class)
    private OffsetDateTime agreedTime = null;

    /**
     * Firstname
     */
    @Size(max = 255)
    private String firstname;

    /**
     * Lastname
     */
    @Size(max = 255)
    private String lastname;

    /**
     * short name to be displayed
     */
    @Size(max = 255)
    private String commonname;

    /**
     * User affiliation
     */
    @Size(max = 255)
    private String affiliation;

    /**
     * System-wide unique name. Alphanumeric only
     */
    @Pattern(regexp = "[a-zA-Z0-9_\\-\\.]+")
    @Size(max = 255)
    @NotNull
    private String username;

    /**
     * List of accounts the user can authenticate with
     */
    @OneToMany(mappedBy = "user", cascade = { CascadeType.PERSIST, CascadeType.REMOVE })
    @JsonbTransient
    private List<Account> accounts = new ArrayList<>();

    // Note : the TeamMember list must be retrieved with a DAO
    // because the user must not be seen as changed when a team member is added or removed

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

    /**
     * @return Id of the user
     */
    @Override
    public Long getId() {
        return id;
    }

    /**
     * Set user id
     *
     * @param id id of the user
     */
    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 isAdmin
     */
    public boolean isAdmin() {
        return isAdmin;
    }

    /**
     * @param isAdmin the isAdmin to set
     */
    public void setAdmin(boolean isAdmin) {
        this.isAdmin = isAdmin;
    }

    /**
     * @return last seen at
     */
    public OffsetDateTime getLastSeenAt() {
        return lastSeenAt;
    }

    /**
     * update most recent activity date of the user
     *
     * @param lastSeenAt the lastSeenAt date
     */
    public void setLastSeenAt(OffsetDateTime lastSeenAt) {
        this.lastSeenAt = lastSeenAt;
    }

    /**
     * Get last known activity date.
     *
     * @return transient <code>this.activityDate</code> if it exists or persisted
     *         <code>this.lastSeenAt</code> otherwise
     */
    public OffsetDateTime getActivityDate() {
        if (activityDate != null) {
            return activityDate;
        } else {
            return lastSeenAt;
        }
    }

    /**
     * Set transient activity date
     *
     * @param activityDate new activityDate
     */
    public void setActivityDate(OffsetDateTime activityDate) {
        this.activityDate = activityDate;
    }

    /**
     *
     *
     * @return user agreedTime
     */
    public OffsetDateTime getAgreedTime() { return agreedTime; }

    /**
     * Set user agreedTime
     *
     * @param agreedTime new agreedTime
     */
    public void setAgreedTime(OffsetDateTime agreedTime) { this.agreedTime = agreedTime; }

    /**
     * @return user first name, may be null or empty
     */
    public String getFirstname() {
        return firstname;
    }

    /**
     * Set user first name
     *
     * @param firstname user first name, may be null or empty
     */
    public void setFirstname(String firstname) {
        this.firstname = firstname;
    }

    /**
     * @return user last name, may be null or empty
     */
    public String getLastname() {
        return lastname;
    }

    /**
     * @param lastname user last name, may be null or empty
     */
    public void setLastname(String lastname) {
        this.lastname = lastname;
    }

    /**
     * Short display name to use
     *
     * @return user common name
     */
    public String getCommonname() {
        return commonname;
    }

    /**
     * @param commonname user common name
     */
    public void setCommonname(String commonname) {
        this.commonname = commonname;
    }

    /**
     * Get the value of affiliation
     *
     * @return the value of affiliation
     */
    public String getAffiliation() {
        return affiliation;
    }

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

    /**
     * Get user's username. This name is unique system-wide
     *
     * @return username
     */
    public String getUsername() {
        return username;
    }

    /**
     * Change the user username. This is sensitive operation as this username is used to reference
     * the user within blocks and so on. Final user shouldn't be able to update its name itself.
     *
     * @param username new username
     */
    public void setUsername(String username) {
        this.username = username;
    }

    /**
     * @return user accounts
     */
    public List<Account> getAccounts() {
        return accounts;
    }

    /**
     * Set user accounts
     *
     * @param accounts account list
     */
    public void setAccounts(List<Account> accounts) {
        this.accounts = accounts;
    }

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

    /**
     * get most preferred name to display.
     *
     * @return name to display
     */
    @JsonbTransient
    public String getDisplayName() {
        StringBuilder sb = new StringBuilder();

        if (StringUtils.isNotBlank(this.firstname)) {
            sb.append(this.firstname);
        }
        if (StringUtils.isNotBlank(this.firstname) && StringUtils.isNotBlank(this.lastname)) {
            sb.append(" ");
        }
        if (StringUtils.isNotBlank(this.lastname)) {
            sb.append(this.lastname);
        }

        String toString = sb.toString();
        if (StringUtils.isNotBlank(toString)) {
            return toString;
        }

        return "Someone";
    }

    // ---------------------------------------------------------------------------------------------
    // init
    // ---------------------------------------------------------------------------------------------

    /**
     * Set lastSeenAt to now
     */
    // Note : seems to be unused
    public void touchLastSeenAt() {
        this.setLastSeenAt(OffsetDateTime.now());
    }

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

    @Override
    public void mergeToUpdate(ColabEntity other) throws ColabMergeException {
        if (other instanceof User) {
            User o = (User) other;
            this.setDeletionStatus(o.getDeletionStatus());
            this.setFirstname(o.getFirstname());
            this.setLastname(o.getLastname());
            this.setCommonname(o.getCommonname());
            this.setAffiliation(o.getAffiliation());
            // agreedTime cannot be changed by a simple update
        } else {
            throw new ColabMergeException(this, other);
        }
    }

    @Override
    public ChannelsBuilder getChannelsBuilder() {
        return new AboutUserChannelsBuilder(this);
    }

    @Override
    @JsonbTransient
    public Conditions.Condition getReadCondition() {
        return new Conditions.Or(
            // unauthenticated users shall read user data to authenticate
            new Conditions.Not(Conditions.authenticated),
            new Conditions.IsCurrentUserThisUser(this),
            new Conditions.DoCurrentUserWorkOnSameProjectThanUser(this)
        );
    }

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

    @Override
    @JsonbTransient
    public Conditions.Condition getCreateCondition() {
        // anyone can create a user
        return Conditions.alwaysTrue;
    }

    @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 "User{" + "id=" + id + ", deletion=" + getDeletionStatus()
            + ", isAdmin=" + isAdmin + ", username=" + username + '}';
    }

}