Project.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.project;
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.common.DeletionStatus;
import ch.colabproject.colab.api.model.common.Illustration;
import ch.colabproject.colab.api.model.common.Tracking;
import ch.colabproject.colab.api.model.team.TeamMember;
import ch.colabproject.colab.api.model.team.TeamRole;
import ch.colabproject.colab.api.model.team.acl.HierarchicalPosition;
import ch.colabproject.colab.api.model.tools.EntityHelper;
import ch.colabproject.colab.api.security.permissions.Conditions;
import ch.colabproject.colab.api.security.permissions.project.ProjectConditions;
import ch.colabproject.colab.api.ws.channel.tool.ChannelsBuilders.AboutProjectOverviewChannelsBuilder;
import ch.colabproject.colab.api.ws.channel.tool.ChannelsBuilders.ChannelsBuilder;
import javax.json.bind.annotation.JsonbTransient;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* A project as persisted in database
*
* @author maxence
* @author sandra
*/
@Entity
@Table(
indexes = {
@Index(columnList = "rootcard_id"),
}
)
@NamedQuery(name = "Project.findAll",
query = "SELECT p FROM Project p")
@NamedQuery(name = "Project.findAllGlobal",
query = "SELECT p from Project p WHERE p.globalProject = true AND p.type = :model")
@NamedQuery(name = "Project.findByTeamMemberUser",
query = "SELECT p FROM Project p JOIN p.teamMembers members WHERE members.user.id = :userId")
@NamedQuery(name = "Project.findIdsByTeamMemberUser",
query = "SELECT p.id FROM Project p JOIN p.teamMembers m WHERE m.user.id = :userId")
@NamedQuery(name = "Project.findByInstanceMakerUser",
query = "SELECT p FROM Project p JOIN InstanceMaker im WHERE im.project.id = p.id AND im.user.id = :userId")
@NamedQuery(name = "Project.findIdsByInstanceMakerUser",
query = "SELECT p.id FROM Project p JOIN InstanceMaker im WHERE im.project.id = p.id AND im.user.id = :userId")
@NamedQuery(name = "Project.doUsersHaveACommonProject",
query = "SELECT TRUE FROM Project p WHERE ("
+ " ( p.id IN ( SELECT tm.project.id FROM TeamMember tm WHERE tm.user.id = :aUserId ) "
+ " OR p.id IN ( SELECT im.project.id FROM InstanceMaker im WHERE im.user.id = :aUserId ) ) "
+ "AND "
+ " ( p.id IN ( SELECT tm.project.id FROM TeamMember tm WHERE tm.user.id = :bUserId ) "
+ " OR p.id IN ( SELECT im.project.id FROM InstanceMaker im WHERE im.user.id = :bUserId ) ) "
+ ")")
public class Project implements ColabEntity, WithWebsocketChannels {
private static final long serialVersionUID = 1L;
/**
* Project sequence name
*/
public static final String PROJECT_SEQUENCE_NAME = "project_seq";
// ---------------------------------------------------------------------------------------------
// fields
// ---------------------------------------------------------------------------------------------
/**
* Project ID
*/
@Id
@SequenceGenerator(name = PROJECT_SEQUENCE_NAME, allocationSize = 1)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = PROJECT_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;
/**
* The kind : project or model
*/
@NotNull
@Enumerated(EnumType.STRING)
private ProjectType type = ProjectType.PROJECT;
/**
* The name
*/
@Size(max = 255)
private String name;
/**
* The description
*/
@Size(max = 255)
private String description;
/**
* Global means accessible to everyone
*/
private boolean globalProject;
/**
* global state on load
*/
@Transient
private boolean initialGlobal;
/**
* The icon to illustrate the project
*/
@Embedded
private Illustration illustration;
/**
* The root card of the project containing all other cards
*/
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JsonbTransient
private Card rootCard;
// No need to have a rootCardId
// We do not want to serialize the root card it of the project when sending to client
// It could cause access control problems. A user can read some project, but not its cards
/**
* Roles
*/
@OneToMany(mappedBy = "project", cascade = CascadeType.ALL)
@JsonbTransient
private List<TeamRole> roles = new ArrayList<>();
/**
* List of team members
*/
@OneToMany(mappedBy = "project", cascade = CascadeType.ALL)
@JsonbTransient
private List<TeamMember> teamMembers = new ArrayList<>();
/**
* List of instanceMakers
*/
@OneToMany(mappedBy = "project", cascade = CascadeType.ALL)
@JsonbTransient
private List<InstanceMaker> instanceMakers = new ArrayList<>();
/**
* List of elements to be defined within the cards
*/
@OneToMany(mappedBy = "project", cascade = CascadeType.ALL)
@JsonbTransient
private List<AbstractCardType> elementsToBeDefined = new ArrayList<>();
// ---------------------------------------------------------------------------------------------
// getters and setters
// ---------------------------------------------------------------------------------------------
/**
* @return the project ID
*/
@Override
public Long getId() {
return id;
}
/**
* @param id the project 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 kind : project or model
*/
public ProjectType getType() {
return type;
}
/**
* @param type the kind : project or model
*/
public void setType(ProjectType type) {
this.type = type;
}
/**
* @return the project name
*/
public String getName() {
return name;
}
/**
* Set the project name
*
* @param name the project name
*/
public void setName(String name) {
this.name = name;
}
/**
* @return the description
*/
public String getDescription() {
return description;
}
/**
* @param description the description to set
*/
public void setDescription(String description) {
this.description = description;
}
/**
* @return if project is global
*/
public boolean isGlobalProject() {
return globalProject;
}
/**
* @param global if project is global
*/
public void setGlobalProject(boolean global) {
this.globalProject = global;
}
/**
* @return the icon to illustrate the project
*/
public Illustration getIllustration() {
return illustration;
}
/**
* @param illustration the icon to illustrate the project
*/
public void setIllustration(Illustration illustration) {
this.illustration = illustration;
}
/**
* @return the root card of the project that contains every other cards
*/
public Card getRootCard() {
return rootCard;
}
/**
* @param rootCard the root card of the project that contains every other cards
*/
public void setRootCard(Card rootCard) {
this.rootCard = rootCard;
}
/**
* Get the value of roles
*
* @return the value of roles
*/
public List<TeamRole> getRoles() {
return roles;
}
/**
* Get a role by its name
*
* @param name name of the role
* @return the role or null
*/
public TeamRole getRoleByName(String name) {
if (name != null) {
for (TeamRole r : this.roles) {
if (name.equals(r.getName())) {
return r;
}
}
}
return null;
}
/**
* Set the value of roles
*
* @param roles new value of roles
*/
public void setRoles(List<TeamRole> roles) {
this.roles = roles;
}
/**
* get team members
*
* @return members
*/
public List<TeamMember> getTeamMembers() {
return teamMembers;
}
/**
* Get all members with given position
*
* @param position the needle
* @return list of team member with the given position
*/
public List<TeamMember> getTeamMembersByPosition(HierarchicalPosition position) {
return this.teamMembers.stream()
.filter(member -> member.getPosition() == position)
.collect(Collectors.toList());
}
/**
* Set team members
*
* @param teamMembers list of members
*/
public void setTeamMembers(List<TeamMember> teamMembers) {
this.teamMembers = teamMembers;
}
/**
* @return List of instanceMakers
*/
public List<InstanceMaker> getInstanceMakers() {
return instanceMakers;
}
/**
* @param instanceMakers list of instanceMakers
*/
public void setInstanceMakers(List<InstanceMaker> instanceMakers) {
this.instanceMakers = instanceMakers;
}
/**
* @return the elementsToDefine
*/
public List<AbstractCardType> getElementsToBeDefined() {
return elementsToBeDefined;
}
/**
* @param elements the elementsToDefine to set
*/
public void setElementsToBeDefined(List<AbstractCardType> elements) {
this.elementsToBeDefined = elements;
}
// ---------------------------------------------------------------------------------------------
// helpers
// ---------------------------------------------------------------------------------------------
/**
* @return is now global or was just not global
*/
@JsonbTransient
public boolean isOrWasGlobal() {
return this.isGlobalProject() || this.initialGlobal;
}
/**
*
* @return is project a model
*/
@JsonbTransient
public boolean isModel() { return this.type == ProjectType.MODEL; }
// ---------------------------------------------------------------------------------------------
// concerning the whole class
// ---------------------------------------------------------------------------------------------
/**
* JPA post-load callback. Used to keep trace of the initial value of the
* <code>globalProject</code> field.
*/
@PostLoad
public void postLoad() {
// keep trace of modification
this.initialGlobal = this.globalProject;
}
@Override
public void mergeToUpdate(ColabEntity other) throws ColabMergeException {
if (other instanceof Project) {
Project o = (Project) other;
this.setDeletionStatus(o.getDeletionStatus());
this.setType(o.getType());
this.setName(o.getName());
this.setDescription(o.getDescription());
this.setGlobalProject(o.isGlobalProject());
this.setIllustration(o.getIllustration());
} else {
throw new ColabMergeException(this, other);
}
}
/**
* Project if propagated through its own overview channel.
*
* @return the channel
*/
@Override
public ChannelsBuilder getChannelsBuilder() {
return new AboutProjectOverviewChannelsBuilder(this);
}
@Override
@JsonbTransient
public Conditions.Condition getReadCondition() {
return new ProjectConditions.IsProjectReadable(this.id);
}
@Override
@JsonbTransient
public Conditions.Condition getUpdateCondition() {
return new Conditions.IsCurrentUserInternalToProject(this);
}
@Override
@JsonbTransient
public Conditions.Condition getCreateCondition() {
// anybody can create a project
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 "Project{" + "id=" + id + ", deletion=" + getDeletionStatus()
+ ", type=" + type.name() + ", name=" + name + ", descr=" + description
+ ", global=" + globalProject + '}';
}
}