User.java

package com.tradecloud.authentication;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.tradecloud.common.base.PersistenceBase;
import com.tradecloud.common.externalreference.IntegratedSystem;
import com.tradecloud.domain.base.utils.DateUtils;
import com.tradecloud.domain.configuration.*;
import com.tradecloud.domain.model.organisationalunit.OrganisationalUnit;
import com.tradecloud.domain.party.Employee;
import com.tradecloud.domain.party.ServiceProvider;
import com.tradecloud.domain.party.base.Contact;
import com.tradecloud.domain.place.Region;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.FetchMode;
import org.hibernate.annotations.ForeignKey;
import org.hibernate.annotations.NaturalId;
import org.springframework.security.core.userdetails.UserDetails;

import javax.persistence.*;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import javax.xml.bind.annotation.*;
import java.io.Serializable;
import java.security.Principal;
import java.util.*;

/**
 * We'd normally call DB tables by the singular e.g. user But user is a reserved
 * word in PSQL so we make the exception here and use users.
 */
@Entity
@Table(name = "users", uniqueConstraints = {@UniqueConstraint(columnNames = {"username"})})
@Access(AccessType.FIELD)
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement
public class User extends PersistenceBase implements Serializable, UserDetails, Principal, Comparable<User> {

    private static final long serialVersionUID = 1L;

    public static final int PASSWORD_EXPIRY_DAYS = 30;

    public static final int PASSWORD_EXPIRY_WARNING_DAYS = 5;

    public static final int MAX_INCORRECT_LOGIN_ATTEMPTS = 3;

    @XmlAttribute
    @NotNull(message = "Username is required")
    @Size(max = 255, message = "Username must be less than 255 characters")
    @NaturalId
    private String username;

    @NotNull(message = "Password is required")
    @XmlAttribute
    private String password;

    @XmlAttribute
    @Temporal(javax.persistence.TemporalType.TIMESTAMP)
    private Date passwordChangedDate;

    @XmlAttribute
    @Transient
    private String plainCurrentPassword;

    @XmlAttribute
    @Transient
    private String plainNewPassword;

    @NotNull(message = "Primary client is required")
    @XmlElement
    private String primaryClient;

    @XmlAttribute
    private String hash = SHA1;

    //    @Autowired
    @OneToOne
    private Contact contact;
    private boolean superUser;

    //This user can manage attributes and information hidden from clients
//must be set only via the script,client users should always be false
    private boolean internalAdmin;

    private transient boolean supplier;

    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(name = "user_role", joinColumns = {@JoinColumn(name = "user_id")}, inverseJoinColumns = {@JoinColumn(name = "authority")})
    @ForeignKey(name = "fk_role_user", inverseName = "fk_user_role")
    @XmlElementWrapper(name = "Roles")
    @XmlElement(name = "Role")
    private Set<Role> roles = new HashSet<>();

    // these events had to be made xmltransient for
    // JAXB to covert the user to XML in UIResourceImpl#whoAmI()
    @XmlTransient
    @ManyToMany(cascade = CascadeType.ALL, mappedBy = "users")
    @ForeignKey(name = "fk_integrated_user", inverseName = "fk_orderevents")
    private Set<OrderEvents> orderEvents = new HashSet<>();

    @XmlTransient
    @ManyToMany(cascade = CascadeType.ALL, mappedBy = "users")
    @ForeignKey(name = "fk_integrated_user", inverseName = "fk_costingevents")
    private Set<CostingEvents> costingEvents = new HashSet<>();

    @XmlTransient
    @ManyToMany(cascade = CascadeType.ALL, mappedBy = "users")
    @ForeignKey(name = "fk_integrated_user", inverseName = "fk_logisticsevents")
    private Set<LogisticsEvents> logisticsEvents = new HashSet<>();

    @XmlTransient
    @ManyToMany(cascade = CascadeType.ALL, mappedBy = "users")
    @ForeignKey(name = "fk_integrated_user", inverseName = "fk_accountingevents")
    private Set<AccountingEvents> accountingEvents = new HashSet<>();

    @XmlTransient
    @ManyToMany(cascade = CascadeType.ALL, mappedBy = "users")
    @ForeignKey(name = "fk_integrated_user", inverseName = "fk_supplierevents")
    private Set<SupplierEvents> supplierEvents = new HashSet<>();

    @XmlTransient
    @ManyToMany(cascade = CascadeType.ALL, mappedBy = "users")
    @ForeignKey(name = "fk_integrated_user", inverseName = "fk_productevents")
    private Set<ProductEvents> productEvents = new HashSet<>();

    @XmlTransient
    @ManyToMany(cascade = CascadeType.ALL, mappedBy = "users")
    @ForeignKey(name = "fk_integrated_user", inverseName = "fk_dealeventconfig")
    private Set<DealEventConfig> dealEventConfigs = new HashSet<>();

    @OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
    @ForeignKey(name = "fk_userinfo")
    @XmlElement(name = "UserInfo")
    private UserInfo userInfo;

    @ManyToOne
    @XmlElement(name = "ServiceProvider")
    private ServiceProvider serviceProvider;

    @ManyToOne
    @XmlElement(name = "IntegratedSystem")
    private IntegratedSystem integratedSystem;

    @XmlAttribute
    private boolean enabled;

    @XmlAttribute
    private boolean accountNonExpired = true;

/*    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(name = "costingclientconfig_organisationalUnits", joinColumns = {@JoinColumn(name = "costingclientconfig_id", unique = false)},
            inverseJoinColumns = {@JoinColumn(name = "organisationalUnit_id", unique = false)})

    @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.DETACH)*/
    //@Fetch(value = FetchMode.SUBSELECT)
    //private Set<OrganisationalUnit> organisationalUnit = new HashSet<OrganisationalUnit>();

    @XmlElementWrapper(name = "organisationalUnits")
    @XmlElement(name = "OrganisationalUnit")
    @NotNull(message = "user must linked to org unit")
    @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.DETACH)
    @JoinTable(name = "organisationalunit_user", joinColumns = {@JoinColumn(name = "user_id", unique = false)},
            inverseJoinColumns = {@JoinColumn(name = "organisationalunit_id", unique = false)})
    private Set<OrganisationalUnit> organisationalUnit = new HashSet<>();

    @XmlAttribute
    private String passwordHint;

    @NotNull(message = "Incorrect login attempts is required")
    private int incorrectLoginAttempts = 0;

    // Integration users shouldn't have their password expire. I think.
    private boolean passwordExpires = true;

    @XmlAttribute
    @Temporal(javax.persistence.TemporalType.TIMESTAMP)
    private Date lastLoginDate;

    @ManyToOne
    @ForeignKey(name = "fk_createdby")
    private User createdBy;

    @ManyToOne
    @ForeignKey(name = "fk_lastupdateby")
    private User lastUpdateBy;

    @XmlTransient
    @ElementCollection(fetch = FetchType.LAZY)
    @CollectionTable(name = "user_passwordhistories", joinColumns = {@JoinColumn(name = "user_id", unique = false)})
    @ForeignKey(name = "fk_user")
    @Fetch(value = FetchMode.SELECT)
    private List<PasswordHistory> passwordHistory = new ArrayList<>();

    @Enumerated(EnumType.STRING)
    private UserStyle userStyle;

    private static final String SHA1 = "SHA-1";

    @Enumerated(value = EnumType.STRING)
    private CostingSnapShot costingSnapShot;

    private transient Set<OrganisationalUnit> organisationalUnits = new HashSet<>();

    @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
    private Employee employee;

//    @XmlTransient
//    @OneToMany
//    @JsonIgnore
//    @JoinTable(name = "user_finalDestinations")
//    private Set<FinalDestination> finalDestinations= new HashSet<>();

    @XmlTransient
    @OneToMany
    @JsonIgnore
    @JoinTable(name = "user_regions",
            joinColumns = @JoinColumn(name = "users_id"),
            inverseJoinColumns = @JoinColumn(name = "regions_code", unique = false))
    @Fetch(value = FetchMode.SELECT)
    private Set<Region> regions= new HashSet<>();


    public User(String password, String username, Collection<Role> roles, boolean enabled, boolean accountNonExpired,
                String primaryClient, String hash) {

        encodePassword(password);
        this.username = username;
        this.roles = new HashSet<>(roles);
        this.enabled = enabled;
        this.accountNonExpired = accountNonExpired;
        this.primaryClient = primaryClient;
        this.hash = hash;
    }

    @Override
    public Collection<Role> getAuthorities() {
        return getRoles();
    }

    public void setAuthorities(Collection<Role> newAuthorities) {
        this.setRoles(new HashSet<>(newAuthorities));
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }

    @Override
    public boolean isAccountNonExpired() {
        return accountNonExpired;
    }

    public boolean setAccountNonExpired(boolean accountNonExpired) {
        return this.accountNonExpired = accountNonExpired;
    }

    @Override
    public boolean isAccountNonLocked() {
        boolean locked = !enabled || (incorrectLoginAttempts > MAX_INCORRECT_LOGIN_ATTEMPTS);
        return !locked;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        boolean expired =
                passwordExpires && passwordChangedDate != null
                        && (DateUtils.getDaysBetween2(passwordChangedDate, new Date()) >= PASSWORD_EXPIRY_DAYS);
        return !expired;
    }

    public User() {
    }

    /**
     * Constructor for creating temp user object just for authentication against the framework.
     *
     * @param username   The Username
     * @param clientCode The ClientCode
     */
    public User(String username, String clientCode) {
        this.username = username;
        this.password = username;
        this.primaryClient = clientCode;
    }

    public static User create(String username, String password, String primaryClient, List<String> roleStrings) {
        List<Role> roles = new ArrayList<>();
        for (String role : roleStrings) {
            roles.add(new Role(role));
        }
        return new User(password, username, roles, true, true, primaryClient, PasswordEncode.SHA_1);
    }

    public boolean hasAuthority(final Role authority) {
        return getRoles().contains(authority);
    }

    public boolean hasAuthority(final String authority) {
        return getRoles().contains(new Role(authority));
    }

    public boolean hasAuthority(final String... authorities) {
        for (String authority : authorities) {
            if (getRoles().contains(new Role(authority))) {
                return true;
            }
        }
        return false;
    }

    public int getIncorrectLoginAttempts() {
        return incorrectLoginAttempts;
    }

    public void setIncorrectLoginAttempts(int incorrectLoginAttempts) {
        this.incorrectLoginAttempts = incorrectLoginAttempts;
    }

    public boolean getPasswordExpires() {
        return passwordExpires;
    }

    public void setPasswordExpires(boolean passwordExpires) {
        this.passwordExpires = passwordExpires;
    }

    public List<PasswordHistory> getPasswordHistory() {
        return passwordHistory;
    }

    public void setPasswordHistory(List<PasswordHistory> passwordHistory) {
        this.passwordHistory = passwordHistory;
    }

    public String getPlainCurrentPassword() {
        return plainCurrentPassword;
    }

    public void setPlainCurrentPassword(String plainCurrentPassword) {
        this.plainCurrentPassword = plainCurrentPassword;
    }

    public UserStyle getUserStyle() {
        return userStyle;
    }

    public void setUserStyle(UserStyle userStyle) {
        this.userStyle = userStyle;
    }

    @Embeddable
    @Access(AccessType.FIELD)
    public static class Password {

        private String password;

        /**
         * Used by Hibernate.
         */
        private Password() {

        }

        public static Password encode(String plaintext) {
            return new Password(PasswordEncode.createPasswordHash(SHA1, PasswordEncode.BASE16_ENCODING, "UTF-16", "", plaintext));
        }

        private Password(String password) {
            this.password = password;
        }

        @Override
        public boolean equals(final Object other) {
            if (!(other instanceof Password)) {
                return false;
            }
            Password castOther = (Password) other;
            return new EqualsBuilder().append(password, castOther.password).isEquals();
        }

        @Override
        public int hashCode() {
            return new HashCodeBuilder().append(password).toHashCode();
        }

        @Override
        public String toString() {
            return new ToStringBuilder(this).append("password", password).toString();
        }

    }

    public UserInfo getUserInfo() {
        return userInfo;
    }

    public void setUserInfo(UserInfo userInfo) {
        this.userInfo = userInfo;
    }

    @Override
    public String getName() {
        return getUsername();
    }

    public String getHash() {
        return hash;
    }

    public void setHash(String hash) {
        this.hash = hash;
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

    /**
     * Can the user login with the supplied plain text password.
     *
     * @param plaintext
     * @return
     */
    public boolean canLogin(String plaintext) {
        return this.password.equals(Password.encode(plaintext).password);
    }

    /**
     * Encodes the given plaintext into the password.
     *
     * @param password
     */
    public final void encodePassword(String password) {
        // A bit weird, but use null as the case where the user doesn't want to change the password
        if (password != null) {
            this.password = PasswordEncode.encode(password);
        }
    }

    /**
     * Password it encoded. Only to be used when sending to the backend.
     * Have moved this logic into encodePassword. Clearer, and will probably
     * interfer with hibernate and jaxb less.
     *
     * @param password
     */
    public void setPassword(String password) {
        this.password = password;
    }

    public String getPlainNewPassword() {
        return plainNewPassword;
    }

    public void setPlainNewPassword(String plainNewPassword) {
        this.plainNewPassword = plainNewPassword;
    }

    public String getPasswordHint() {
        return passwordHint;
    }

    public void setPasswordHint(String passwordHint) {
        this.passwordHint = passwordHint;
    }

    @Override
    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPasswordChangedDate(Date now) {
        passwordChangedDate = now;
    }

    public Date getPasswordChangedDate() {
        return passwordChangedDate;
    }

    /**
     * Not using any potentially lazy fields.
     *
     * @return
     */
    @Override
    public int hashCode() {
        return new HashCodeBuilder()
                .append(username)
                .append(enabled)
                .append(passwordChangedDate)
                .append(passwordHint)
                .append(primaryClient)
                .append(passwordExpires)
                .hashCode();
    }

    /**
     * Not using any potentially lazy fields.
     *
     * @param obj
     * @return
     */
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (!(obj instanceof User)) {
            return false;
        }
        User other = (User) obj;

        return new EqualsBuilder()
                .append(username, other.getUsername())
                .append(enabled, other.isEnabled())
                .append(passwordChangedDate, other.getPasswordChangedDate())
                .append(passwordHint, other.getPasswordHint())
                .append(primaryClient, other.getPrimaryClient())
                .append(passwordExpires, other.getPasswordExpires())
                .isEquals();
    }

    /**
     * If the allowMultipleClients variable is falst then always return the
     * first element of the allowedClients. Otherwise return the logged in
     * client.
     */
    public String getActiveClient() {
        return primaryClient;
    }

    public String getPrimaryClient() {
        return primaryClient;
    }

    public void setPrimaryClient(String primaryClient) {
        this.primaryClient = primaryClient;
    }

    /**
     * For debug only. Do not put fields in here that might be cached or lazy loaded.
     *
     * @return
     */
    @Override
    public String toString() {
        return new ToStringBuilder(this)
                .append("username", username)
                .append("enabled", enabled)
                .append("passwordChangeDate", passwordChangedDate)
                .append("passwordHint", passwordHint)
                .append("primaryClient", primaryClient)
                .append("passwordExpires", passwordExpires)
                .toString();
    }

    public Set<Role> getRoles() {
        return roles;
    }

    public List<Role> getRolesList() {
        List<Role> list = new ArrayList<>(roles);
        Collections.sort(list);
        return list;
    }

    public void setRoles(Set<Role> roles) {
        this.roles = new HashSet<>(roles);
    }

    public void setRolesList(List<Role> roles) {
        this.roles = new HashSet<>(roles);
    }

    @Override
    public int compareTo(User o) {
        return getUsername().compareToIgnoreCase(o.getUsername());
    }

    public Set<OrderEvents> getOrderEvents() {
        return orderEvents;
    }

    public void setOrderEvents(Set<OrderEvents> orderEvents) {
        this.orderEvents = orderEvents;
    }

    public List<OrderEvents> getOrderEventsAsList() {
        return new ArrayList<>(getOrderEvents());
    }

    public void setOrderEventsAsList(List<OrderEvents> orderEvents) {
        setOrderEvents(new HashSet<>(orderEvents));
    }

    public Set<SupplierEvents> getSupplierEvents() {
        return supplierEvents;
    }

    public List<SupplierEvents> getSupplierEventsAsList() {
        return new ArrayList<>(getSupplierEvents());
    }

    public void setSupplierEventsAsList(List<SupplierEvents> supplierEvents) {
        setSupplierEvents(new HashSet<>(supplierEvents));
    }

    public void setSupplierEvents(Set<SupplierEvents> supplierEvents) {
        this.supplierEvents = supplierEvents;
    }

    public Set<ProductEvents> getProductEvents() {
        return productEvents;
    }

    public void setProductEvents(Set<ProductEvents> productEvents) {
        this.productEvents = productEvents;
    }

    public List<ProductEvents> getProductEventsAsList() {
        return new ArrayList<>(getProductEvents());
    }

    public void setProductEventsAsList(List<ProductEvents> productEvents) {
        setProductEvents(new HashSet<>(productEvents));
    }

    public Set<CostingEvents> getCostingEvents() {
        return costingEvents;
    }

    public void setCostingEvents(Set<CostingEvents> costingEvents) {
        this.costingEvents = costingEvents;
    }

    public List<CostingEvents> getCostingEventsAsList() {
        return new ArrayList<>(getCostingEvents());
    }

    public void setCostingEventsAsList(List<CostingEvents> costingEvents) {
        setCostingEvents(new HashSet<>(costingEvents));
    }

    public Set<LogisticsEvents> getLogisticsEvents() {
        return logisticsEvents;
    }

    public void setLogisticsEvents(Set<LogisticsEvents> logisticsEvents) {
        this.logisticsEvents = logisticsEvents;
    }

    public List<LogisticsEvents> getLogisticsEventsAsList() {
        return new ArrayList<>(getLogisticsEvents());
    }

    public void setLogisticsEventsAsList(List<LogisticsEvents> logisticsEvents) {
        setLogisticsEvents(new HashSet<>(logisticsEvents));
    }

    public Set<AccountingEvents> getAccountingEvents() {
        return accountingEvents;
    }

    public void setAccountingEvents(Set<AccountingEvents> accountingEvents) {
        this.accountingEvents = accountingEvents;
    }

    public List<AccountingEvents> getAccountingEventsAsList() {
        return new ArrayList<>(getAccountingEvents());
    }

    public void setAccountingEventsAsList(List<AccountingEvents> costingEvents) {
        setAccountingEvents(new HashSet<>(costingEvents));
    }

    public List<DealEventConfig> getDealEventConfigsAsList() {
        return new ArrayList<>(getDealEventConfigs());
    }

    public Set<OrganisationalUnit> getOrganisationalUnit() {
        return organisationalUnit;
    }

    public void setOrganisationalUnit(Set<OrganisationalUnit> organisationalUnit) {
        this.organisationalUnit = organisationalUnit;
    }

    public ServiceProvider getServiceProvider() {
        return serviceProvider;
    }

    public void setServiceProvider(ServiceProvider serviceProvider) {
        this.serviceProvider = serviceProvider;
    }

    public IntegratedSystem getIntegratedSystem() {
        return integratedSystem;
    }

    public void setIntegratedSystem(IntegratedSystem integratedSystem) {
        this.integratedSystem = integratedSystem;
    }

    public Date getLastLoginDate() {
        return lastLoginDate;
    }

    public void setLastLoginDate(Date lastLoginDate) {
        this.lastLoginDate = lastLoginDate;
    }

    public User getCreatedBy() {
        return createdBy;
    }

    public void setCreatedBy(User createBy) {
        this.createdBy = createBy;
    }

    public User getLastUpdateBy() {
        return lastUpdateBy;
    }

    public void setLastUpdateBy(User lastUpdateBy) {
        this.lastUpdateBy = lastUpdateBy;
    }

    public Set<DealEventConfig> getDealEventConfigs() {
        return dealEventConfigs;
    }

    public void setDealEventConfigs(Set<DealEventConfig> dealEventConfigs) {
        this.dealEventConfigs = dealEventConfigs;
    }

    public CostingSnapShot getCostingSnapShot() {
        return costingSnapShot;
    }

    public void setCostingSnapShot(CostingSnapShot costingSnapShot) {
        this.costingSnapShot = costingSnapShot;
    }

    public Set<OrganisationalUnit> getOrganisationalUnits() {
        return organisationalUnits;
    }

    public void setOrganisationalUnits(Set<OrganisationalUnit> organisationalUnits) {
        this.organisationalUnits = organisationalUnits;
    }

    public Contact getContact() {
        return contact;
    }

    public void setContact(Contact contact) {
        this.contact = contact;
    }

    public Employee getEmployee() {
        return employee;
    }

    public void setEmployee(Employee employee) {
        this.employee = employee;
    }

    public boolean isSuperUser() {
        return superUser;
    }

    public void setSuperUser(boolean superUser) {
        //can only be set by script
    }

    public boolean isInternalAdmin() {
        return internalAdmin;
    }

    public void _setInternalAdmin(boolean internalAdmin) {
        //can only be set by script
        //can only for test cases
        this.internalAdmin = internalAdmin;
    }

    public boolean isSupplier() {
        return supplier;
    }

    public void setSupplier(boolean supplier) {
        this.supplier = supplier;
    }

//    public Set<FinalDestination> getFinalDestinations() {
//        return finalDestinations;
//    }
//
//    public void setFinalDestinations(Set<FinalDestination> finalDestinations) {
//        this.finalDestinations = finalDestinations;
//    }

    public Set<Region> getRegions() {
        return regions;
    }

    public void setRegions(Set<Region> regions) {
        this.regions = regions;
    }
}