/* * Copyright 2012 Nodeable Inc * * 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 com.streamreduce.core.model; import com.google.code.morphia.annotations.Embedded; import com.google.code.morphia.annotations.Entity; import com.google.code.morphia.annotations.Indexed; import com.google.code.morphia.annotations.PrePersist; import com.google.code.morphia.annotations.Reference; import com.google.code.morphia.annotations.Transient; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.streamreduce.util.JSONUtils; import com.streamreduce.util.SecurityUtil; import net.sf.json.JSONObject; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang.StringUtils; import org.codehaus.jackson.annotate.JsonIgnore; import org.hibernate.validator.constraints.Email; import org.hibernate.validator.constraints.NotEmpty; import org.hibernate.validator.constraints.ScriptAssert; import javax.annotation.Nullable; import javax.validation.Valid; import javax.validation.constraints.NotNull; import java.util.Arrays; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @ScriptAssert.List({ @ScriptAssert(script = "_this.userStatus.toString() == 'PENDING' || (_this.alias != undefined && _this.alias.trim().length() > 0)", lang = "javascript", message = "alias may not be empty"), // Alias is required for non-pending users @ScriptAssert(script = "_this.userStatus.toString() == 'PENDING' || (_this.fullname != undefined && _this.fullname.trim().length() > 0)", lang = "javascript", message = "fullname may not be empty"), // Fullname is required for non-pending users @ScriptAssert(script = "_this.userStatus.toString() == 'PENDING' || _this.account != undefined", lang = "javascript", message = "account may not be empty") // Account is required for non-pending users }) @Entity(value = "users", noClassnameStored = true) public class User extends SobaObject { private static final long serialVersionUID = -8309096404012045338L; private static final ImmutableSet<String> REQUIRED_CONFIG_KEYS = new ImmutableSet.Builder<String>().add( ConfigKeys.GRAVATAR_HASH, ConfigKeys.RECEIVES_COMMENT_NOTIFICATIONS, ConfigKeys.RECEIVES_NEW_MESSAGE_NOTIFICATIONS) .build(); @Indexed(unique = true) @Email @NotEmpty protected String username; @NotEmpty private String password; // @NotNull <<< Handled by ScriptAssert above private String fullname; private String secretKey; // to activate account, password change key, whatever is needed @NotEmpty private String fuid; // for internal use only private boolean accountOriginator; @Reference @NotNull private Set<Role> roles = new HashSet<>(); private boolean userLocked = true; @NotNull @SuppressWarnings("unchecked") private Map<String,Object> userConfig = Maps.newHashMap(new ImmutableMap.Builder() .put(ConfigKeys.RECEIVES_COMMENT_NOTIFICATIONS, true) .put(ConfigKeys.RECEIVES_NEW_MESSAGE_NOTIFICATIONS, true).build()); @Transient @JsonIgnore protected User user; // to avoid a jackson self reference we override and ignore here: SOBA-680 // for UI rendering purposes (it just means they completed their setup process) private UserStatus userStatus = UserStatus.PENDING; // the "api key" for API requests @Valid @Embedded private APIAuthenticationToken authenticationToken; private Date lastActivity; public enum UserStatus { PENDING, ACTIVATED, DISABLED } /** * Constant values used in the userConfig map. */ public static class ConfigKeys { public static final String GRAVATAR_HASH = "gravatarHash"; public static final String ICON = "icon"; public static final String LAST_READ_TIMESTAMP = "lastReadTS"; public static final String RECEIVES_COMMENT_NOTIFICATIONS = "commentNotifications"; public static final String RECEIVES_NEW_MESSAGE_NOTIFICATIONS = "newMessageNotifications"; } private User() { } @PrePersist public void createFUIDIfMissing() { if (fuid == null) { fuid = SecurityUtil.createNodeableFUID(getUsername(), getCreated()); } } @PrePersist public void createDefaultConfig() { if (userConfig == null) { userConfig = Maps.newHashMap(new ImmutableMap.Builder() .put(ConfigKeys.RECEIVES_COMMENT_NOTIFICATIONS, true) .put(ConfigKeys.RECEIVES_NEW_MESSAGE_NOTIFICATIONS, true).build()); } else { if (!userConfig.containsKey(User.ConfigKeys.RECEIVES_NEW_MESSAGE_NOTIFICATIONS)) { userConfig.put(User.ConfigKeys.RECEIVES_NEW_MESSAGE_NOTIFICATIONS, true); } if (!userConfig.containsKey(User.ConfigKeys.RECEIVES_COMMENT_NOTIFICATIONS)) { userConfig.put(User.ConfigKeys.RECEIVES_COMMENT_NOTIFICATIONS, true); } } } @PrePersist public void createGravatarhash() { // Create a gravatarHash user configuration entry userConfig.put(ConfigKeys.GRAVATAR_HASH, DigestUtils.md5Hex(getUsername().trim().toLowerCase())); } private String encryptPassword(String password) { return SecurityUtil.passwordEncrypt(password); } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String clearTextPassword) { this.password = encryptPassword(clearTextPassword); } public void setPasswordHash(String passwordHash) { this.password = passwordHash; } public String getFullname() { return fullname; } public void setFullname(String fullname) { this.fullname = fullname; } public Set<Role> getRoles() { return roles; } public void setRoles(Set<Role> roles) { this.roles = roles; } public void addRole(Role role) { if (!roles.contains(role)) { roles.add(role); } } public void addRoles(Role... role) { Set<Role> set = new HashSet<>(); set.addAll(Arrays.asList(role)); this.roles = set; } public boolean isUserLocked() { return userLocked; } public void setUserLocked(boolean userLocked) { this.userLocked = userLocked; } public String getSecretKey() { return secretKey; } public void setSecretKey(@Nullable String secretKey) { this.secretKey = secretKey; } /** * Gets a copy of the configuration for this user. Modifying the copy returned does not affect the user's config. * In order to modify the configuration, use one of the overloaded setConfigValue methods or * {@link User#appendToConfig(java.util.Map)}. * @return Map representing a copy of this user's config. */ public Map<String,Object> getConfig() { return Maps.newHashMap(userConfig); } /** * Sets a number value identified by the passed in key in the user's config. * * @param key * @param value */ public void setConfigValue(String key, Number value) { this.userConfig.put(key, value); } /** * Sets a String value identified by the passed in key in the user's config. * * @param key String key for config value * @param value */ public void setConfigValue(String key, String value) { this.userConfig.put(key, value); } /** * Appends all entries in the passed in Map to this config, and overwrites values if a key in the passed in * configMap exists already in the current config. * <p/> * Because JSONObject is untyped, if there is a non-string key present, that key is ignored. * * @param configMap - the new keys/values to be added/modified in the current config. */ public void appendToConfig(Map<String,Object> configMap) { if (configMap == null) { return ;} userConfig.putAll(configMap); } /** * Sets a String value identified by the passed in key in the user's config. * * @param key String key for config value * @param value */ public void setConfigValue(String key, Boolean value) { this.userConfig.put(key, value); } /** * Sets a Map value identified by the passed in key in the user's config. * * @param key String key for config value * @param value */ public void setConfigValue(String key, Map<String,Object> value) { this.userConfig.put(key,JSONUtils.replaceJSONNullsFromMap(value)); } /** * Sets a List value identified by the passed in key in the user's config. * * @param key String key for config value * @param value */ public void setConfigValue(String key, List<Object> value) { this.userConfig.put(key,JSONUtils.replaceJSONNullsFromList(value)); } /** * Removes a config value from the User's config. * * @param key */ public void removeConfigValue(String key) { if (REQUIRED_CONFIG_KEYS.contains(key)) { throw new IllegalArgumentException("Unable to removed required configuration key. Required keys are: " + REQUIRED_CONFIG_KEYS); } userConfig.remove(key); } public UserStatus getUserStatus() { return userStatus; } public void setUserStatus(UserStatus userStatus) { this.userStatus = userStatus; } public String getFuid() { return fuid; } public void setFuid(String fuid) { this.fuid = fuid; } public boolean isAccountOriginator() { return accountOriginator; } public void setAccountOriginator(boolean accountOriginator) { this.accountOriginator = accountOriginator; } public APIAuthenticationToken getAuthenticationToken() { return authenticationToken; } public void setAuthenticationToken(APIAuthenticationToken authenticationToken) { this.authenticationToken = authenticationToken; } public Date getLastActivity() { return lastActivity; } public void setLastActivity(Date lastActivity) { this.lastActivity = lastActivity; } @Override @JsonIgnore public User getUser() { return this; } @Override @JsonIgnore public void setUser(User user) { } @Override public void mergeWithJSON(JSONObject json) { super.mergeWithJSON(json); if (json != null) { if (json.containsKey("fullname")) { setFullname(json.getString("fullname")); } } } /* Extending this class is disabled on purpose. If you need to extend the builder, please look at InventoryItem.Builder to see how to do it properly. */ public static final class Builder extends SobaObject.Builder<User, Builder> { public Builder() { super(new User()); } public Builder(User u) { super(new User()); if (u != null) { //Create a shallow copy using direct field access and make defensive copies of mutable references theObject.account = u.account; theObject.alias = u.alias; theObject.created = u.created; theObject.description = u.description; theObject.fuid = u.fuid; theObject.fullname = u.fullname; theObject.hashtags = Sets.newHashSet(u.hashtags); theObject.id = u.id; theObject.modified = u.modified; theObject.password = u.password; theObject.roles = Sets.newHashSet(u.roles); theObject.secretKey = u.secretKey; theObject.silentUpdate = u.silentUpdate; theObject.updatedViaREST = u.updatedViaREST; theObject.user = u.user != null ? new Builder(u).build() : null; theObject.userConfig = u.getConfig(); //create a copy of the config theObject.userLocked = u.userLocked; theObject.username = u.username; theObject.userStatus = u.userStatus; theObject.version = u.version; theObject.visibility = u.visibility; } } public Builder username(String username) { if (isBuilt) { throw new IllegalStateException("The object cannot be modified after built"); } theObject.username = username; return this; } public Builder password(String password) { if (isBuilt) { throw new IllegalStateException("The object cannot be modified after built"); } theObject.setPassword(password); return this; } public Builder secretKey(String secretKey) { if (isBuilt) { throw new IllegalStateException("The object cannot be modified after built"); } theObject.setSecretKey(secretKey); return this; } public Builder fullname(String fullname) { if (isBuilt) { throw new IllegalStateException("The object cannot be modified after built"); } theObject.fullname = fullname; return this; } public Builder userStatus(UserStatus userStatus) { if (isBuilt) { throw new IllegalStateException("The object cannot be modified after built"); } theObject.userStatus = userStatus; return this; } public Builder roles(Set<Role> roles) { if (isBuilt) { throw new IllegalStateException("The object cannot be modified after built"); } theObject.roles = roles; return this; } public Builder roles(Role... role) { if (isBuilt) { throw new IllegalStateException("The object cannot be modified after built"); } Set<Role> set = new HashSet<>(); set.addAll(Arrays.asList(role)); theObject.roles = set; return this; } public Builder accountLocked(boolean isLocked) { if (isBuilt) { throw new IllegalStateException("The object cannot be modified after built"); } theObject.userLocked = isLocked; return this; } public Builder userConfig(Map<String,Object> config) { if (isBuilt) { throw new IllegalStateException("The object cannot be modified after built"); } theObject.userConfig = config; return this; } public Builder accountOriginator(boolean accountOriginator) { if (isBuilt) { throw new IllegalStateException("The object cannot be modified after built"); } theObject.accountOriginator = accountOriginator; return this; } public Builder fuid(String fuid) { if (isBuilt) { throw new IllegalStateException("The object cannot be modified after built"); } theObject.setFuid(fuid); return this; } /** * Allow account to be null here... * * @return */ @Override public User build() { if (isBuilt) { throw new IllegalStateException("The object cannot be modified after built"); } if (StringUtils.isBlank(theObject.username)) { throw new IllegalStateException("username must not be blank"); } // your default alias is your username... if (theObject.getAlias() == null) { if (User.isValidUserAlias(theObject.getUsername())) { theObject.setAlias(theObject.getUsername()); } else { theObject.setAlias(User.formatStringForUserAlias(theObject.getUsername())); } } if (theObject.getPassword() == null) { // random password just so it's not null theObject.setPassword(SecurityUtil.passwordEncrypt(SecurityUtil.generateRandomString())); } if (theObject.getSecretKey() == null) { // random secret key just so it's not null theObject.setSecretKey(SecurityUtil.generateRandomString()); } // create the API token if (theObject.authenticationToken == null) { theObject.setAuthenticationToken(new APIAuthenticationToken()); } return theObject; } /** * {@inheritDoc} */ @Override public Builder getRealBuilder() { return this; } } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; User user = (User) o; return username.equals(user.username); } @Override public int hashCode() { return username.hashCode(); } static public boolean isValidUserAlias(String alias) { String regex = "^[-\\w]+$"; return alias.matches(regex); } static public String formatStringForUserAlias(String alias) { String aliasWithRemovedAtSymbol = alias.replaceAll("@", "_at_"); String aliasWithNonAlphaNumericsOrDashesReplacedWithUnderscores = aliasWithRemovedAtSymbol.replaceAll("[^-\\w]", "_"); return aliasWithNonAlphaNumericsOrDashesReplacedWithUnderscores; } }