com.erudika.para.core.User.java Source code

Java tutorial

Introduction

Here is the source code for com.erudika.para.core.User.java

Source

/*
 * Copyright 2013-2015 Erudika. http://erudika.com
 *
 * 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.
 *
 * For issues and patches go to: https://github.com/erudika
 */
package com.erudika.para.core;

import com.eaio.uuid.UUID;
import com.erudika.para.Para;
import com.erudika.para.annotations.Email;
import com.erudika.para.annotations.Locked;
import com.erudika.para.annotations.Stored;
import com.erudika.para.i18n.CurrencyUtils;
import com.erudika.para.persistence.DAO;
import com.erudika.para.search.Search;
import com.erudika.para.utils.Config;
import com.erudika.para.utils.Pager;
import com.erudika.para.utils.Utils;
import com.fasterxml.jackson.annotation.JsonIgnore;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import org.apache.commons.lang3.StringUtils;
import org.hibernate.validator.constraints.NotBlank;
import org.springframework.util.DigestUtils;

/**
 * The core user object. Stores information about users.
 * @author Alex Bogdanovski [alex@erudika.com]
 */
public class User implements ParaObject {
    private static final long serialVersionUID = 1L;

    @Stored
    @Locked
    private String id;
    @Stored
    @Locked
    private Long timestamp;
    @Stored
    @Locked
    private String type;
    @Stored
    @Locked
    private String appid;
    @Stored
    @Locked
    private String parentid;
    @Stored
    @Locked
    private String creatorid;
    @Stored
    private Long updated;
    @Stored
    private String name;
    @Stored
    private List<String> tags;
    @Stored
    private Integer votes;

    @Stored
    @NotBlank
    private String identifier;
    @Stored
    @Locked
    @NotBlank
    private String groups;
    @Stored
    private Boolean active;
    @Stored
    @NotBlank
    @Email
    private String email;
    @Stored
    private String currency;
    @Stored
    private String picture;
    @Stored
    private String lastIp;
    @Stored
    @Locked
    private String tokenSecret;

    private transient String password;
    private transient DAO dao;
    private transient Search search;

    /**
     * No-args constructor
     */
    public User() {
        this(null);
    }

    /**
     * Default constructor
     * @param id the id
     */
    public User(String id) {
        setId(id);
        setName(getName());
        this.groups = Groups.USERS.toString();
    }

    @Override
    public ParaObject getParent() {
        return this;
    }

    /**
     * Token secret - used for generating JWT tokens.
     * Changing this secret would invalidate all existing user tokens.
     * A kind of global "logout".
     * @return a random string
     */
    @JsonIgnore
    public String getTokenSecret() {
        if (tokenSecret == null) {
            resetTokenSecret();
        }
        return tokenSecret;
    }

    /**
     * Sets the token secret.
     * @param tokenSecret a random string
     */
    public void setTokenSecret(String tokenSecret) {
        this.tokenSecret = tokenSecret;
    }

    /**
     *   The IP address of the user recorded on last login.
     * @return the IP or null
     */
    @JsonIgnore
    public String getLastIp() {
        return lastIp;
    }

    /**
     * Sets the IP of the user.
     * @param lastIp last known IP address
     */
    public void setLastIp(String lastIp) {
        this.lastIp = lastIp;
    }

    /**
     * The profile picture URL.
     * @return a URL or null
     */
    public String getPicture() {
        return StringUtils.isBlank(picture) ? "https://www.gravatar.com/avatar?d=mm&size=400" : picture;
    }

    /**
     * Sets the profile picture URL.
     * @param picture the picture URL.
     */
    public void setPicture(String picture) {
        this.picture = picture;
    }

    /**
     * Is this account active?
     * @return true if active
     */
    public Boolean getActive() {
        if (active == null) {
            active = false;
        }
        return active;
    }

    /**
     * Sets the account active
     * @param active true if active
     */
    public void setActive(Boolean active) {
        this.active = active;
    }

    /**
     * Returns the security groups for this user
     * @return the groups string
     */
    public String getGroups() {
        return groups;
    }

    /**
     * Sets the security groups for this user
     * @param groups the groups string
     */
    public void setGroups(String groups) {
        this.groups = groups;
    }

    /**
     * Returns the main identifier for this user.
     * An identifier is basically a unique username that identifies a user.
     * @return the main identifier
     */
    public String getIdentifier() {
        return identifier;
    }

    /**
     * Sets the main identifier.
     * @param identifier the main identifier
     */
    public void setIdentifier(String identifier) {
        this.identifier = identifier;
    }

    /**
     * The user's email
     * @return email
     */
    public String getEmail() {
        return email;
    }

    /**
     * Sets the email
     * @param email email
     */
    public void setEmail(String email) {
        this.email = email;
    }

    /**
     * The user's currency preference
     * @return a currency 3-letter code in uppercase
     */
    public String getCurrency() {
        return currency;
    }

    /**
     * Sets a preferred currency. Default is "EUR".
     * @param currency a 3-letter currency code
     */
    public void setCurrency(String currency) {
        currency = StringUtils.upperCase(currency);
        if (!CurrencyUtils.getInstance().isValidCurrency(currency)) {
            currency = "EUR";
        }
        this.currency = currency;
    }

    /**
     * Generates a new token secret.
     * This is whould be equivalent to "logout everywhere".
     */
    public void resetTokenSecret() {
        tokenSecret = Utils.generateSecurityToken();
    }

    /**
     * Note: this method assumes that child objects can be modified by their parents.
     * This might not work for special cases where a parent has no rights over a child.
     * @param obj an object
     * @return true if the user is the creator or parent of this object or an admin user
     */
    public boolean canModify(ParaObject obj) {
        if (obj == null || id == null) {
            return false;
        } else {
            boolean isCreatedByMe = obj.getCreatorid() != null
                    && (obj.getCreatorid().startsWith(id + Config.SEPARATOR) || id.equals(obj.getCreatorid()));
            boolean mine = isCreatedByMe || id.equals(obj.getId()) || id.equals(obj.getParentid());
            return (mine || isAdmin());
        }
    }

    @Override
    public String create() {
        if (StringUtils.isBlank(getIdentifier())) {
            return null;
        }
        if (!StringUtils.isBlank(getPassword()) && getPassword().length() < Config.MIN_PASS_LENGTH) {
            return null;
        }

        // admin detected
        if (!Config.ADMIN_IDENT.isEmpty() && Config.ADMIN_IDENT.equals(getIdentifier())) {
            setGroups(User.Groups.ADMINS.toString());
        } else {
            setGroups(User.Groups.USERS.toString());
        }

        setGravatarPicture();

        if (StringUtils.isBlank(tokenSecret)) {
            resetTokenSecret();
        }

        if (getDao().create(getAppid(), this) != null) {
            createIdentifier(getId(), getIdentifier(), getPassword());
        }

        return getId();
    }

    @Override
    public void delete() {
        if (getId() != null) {
            getDao().deleteAll(getAppid(), getIdentifiers());
            getDao().delete(getAppid(), this);
        }
    }

    /**
     * Returns a list of identifiers for this user (can have many).
     * @return a list of {@link Sysprop} objects
     */
    @JsonIgnore
    public List<Sysprop> getIdentifiers() {
        return getSearch().findTerms(getAppid(), Utils.type(Sysprop.class),
                Collections.singletonMap(Config._CREATORID, getId()), true);
    }

    /**
     * Attaches a new identifier to this user.
     * @param identifier a new identifier
     */
    public void attachIdentifier(String identifier) {
        if (this.exists()) {
            createIdentifier(getId(), identifier);
        }
    }

    /**
     * Detaches an identifier from this user.
     * @param identifier an attached identifier
     */
    public void detachIdentifier(String identifier) {
        if (!StringUtils.equals(identifier, getIdentifier())) {
            Sysprop s = getDao().read(getAppid(), identifier);
            if (s != null && StringUtils.equals(getId(), s.getCreatorid())) {
                deleteIdentifier(identifier);
            }
        }
    }

    /**
     * Is the main identifier a Facebook id
     * @return true if user is signed in with Facebook
     */
    @JsonIgnore
    public boolean isFacebookUser() {
        return StringUtils.startsWithIgnoreCase(identifier, Config.FB_PREFIX);
    }

    /**
     * Is the main identifier a Google+ id
     * @return true if user is signed in with Google+
     */
    @JsonIgnore
    public boolean isGooglePlusUser() {
        return StringUtils.startsWithIgnoreCase(identifier, Config.GPLUS_PREFIX);
    }

    /**
     * Is the main identifier a LinkedIn id
     * @return true if user is signed in with LinkedIn
     */
    @JsonIgnore
    public boolean isLinkedInUser() {
        return StringUtils.startsWithIgnoreCase(identifier, Config.LINKEDIN_PREFIX);
    }

    /**
     * Is the main identifier a Twitter id
     * @return true if user is signed in with Twitter
     */
    @JsonIgnore
    public boolean isTwitterUser() {
        return StringUtils.startsWithIgnoreCase(identifier, Config.TWITTER_PREFIX);
    }

    /**
     * Is the main identifier a GitHub id
     * @return true if user is signed in with GitHub
     */
    @JsonIgnore
    public boolean isGitHubUser() {
        return StringUtils.startsWithIgnoreCase(identifier, Config.GITHUB_PREFIX);
    }

    /**
     * Checks for admin rights
     * @return true if user has admin rights
     */
    @JsonIgnore
    public boolean isAdmin() {
        return StringUtils.equalsIgnoreCase(this.groups, Groups.ADMINS.toString());
    }

    /**
     * Checks for moderator rights
     * @return true if user has mod rights
     */
    @JsonIgnore
    public boolean isModerator() {
        return isAdmin() ? true : StringUtils.equalsIgnoreCase(this.groups, Groups.MODS.toString());
    }

    /**
     * Returns the name of the identity provider.
     * @return "facebook", "google"... etc.
     */
    public String getIdentityProvider() {
        if (isFacebookUser()) {
            return "facebook";
        } else if (isGooglePlusUser()) {
            return "google";
        } else if (isGitHubUser()) {
            return "github";
        } else if (isTwitterUser()) {
            return "twitter";
        } else if (isLinkedInUser()) {
            return "linkedin";
        } else {
            return "none";
        }
    }

    /**
     * The password. A transient field used for validation.
     * @return the password.
     */
    @JsonIgnore
    public String getPassword() {
        return password;
    }

    /**
     * Sets a password.
     * @param password a password
     */
    public void setPassword(String password) {
        this.password = password;
    }

    /**
     * Returns a user object for a given identifier.
     * @param u a user having a valid identifier set.
     * @return a user or null if no user is found for this identifier
     */
    public static final User readUserForIdentifier(User u) {
        if (u == null || StringUtils.isBlank(u.getIdentifier())) {
            return null;
        }
        String identifier = u.getIdentifier();
        Sysprop s = u.getDao().read(u.getAppid(), identifier);
        if (s != null && s.getCreatorid() != null) {
            User user = u.getDao().read(u.getAppid(), s.getCreatorid());
            if (user != null) {
                if (!identifier.equals(user.getIdentifier())) {
                    // the main identifier was changed - update
                    user.setIdentifier(identifier);
                    u.getDao().update(user);
                }
                user.setPassword((String) s.getProperty(Config._PASSWORD));
                return user;
            }
        }
        return null;
    }

    /**
     * Checks if a user has entered the correct password.
     * Compares password hashes.
     * @param u a user with a set password
     * @return true if password matches the one in the data store
     */
    public static final boolean passwordMatches(User u) {
        if (u == null) {
            return false;
        }
        String password = u.getPassword();
        String identifier = u.getIdentifier();
        if (StringUtils.isBlank(password) || StringUtils.isBlank(identifier)) {
            return false;
        }
        Sysprop s = u.getDao().read(u.getAppid(), identifier);
        if (s != null) {
            String storedHash = (String) s.getProperty(Config._PASSWORD);
            return Utils.bcryptMatches(password, storedHash);
        }
        return false;
    }

    /**
     * Generates a new password reset token. Sent via email for pass reset.
     * @return the pass reset token
     */
    public final String generatePasswordResetToken() {
        if (StringUtils.isBlank(identifier)) {
            return "";
        }
        Sysprop s = getDao().read(getAppid(), identifier);
        if (s != null) {
            String token = Utils.generateSecurityToken(42, true);
            s.addProperty(Config._RESET_TOKEN, token);
            getDao().update(getAppid(), s);
            return token;
        }
        return "";
    }

    /**
     * Changes the user password permanently.
     * @param token the reset token. see {@link #generatePasswordResetToken()}
     * @param newpass the new password
     * @return true if successful
     */
    public final boolean resetPassword(String token, String newpass) {
        if (StringUtils.isBlank(newpass) || StringUtils.isBlank(token)
                || newpass.length() < Config.MIN_PASS_LENGTH) {
            return false;
        }
        Sysprop s = getDao().read(getAppid(), identifier);
        if (isValidToken(s, Config._RESET_TOKEN, token)) {
            s.removeProperty(Config._RESET_TOKEN);
            String hashed = Utils.bcrypt(newpass);
            s.addProperty(Config._PASSWORD, hashed);
            setPassword(hashed);
            getDao().update(getAppid(), s);
            return true;
        }
        return false;
    }

    private boolean createIdentifier(String userid, String newIdent) {
        return createIdentifier(userid, newIdent, null);
    }

    /**
     * Creates a new identifier object using {@link Sysprop}.
     * Used for identifying a user when signing in.
     * @param userid a user id
     * @param newIdent a new identifier
     * @param password a password for the user (optional)
     * @return true if successful
     */
    private boolean createIdentifier(String userid, String newIdent, String password) {
        if (StringUtils.isBlank(userid) || StringUtils.isBlank(newIdent)) {
            return false;
        }
        Sysprop s = new Sysprop();
        s.setId(newIdent);
        s.setName(Config._IDENTIFIER);
        s.setCreatorid(userid);
        if (!StringUtils.isBlank(password)) {
            String hashed = Utils.bcrypt(password);
            s.addProperty(Config._PASSWORD, hashed);
            setPassword(hashed);
        }
        return getDao().create(getAppid(), s) != null;
    }

    /**
     * Deletes the identifier and the user can no longer sign in with it.
     * @param ident the attached identifier
     */
    private void deleteIdentifier(String ident) {
        if (!StringUtils.isBlank(ident)) {
            getDao().delete(getAppid(), new Sysprop(ident));
        }
    }

    /**
     * Generates a new email confirmation token. Sent via email for user activation.
     * @return a Base64 encoded UUID
     */
    public String generateEmailConfirmationToken() {
        if (StringUtils.isBlank(identifier)) {
            return "";
        }
        Sysprop s = getDao().read(getAppid(), identifier);
        if (s != null) {
            String token = Utils.base64encURL(new UUID().toString().getBytes());
            s.addProperty(Config._EMAIL_TOKEN, token);
            getDao().update(getAppid(), s);
            return token;
        }
        return "";
    }

    /**
     * Activates a user if a given token matches the one stored.
     * @param token the email confirmation token. see {@link #generateEmailConfirmationToken() }
     * @return true if successful
     */
    public final boolean activateWithEmailToken(String token) {
        Sysprop s = getDao().read(getAppid(), identifier);
        if (isValidToken(s, Config._EMAIL_TOKEN, token)) {
            s.removeProperty(Config._EMAIL_TOKEN);
            getDao().update(getAppid(), s);
            setActive(true);
            update();
            return true;
        }
        return false;
    }

    /**
     * Validates a token sent via email for password reset.
     * @param token a token
     * @return true if valid
     */
    public final boolean isValidPasswordResetToken(String token) {
        Sysprop s = getDao().read(getAppid(), identifier);
        return isValidToken(s, Config._RESET_TOKEN, token);
    }

    /**
     * Validates a token sent for email confirmation.
     * @param token a token
     * @return true if valid
     */
    public final boolean isValidEmailConfirmationToken(String token) {
        Sysprop s = getDao().read(getAppid(), identifier);
        return isValidToken(s, Config._EMAIL_TOKEN, token);
    }

    private boolean isValidToken(Sysprop s, String key, String token) {
        if (StringUtils.isBlank(token)) {
            return false;
        }
        if (s != null && s.hasProperty(key)) {
            String storedToken = (String) s.getProperty(key);
            // tokens expire afer a reasonably short period ~ 30 mins
            long timeout = Config.PASSRESET_TIMEOUT_SEC * 1000;
            if (StringUtils.equals(storedToken, token) && (s.getUpdated() + timeout) > Utils.timestamp()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Sets the profile picture using the Gravatar service.
     */
    private void setGravatarPicture() {
        if (StringUtils.isBlank(picture)) {
            if (email != null) {
                String emailHash = DigestUtils.md5DigestAsHex(email.getBytes());
                setPicture("https://www.gravatar.com/avatar/" + emailHash + "?size=400&d=mm&r=pg");
            } else {
                setPicture("https://www.gravatar.com/avatar?d=mm&size=400");
            }
        }
    }

    /**
     * Simple groups enum
     */
    public static enum Groups {
        /**
         * The standard user group
         */
        USERS,
        /**
         * Moderators group
         */
        MODS,
        /**
         * Administrators group
         */
        ADMINS;

        public String toString() {
            return this.name().toLowerCase();
        }
    }

    /**
     * Simple user roles enum
     */
    public static enum Roles {
        /**
         * The standard role
         */
        USER,
        /**
         * The moderator role
         */
        MOD,
        /**
         * The administrator role
         */
        ADMIN;

        public String toString() {
            return "ROLE_".concat(this.name());
        }
    }

    ////////////////////////////////////////////////////////

    @Override
    public final String getId() {
        return id;
    }

    @Override
    public final void setId(String id) {
        this.id = id;
    }

    @Override
    public final String getType() {
        type = (type == null) ? Utils.type(this.getClass()) : type;
        return type;
    }

    @Override
    public final void setType(String type) {
        this.type = type;
    }

    @Override
    public String getAppid() {
        appid = (appid == null) ? Config.APP_NAME_NS : appid;
        return appid;
    }

    @Override
    public void setAppid(String appid) {
        this.appid = appid;
    }

    @Override
    public String getObjectURI() {
        return CoreUtils.getObjectURI(this);
    }

    @Override
    public List<String> getTags() {
        return tags;
    }

    @Override
    public void setTags(List<String> tags) {
        this.tags = tags;
    }

    @Override
    public Long getTimestamp() {
        return (timestamp != null && timestamp != 0) ? timestamp : null;
    }

    @Override
    public void setTimestamp(Long timestamp) {
        this.timestamp = timestamp;
    }

    @Override
    public String getCreatorid() {
        return creatorid;
    }

    @Override
    public void setCreatorid(String creatorid) {
        this.creatorid = creatorid;
    }

    @Override
    public final String getName() {
        return CoreUtils.getName(name, id);
    }

    @Override
    public final void setName(String name) {
        this.name = (name == null || !name.isEmpty()) ? name : this.name;
    }

    @Override
    public String getPlural() {
        return Utils.singularToPlural(getType());
    }

    @Override
    public ParaObject getCreator() {
        return getDao().read(getAppid(), creatorid);
    }

    @Override
    public String getParentid() {
        return parentid;
    }

    @Override
    public void setParentid(String parentid) {
        this.parentid = parentid;
    }

    @Override
    public Long getUpdated() {
        return (updated != null && updated != 0) ? updated : null;
    }

    @Override
    public void setUpdated(Long updated) {
        this.updated = updated;
    }

    @Override
    public void update() {
        getDao().update(getAppid(), this);
    }

    @Override
    public boolean exists() {
        return getDao().read(id) != null;
    }

    @Override
    public DAO getDao() {
        if (dao == null) {
            dao = Para.getDAO();
        }
        return dao;
    }

    @Override
    public void setDao(DAO dao) {
        this.dao = dao;
    }

    @Override
    public Search getSearch() {
        if (search == null) {
            search = Para.getSearch();
        }
        return search;
    }

    @Override
    public void setSearch(Search search) {
        this.search = search;
    }

    @Override
    public boolean voteUp(String userid) {
        return CoreUtils.vote(this, userid, VoteValue.UP);
    }

    @Override
    public boolean voteDown(String userid) {
        return CoreUtils.vote(this, userid, VoteValue.DOWN);
    }

    @Override
    public Integer getVotes() {
        return (votes == null) ? 0 : votes;
    }

    @Override
    public void setVotes(Integer votes) {
        this.votes = votes;
    }

    @Override
    public Long countLinks(String type2) {
        return CoreUtils.countLinks(this, type2);
    }

    @Override
    public List<Linker> getLinks(String type2, Pager... pager) {
        return CoreUtils.getLinks(this, type2, pager);
    }

    @Override
    public <P extends ParaObject> List<P> getLinkedObjects(String type, Pager... pager) {
        return CoreUtils.getLinkedObjects(this, type, pager);
    }

    @Override
    public boolean isLinked(String type2, String id2) {
        return CoreUtils.isLinked(this, type2, id2);
    }

    @Override
    public boolean isLinked(ParaObject toObj) {
        return CoreUtils.isLinked(this, toObj);
    }

    @Override
    public String link(String id2) {
        return CoreUtils.link(this, id2);
    }

    @Override
    public void unlink(String type, String id2) {
        CoreUtils.unlink(this, type, id2);
    }

    @Override
    public void unlinkAll() {
        CoreUtils.unlinkAll(this);
    }

    @Override
    public Long countChildren(String type) {
        return CoreUtils.countChildren(this, type);
    }

    @Override
    public <P extends ParaObject> List<P> getChildren(String type, Pager... pager) {
        return CoreUtils.getChildren(this, type, pager);
    }

    @Override
    public <P extends ParaObject> List<P> getChildren(String type, String field, String term, Pager... pager) {
        return CoreUtils.getChildren(this, type, field, term, pager);
    }

    @Override
    public void deleteChildren(String type) {
        CoreUtils.deleteChildren(this, type);
    }

    @Override
    public int hashCode() {
        int hash = 7;
        hash = 67 * hash + Objects.hashCode(this.id) + Objects.hashCode(this.name);
        return hash;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final ParaObject other = (ParaObject) obj;
        if (!Objects.equals(this.id, other.getId())) {
            return false;
        }
        return true;
    }

    @Override
    public String toString() {
        return ParaObjectUtils.toJSON(this);
    }
}