/**
* Yobi, Project Hosting SW
*
* Copyright 2012 NAVER Corp.
* http://yobi.io
*
* @Author Ahn Hyeok Jun
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package models;
import java.text.SimpleDateFormat;
import java.util.*;
import javax.persistence.*;
import controllers.UserApp;
import models.enumeration.*;
import models.resource.GlobalResource;
import models.resource.Resource;
import models.resource.ResourceConvertible;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.crypto.hash.Sha256Hash;
import org.apache.shiro.util.ByteSource;
import play.data.format.Formats;
import play.data.validation.Constraints;
import play.data.validation.Constraints.*;
import play.db.ebean.Model;
import play.db.ebean.Transactional;
import utils.JodaDateUtil;
import utils.ReservedWordsValidator;
import com.avaje.ebean.Ebean;
import com.avaje.ebean.ExpressionList;
import com.avaje.ebean.Page;
import com.avaje.ebean.RawSql;
import com.avaje.ebean.RawSqlBuilder;
@Table(name = "n4user")
@Entity
public class User extends Model implements ResourceConvertible {
private static final long serialVersionUID = 1L;
public static final Model.Finder<Long, User> find = new Finder<>(Long.class, User.class);
public static final Comparator<User> USER_NAME_COMPARATOR = new Comparator<User>() {
@Override
public int compare(User u1, User u2) {
return u1.name.compareTo(u2.name);
}
};
/**
* Max number of user size to show per page at site admin user list page
*/
public static final int USER_COUNT_PER_PAGE = 30;
public static final Long SITE_MANAGER_ID = 1l;
public static final String LOGIN_ID_PATTERN = "[a-zA-Z0-9-]+([_.][a-zA-Z0-9-]+)*";
public static final String LOGIN_ID_PATTERN_ALLOW_FORWARD_SLASH = "[a-zA-Z0-9-/]+([_.][a-zA-Z0-9-/]+)*";
public static final User anonymous = new NullUser();
@Id
public Long id;
/**
* name to show at web pages
*/
public String name;
@Pattern(value = "^" + LOGIN_ID_PATTERN + "$", message = "user.wrongloginId.alert")
@Required
@ValidateWith(ReservedWordsValidator.class)
public String loginId;
/**
* only used for password reset
*/
@Transient
public String oldPassword;
public String password;
public String passwordSalt;
@Constraints.Email(message = "user.wrongEmail.alert")
public String email;
/**
* used for enabling remember me feature
*
* remember me = keep a use logged in
*/
public boolean rememberMe;
@Enumerated(EnumType.STRING)
public UserState state;
@Formats.DateTime(pattern = "yyyy-MM-dd")
public Date lastStateModifiedDate;
/**
* account creation date
*/
@Formats.DateTime(pattern = "yyyy-MM-dd")
public Date createdDate;
/**
* user auth role of project
*
* It can be a project admin or member
*/
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
public List<ProjectUser> projectUser;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
public List<OrganizationUser> groupUser;
/**
* project which is requested member join
*/
@ManyToMany(cascade = CascadeType.ALL)
@JoinTable(name = "user_enrolled_project", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "project_id"))
public List<Project> enrolledProjects;
@ManyToMany(cascade = CascadeType.ALL)
@JoinTable(name = "user_enrolled_organization", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "organization_id"))
public List<Organization> enrolledOrganizations;
@ManyToMany(mappedBy = "receivers")
@OrderBy("created DESC")
public List<NotificationEvent> notificationEvents;
/**
* alternate user email addresses
*
* It is used to recognize as same user when a user uses multiple emails.
*
* cf. {@link #email} is regarded as a user's representative email
*
* {@link #email} can be set as one of {@link #emails}.
* In that case, selected {@link #email} is removed from {@link #emails}
*/
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
public List<Email> emails;
@OneToOne(mappedBy = "user", cascade = CascadeType.ALL)
public RecentlyVisitedProjects recentlyVisitedProjects;
@OneToMany(mappedBy = "user")
public List<Mention> mentions;
/**
* The user's preferred language code which can be recognized by {@link play.api.i18n.Lang#get},
* such as "ko", "en-US" or "ja". This field is used as a language for notification mail.
*/
public String lang;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
public List<OrganizationUser> organizationUsers;
public User() {
}
public User(Long id) {
this.id = id;
}
/**
* User creation date which forms of "MMM dd, yyyy"
*
* It is made for view pages
*
* @return
*/
public String getDateString() {
SimpleDateFormat sdf = new SimpleDateFormat("MMM dd, yyyy", Locale.US);
return sdf.format(this.createdDate);
}
public List<Project> myProjects(String orderString) {
return Project.findProjectsByMemberWithFilter(id, orderString);
}
/**
* Create a user and set creation date
*
* @param user
* @return user's id (not login id)
*/
public static Long create(User user) {
user.createdDate = JodaDateUtil.now();
user.save();
return user.id;
}
/**
* find a user by login id string
*
* If there is no user correspond to login id string,
* then return {@link #anonymous} not null
*
* @param loginId
* @return User or {@link #anonymous}
*/
public static User findByLoginId(String loginId) {
User user = find.where().ieq("loginId", loginId).findUnique();
if (user == null) {
return anonymous;
}
else {
return user;
}
}
/**
*
* Find a user by email account.
* - find a user with a given email account or who has the email account as one of sub email accounts.
* - If no user matched up with the given email account, then return new {@link models.NullUser}
* after setting the email account to the object.
*
* @param email
* @return
*/
public static User findByEmail(String email) {
User user = find.where().eq("email", email).findUnique();
if (user != null) {
return user;
}
Email subEmail = Email.findByEmail(email, true);
if (subEmail != null) {
return subEmail.user;
}
User anonymous = new NullUser();
anonymous.email = email;
return anonymous;
}
public static User findByLoginKey(String loginIdOrEmail) {
User user = find.where().ieq("loginId", loginIdOrEmail).findUnique();
if (user == null) {
user = find.where().eq("email", loginIdOrEmail).findUnique();
}
return (user == null) ? anonymous : user;
}
/**
* check is existed user
*
* @param loginId
* @return boolean
*/
public static boolean isLoginIdExist(String loginId) {
int findRowCount = find.where().ieq("loginId", loginId).findRowCount();
return (findRowCount != 0);
}
/**
* all user list mapped by id and name
*
*/
public static Map<String, String> options() {
LinkedHashMap<String, String> options = new LinkedHashMap<>();
for (User user : User.find.orderBy("name").findList()) {
options.put(user.id.toString(), user.name);
}
return options;
}
/**
*
* All user list except anonymous and site admin
*
* @param pageNum
* @param query If {@code query}is not null, search list contains {@code query}
* @return user list forms of Page which is ordered by login id
*/
public static Page<User> findUsers(int pageNum, String query, UserState state) {
ExpressionList<User> el = User.find.where();
el.ne("id",SITE_MANAGER_ID);
el.ne("loginId",anonymous.loginId);
el.eq("state", state);
if(StringUtils.isNotBlank(query)) {
el = el.disjunction();
el = el.icontains("loginId", query).icontains("name", query);
el.endJunction();
}
return el.findPagingList(USER_COUNT_PER_PAGE).getPage(pageNum);
}
/**
* project user list except site admin
*
* @param projectId
* @return project admin and member list
*/
public static List<User> findUsersByProject(Long projectId) {
return find.where().eq("projectUser.project.id", projectId)
.ne("projectUser.role.id", RoleType.SITEMANAGER.roleType()).orderBy().asc("name")
.findList();
}
@Transient
public Long avatarId() {
List<Attachment> attachments = Attachment.findByContainer(avatarAsResource());
if (attachments.size() > 0) {
return attachments.get(attachments.size() - 1).id;
}
else {
return null;
}
}
/**
* Check if email exists or not
*
* Also check the email exist in {@link Email} as a certificated side email
*
* @param emailAddress
* @return boolean
*/
public static boolean isEmailExist(String emailAddress) {
User user = find.where().ieq("email", emailAddress).findUnique();
return user != null || Email.exists(emailAddress, true);
}
/**
* check whether this is a anonymous
*/
public boolean isAnonymous() {
return id == null || id.equals(anonymous.id);
}
/**
* reset user password with a new password
*
* When new password save, it will be encryped with {@link User.passwordSalt}
*
* @param loginId
* @param newPassword
*/
public static void resetPassword(String loginId, String newPassword) {
User user = findByLoginId(loginId);
user.password = new Sha256Hash(newPassword, ByteSource.Util.bytes(user.passwordSalt), 1024)
.toBase64();
user.save();
}
@Override
public Resource asResource() {
return new GlobalResource() {
@Override
public String getId() {
return id.toString();
}
@Override
public ResourceType getType() {
return ResourceType.USER;
}
};
}
public Resource avatarAsResource() {
return new GlobalResource() {
@Override
public String getId() {
return id.toString();
}
@Override
public ResourceType getType() {
return ResourceType.USER_AVATAR;
}
};
}
public boolean isSiteManager() {
return SiteAdmin.exists(this);
}
public List<Project> getEnrolledProjects() {
if (this.enrolledProjects == null) {
this.enrolledProjects = new ArrayList<>();
}
return this.enrolledProjects;
}
public List<Organization> getEnrolledOrganizations() {
if (this.enrolledOrganizations == null) {
this.enrolledOrganizations = new ArrayList<>();
}
return this.enrolledOrganizations;
}
@Transactional
public void addWatching(Project project) {
Watch.watch(this, project.asResource());
}
@Transactional
public void removeWatching(Project project) {
Watch.unwatch(this, project.asResource());
}
public static boolean isWatching(Project project) {
return Watch.isWatching(project.asResource());
}
public List<Project> getWatchingProjects() {
return getWatchingProjects(null);
}
public List<Project> getWatchingProjects(String orderString) {
List<String> projectIds = Watch.findWatchedResourceIds(this, ResourceType.PROJECT);
List<Project> projects = new ArrayList<>();
for (String id : projectIds) {
projects.add(Project.find.byId(Long.valueOf(id)));
}
if (StringUtils.isBlank(orderString)) {
return projects;
}
return Ebean.filter(Project.class).sort(orderString).filter(projects);
}
/**
* request join as a member at {@code project}
*
* @param project
*/
public void enroll(Project project) {
getEnrolledProjects().add(project);
this.update();
}
public void enroll(Organization organization) {
getEnrolledOrganizations().add(organization);
this.update();
}
/**
* cancel enrol request at {@code project}
*
* @param project
*/
public void cancelEnroll(Project project) {
getEnrolledProjects().remove(project);
this.update();
}
public void cancelEnroll(Organization organization) {
getEnrolledOrganizations().remove(organization);
this.update();
}
/**
* check current logged in user already sent a enrol request to {@code project}
*
* @param project
* @return
*/
public static boolean enrolled(Project project) {
User user = UserApp.currentUser();
if (user.isAnonymous()) {
return false;
}
return user.getEnrolledProjects().contains(project);
}
public static boolean enrolled(Organization organization) {
User user = UserApp.currentUser();
if (user.isAnonymous()) {
return false;
}
return user.getEnrolledOrganizations().contains(organization);
}
@Override
public void delete() {
for (Assignee assignee : Assignee.finder.where().eq("user.id", id).findList()) {
assignee.delete();
}
super.delete();
}
public void changeState(UserState state) {
this.state = state;
lastStateModifiedDate = new Date();
if (this.state == UserState.DELETED) {
name = "DELETED";
oldPassword = "";
password = "";
passwordSalt = "";
email = "deleted-" + loginId + "@noreply.yobi.io";
rememberMe = false;
projectUser.clear();
enrolledProjects.clear();
notificationEvents.clear();
for (Assignee assignee : Assignee.finder.where().eq("user.id", id).findList()) {
for (Issue issue : assignee.issues) {
issue.assignee = null;
issue.update();
}
assignee.delete();
}
}
update();
}
public String avatarUrl() {
Long avatarId = avatarId();
if (avatarId == null) {
return UserApp.DEFAULT_AVATAR_URL;
}
else {
return controllers.routes.AttachmentApp.getFile(avatarId).url();
}
}
/**
* All user post a issue at a project whose id is {@code projectId}
*
* @param projectId
* @return
*/
public static List<User> findIssueAuthorsByProjectId(long projectId) {
String sql = "select user.id, user.name, user.login_id from issue issue, n4user user where issue.author_id = user.id group by issue.author_id";
return createUserSearchQueryWithRawSql(sql).where()
.eq("issue.project_id", projectId)
.orderBy("user.name ASC")
.findList();
}
/**
* All user list sent pull-requests at project whose id is {@code projectId}
*
* @param projectId
* @return
*/
public static List<User> findPullRequestContributorsByProjectId(long projectId) {
String sql = "SELECT user.id, user.name, user.login_id FROM pull_request pullrequest, n4user user WHERE pullrequest.contributor_id = user.id GROUP BY pullrequest.contributor_id";
return createUserSearchQueryWithRawSql(sql).where()
.eq("pullrequest.to_project_id", projectId)
.orderBy("user.name ASC")
.findList();
}
private static com.avaje.ebean.Query<User> createUserSearchQueryWithRawSql(String sql) {
RawSql rawSql = RawSqlBuilder.parse(sql).columnMapping("user.login_id", "loginId").create();
return User.find.setRawSql(rawSql);
}
/**
* find users at a project whose id is {@code projectId} and role is {@code roleType}
*
* @param projectId
* @param roleType
* @return
*/
public static List<User> findUsersByProject(Long projectId, RoleType roleType) {
return find.where().eq("projectUser.project.id", projectId)
.eq("projectUser.role.id", roleType.roleType()).orderBy().asc("name").findList();
}
public static List<User> findUsersByOrganization(Long organizationId, RoleType roleType) {
return find.where().eq("organizationUsers.organization.id", organizationId)
.eq("organizationUsers.role.id", roleType.roleType()).orderBy().asc("name").findList();
}
/**
* add to user's alternate email list
*
* @param email
*/
public void addEmail(Email email) {
email.save();
emails.add(email);
}
/**
* check {@code newEmail} exists in user's alternate email list
*
* @param newEmail
* @return boolean
*/
public boolean has(String newEmail) {
for (Email email : emails) {
if (email.email.equals(newEmail)) {
return true;
}
}
return false;
}
/**
* remove {@code email} from user's altenate emails
* @param email
*/
public void removeEmail(Email email) {
emails.remove(email);
email.delete();
}
public void visits(Project project) {
this.recentlyVisitedProjects = RecentlyVisitedProjects.addNewVisitation(this, project);
this.update();
}
public List<ProjectVisitation> getVisitedProjects(int size) {
if(size < 1 || this.recentlyVisitedProjects == null) {
return new ArrayList<>();
}
return this.recentlyVisitedProjects.findRecentlyVisitedProjects(size);
}
public List<Organization> getOrganizations(int size) {
if(size < 1) {
throw new IllegalArgumentException("the size should be bigger then 0");
}
List<Organization> orgs = new ArrayList<>();
for(OrganizationUser ou : OrganizationUser.findByUser(this, size)) {
orgs.add(ou.organization);
}
return orgs;
}
public void createOrganization(Organization organization) {
OrganizationUser ou = new OrganizationUser();
ou.user = this;
ou.organization = organization;
ou.role = Role.findByRoleType(RoleType.ORG_ADMIN);
ou.save();
this.add(ou);
organization.add(ou);
this.update();
}
private void add(OrganizationUser ou) {
this.organizationUsers.add(ou);
}
}