CustomMenu

Saturday, April 26, 2014

Spring Thymeleaf CRUD User Maintenance - Part 1

In this series of posts we will go back to coding the infrastructure around the option trading system.  We will build on the code from the last series, Spring Security 3.2 - Users, Roles, Permissions, and add CRUD functionality to the User entity.  While we are at it, we will also refactor some of the CRUD code for the Strategy entity.

1. Let's start by updating the security configuration.  We will be adding a controller to handle requests for user information and will have this controller handle URLs that match "/user".  The security configuration will need to be updated to allow access to this path as shown below.
package com.dtr.oas.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import com.dtr.oas.exception.AccessDeniedExceptionHandler;

@Configuration
@EnableWebMvcSecurity
@ComponentScan(basePackageClasses=com.dtr.oas.service.UserServiceImpl.class)
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Autowired
    public void configureGlobal(UserDetailsService userDetailsService, AuthenticationManagerBuilder auth) throws Exception {
        auth
            .userDetailsService(userDetailsService);
    }
   
    @Autowired
    AccessDeniedExceptionHandler accessDeniedExceptionHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/resources/**").permitAll()
                .antMatchers("/error/**").permitAll()
                .antMatchers("/strategy/**").hasRole("ADMIN")
                .antMatchers("/user/**").hasRole("ADMIN")
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/")
                .permitAll()
                .and()
            .logout()
                .permitAll()
                .and()
            .exceptionHandling()
                .accessDeniedHandler(accessDeniedExceptionHandler);
    }
}

2. Now we will move down to the entity layer. After a bit of thought, I've decided to change the User entity to have a one-to-one mapping to Role, rather than a one-to-many relationship.  This will allow a user to have only one role (rather than many), but fine grained permissions associated with this role.  The changes to the user object are highlighted below.
package com.dtr.oas.model;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinTable;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import org.hibernate.validator.constraints.NotEmpty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import com.google.common.base.Objects;

@Entity  
@Table(name="USERS")
public class User extends BaseEntity implements UserDetails {

    private static final long serialVersionUID = 6311364761937265306L;
    static Logger logger = LoggerFactory.getLogger(User.class);
    
    @NotNull(message = "{error.user.username.null}")
    @NotEmpty(message = "{error.user.username.empty}")
    @Size(max = 50, message = "{error.user.username.max}")
    @Column(name = "username", length = 50)
    private String username;

    @NotNull(message = "{error.user.password.null}")
    @NotEmpty(message = "{error.user.password.empty}")
    @Size(max = 50, message = "{error.user.password.max}")
    @Column(name = "password", length = 50)
    private String password;
    
    @Column(name = "enabled")
    private boolean enabled;
    
    @OneToOne(fetch = FetchType.EAGER)  
    @JoinTable(name = "user_roles",  
        joinColumns        = {@JoinColumn(name = "user_id", referencedColumnName = "id")},  
        inverseJoinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")}  
    )  
    private Role role;
    
    public String getUsername() {
        return username;
    }

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

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public boolean getEnabled() {
        return enabled;
    }

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

    public Role getRole() {
        return role;
    }

    public void setRole(Role role) {
        this.role = role;
    }

    @Override
    public String toString() {
        return String.format("%s(id=%d, username=%s, password=%s, enabled=%b)", 
                this.getClass().getSimpleName(), 
                this.getId(), 
                this.getUsername(), 
                this.getPassword(), 
                this.getEnabled());
    }

    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (o == null)
            return false;

        if (o instanceof User) {
            final User other = (User) o;
            return Objects.equal(getId(), other.getId())
                    && Objects.equal(getUsername(), other.getUsername())
                    && Objects.equal(getPassword(), other.getPassword())
                    && Objects.equal(getEnabled(), other.getEnabled());
        }
        return false;
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(getId(), getUsername(), getPassword(), getEnabled());
    }

    @Transient
    public Set<Permission> getPermissions() {
        Set<Permission> perms = new HashSet<Permission>();
        perms.addAll(role.getPermissions()); 
        return perms;
    }

    @Override
    @Transient
    public Collection<GrantedAuthority> getAuthorities() {
        Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
        authorities.add(getRole());
        authorities.addAll(getPermissions());
        return authorities;
    }

    @Override
    public boolean isAccountNonExpired() {
        //return true = account is valid / not expired
        return true; 
    }

    @Override
    public boolean isAccountNonLocked() {
        //return true = account is not locked
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        //return true = password is valid / not expired
        return true;
    }

    @Override
    public boolean isEnabled() {
        return this.getEnabled();
    }
}

3. At the DAO layer there are some changes to the update method. We want to allow a username to be changed in the application, but we need to make sure that usernames are not duplicated. In order to accommodate this functionality at the DAO layer, the update method needs to be changed. This is also true of our other DAOs. The modified method signature is highlighted in both the interface and the implementation below.
package com.dtr.oas.dao;
import java.util.List;
import com.dtr.oas.exception.DuplicateUserException;
import com.dtr.oas.exception.UserNotFoundException;
import com.dtr.oas.model.User;

public interface UserDAO {

    public void addUser(User user) throws DuplicateUserException;

    public User getUser(int userId) throws UserNotFoundException;
    
    public User getUser(String username) throws UserNotFoundException;

    public void updateUser(User user) throws UserNotFoundException, DuplicateUserException;

    public void deleteUser(int userId) throws UserNotFoundException;

    public List<User> getUsers();

}
package com.dtr.oas.dao;
import java.util.List;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import com.dtr.oas.exception.DuplicateUserException;
import com.dtr.oas.exception.UserNotFoundException;
import com.dtr.oas.model.User;

@Repository
public class UserDAOImpl implements UserDAO {
    static Logger logger = LoggerFactory.getLogger(UserDAOImpl.class);

    @Autowired
    private SessionFactory sessionFactory;

    private Session getCurrentSession() {
        return sessionFactory.getCurrentSession();
    }

    @Override
    public void addUser(User user) throws DuplicateUserException {
        logger.debug("UserDAOImpl.addUser() - [" + user.getUsername() + "]");
        try {
            User userCheck = getUser(user.getUsername());
            String message = "The user [" + userCheck.getUsername() + "] already exists";
            throw new DuplicateUserException(message);
        } catch (UserNotFoundException e) { 
            getCurrentSession().save(user);
        }
    }

    @Override
    public User getUser(int userId) throws UserNotFoundException {
        logger.debug("UserDAOImpl.getUser() - [" + userId + "]");
        User userObject = (User) getCurrentSession().get(User.class, userId);
        
        if (userObject == null) {
            throw new UserNotFoundException("User id [" + userId + "] not found");
        } else {
            return userObject;
        }
    }

    @SuppressWarnings("unchecked")
    @Override
    public User getUser(String usersName) throws UserNotFoundException {
        logger.debug("UserDAOImpl.getUser() - [" + usersName + "]");
        Query query = getCurrentSession().createQuery("from User where username = :usersName ");
        query.setString("usersName", usersName);
        
        logger.debug(query.toString());
        if (query.list().size() == 0 ) {
            throw new UserNotFoundException("User [" + usersName + "] not found");
        } else {
            logger.debug("User List Size: " + query.list().size());
            List<User> list = (List<User>)query.list();
            User userObject = (User) list.get(0);

            return userObject;
        }
    }

    @Override
    public void updateUser(User user) throws UserNotFoundException, DuplicateUserException {
        User userCheck = getUser(user.getId());
        
        if (userCheck.getId() == user.getId()) {
            userCheck.setEnabled(user.getEnabled());
            userCheck.setPassword(user.getPassword());
            userCheck.setUsername(user.getUsername());
            userCheck.setRole(user.getRole());
            getCurrentSession().update(userCheck);
        } else {
            String message = "The user [" + userCheck.getUsername() + "] already exists";
            throw new DuplicateUserException(message);
        }
    }

    @Override
    public void deleteUser(int userId) throws UserNotFoundException {
        User user = getUser(userId);
        if (user != null) {
            getCurrentSession().delete(user);
        }
    }

    @Override
    @SuppressWarnings("unchecked")
    public List<User> getUsers() {
        return getCurrentSession().createQuery("from User").list();
    }
}
You will notice that in the update method, I am getting a User instance (userCheck) from the current hibernate session and performing not null and id checks. I then assign the values from the method argument (user) to this new instance (userCheck), prior to calling update with the userCheck object. Until I performed this mapping and used this update approach, I received the hibernate error: "A different object with the same identifier value was already associated with the session"


4. The service layer classes require method signature modifications for the update methods. The updated interface and implementation classes are shown below, with the changes highlighted.
package com.dtr.oas.service;
import java.util.List;
import org.springframework.security.core.userdetails.UserDetailsService;
import com.dtr.oas.exception.DuplicateUserException;
import com.dtr.oas.exception.UserNotFoundException;
import com.dtr.oas.model.User;

public interface UserService extends UserDetailsService {

    public void addUser(User user) throws DuplicateUserException;

    public User getUser(int userId) throws UserNotFoundException;

    public User getUser(String username) throws UserNotFoundException;

    public void updateUser(User user) throws UserNotFoundException, DuplicateUserException;

    public void deleteUser(int userId) throws UserNotFoundException;

    public List<User> getUsers();
}
package com.dtr.oas.service;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.dtr.oas.dao.UserDAO;
import com.dtr.oas.exception.DuplicateUserException;
import com.dtr.oas.exception.UserNotFoundException;
import com.dtr.oas.model.User;

@Service
@Transactional
public class UserServiceImpl implements UserService {
    static Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);
    
    @Autowired
    private UserDAO userDAO;

    @Override
    public void addUser(User user) throws DuplicateUserException {
        userDAO.addUser(user);
    }

    @Override
    public User getUser(int userId) throws UserNotFoundException {
        return userDAO.getUser(userId);
    }

    @Override
    public User getUser(String username) throws UserNotFoundException {
        return userDAO.getUser(username);
    }

    @Override
    public void updateUser(User user) throws UserNotFoundException, DuplicateUserException {
        userDAO.updateUser(user);
    }

    @Override
    public void deleteUser(int userId) throws UserNotFoundException {
        userDAO.deleteUser(userId);
    }

    @Override
    public List<User> getUsers() {
        return userDAO.getUsers();
    }
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        try {
            return getUser(username);
        } catch (UserNotFoundException e) {
            throw new UsernameNotFoundException(e.getMessage());
        }
    }
}

In the next post we will create a controller class, a DTO for the User entity, and create the Thymeleaf pages (with some Bootstrap updates as well).

1 comment:

Senhor Wilson said...

Great working so far! I learn a lot :) Keep going!

Post a Comment