Add spring form checkbox selections to a collection and persist it

2.5k views Asked by At

I'm currently working on a simple SpringMVC application. I've recently started to use Spring-security. The users and their respective roles are kept in the database. My current task is to implement a "register user" form. I'm using a simple .jsp and spring forms. The users can have 2 roles : ROLE_USER and ROLE_ADMIN, and ROLE_USER is checked by default.

Here are my AppUser and AppUserRoles models.

AppUser.java :

import java.util.HashSet;
import java.util.Set;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.validation.constraints.Size;

import org.hibernate.validator.constraints.NotEmpty;

@Entity
@Table(name="appusers")
public class AppUser {

    @Id
    @Column(name="USERNAME", unique = true, nullable = false, length = 45)
    @NotEmpty(message="Username field cannot be empty")
    private String username;

    @Column(name="PASSWORD", nullable = false, length = 60)
    @NotEmpty(message="Password field cannot be empty")
    @Size(min=6,max=10,message="Password must be between 6 and 10 letters")
    private String password;

    @NotEmpty(message="The user must have at least one defined role")
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "appUser")
    private Set<AppUserRole> userRole = new HashSet<AppUserRole>(0);

    public AppUser() {
        super();
    }


    public AppUser(String username, String password) {
        super();
        this.username = username;
        this.password = password;
    }


    public AppUser(String username, String password, Set<AppUserRole> userRole) {
        super();
        this.username = username;
        this.password = password;
        this.userRole = userRole;
    }

    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 Set<AppUserRole> getUserRole() {
        return userRole;
    }


    public void setUserRole(Set<AppUserRole> userRole) {
        this.userRole = userRole;
    }

}

AppUserRole.java

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;


@Entity
@Table(name="appuser_roles")
public class AppUserRole {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    @Column(name = "USER_ROLE_ID", unique = true, nullable = false)
    private Integer userRoleId;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "username", nullable = false)
    private AppUser appUser;

    @Column(name = "ROLE", nullable = false, length = 45)
    private String role;

    public AppUserRole() {
        super();
    }

    public AppUserRole(AppUser appuser, String role) {
        super();
        this.appUser = appuser;
        this.role = role;
    }

    public Integer getUserRoleId() {
        return userRoleId;
    }
    public void setUserRoleId(Integer userRoleId) {
        this.userRoleId = userRoleId;
    }
    public AppUser getAppuser() {
        return appUser;
    }
    public void setAppuser(AppUser appuser) {
        this.appUser = appuser;
    }
    public String getRole() {
        return role;
    }
    public void setRole(String role) {
        this.role = role;
    }

}

AppUserDAOImpl.java fragment containing the registerUser() method:

@Override
public void registerUser(AppUser appUser) {

    sessionFactory.getCurrentSession().persist(appUser);
    logger.info(appUser.getUsername() + " persisted");
}

UserController.java fragment related to the register form:

@RequestMapping(value="/register")
public String goRegister(Model model) {
    model.addAttribute("AppUser", new AppUser());
    return "register";
}

@RequestMapping(value= "/registeruser_action", method = RequestMethod.POST)
public String addUser(@ModelAttribute("AppUser") @Valid AppUser appUser, BindingResult result){

    if(result.hasErrors()){
        return "register";
    }

    appUserDetailsService.registerUser(appUser);
    return "redirect:/menu";    
}

register.jsp fragment with the actual form:

    <sf:form commandName="AppUser" action="registeruser_action" method="POST" >

        <div>
            Username:<br>
            <sf:input type="text" path="username"/>
            <sf:errors path="username" cssClass="errors"/>
        </div>
        <div>
            Password<br>
            <sf:input type="text" path="password"/>
            <sf:errors path="password" cssClass="errors"/>
        </div>
        <div>
            User role:<br>
            <sf:checkbox path="userRole" value="ROLE_USER" 
                disabled="true" checked="true" /> User <br>
            <sf:checkbox path="userRole" value="ROLE_ADMIN"/> Admin <br>
            <sf:errors path="userRole" cssClass="errors"/>
        </div>
        <br>

        <input type="submit" value="Submit">

    </sf:form>

Specifically, what I want do and can't seem to figure out or find on the internet : I want the checkbox selections to be added to the userRole set in my AppUser.java model, and then to persist them. The thing is, I've mapped form inputs to object fields before in my project, but never to collection. The optimist in me was pretty much hoping that I could do it just by mapping the checkboxes to the userRole path in the spring form, and maybe java and spring would "automagically" understand that it should add them to that collection. The realist in me kinda knows that it's not all that simple. Honestly, I know it's pretty trivial, but I just can't seem to find a solution. Hoping to find that solution here.

PS: I've only posted what I deemed to be important to this question. The service layer, for example, seemed to be extra. Will post extra information/code as required.

EDIT: Ok, I added ROLE_USER in the constructor as per akshay's suggestion. The resulting exception when trying to register a new user made me realize that I'm going about this entirely wrong. I'm trying to assign a String to a Set object. userRole is of type Set, and ROLE_USER is a string. The set contains AppUserRole objects which should contain the username and the respective role string. Basically, the question now is : how can I add a new object containing the username and the respective role straight from the spring form and into the userRole set ?

1

There are 1 answers

9
Alan Hay On BEST ANSWER

Okay, there a few things you need to do here.

Let's assume you want to present a list of all available roles: the user checks those he wants allocated to the current user.

Currently Role is a simple String so there is nothing to bind the checkbox to. You could then create a wrapper on the view tier:

public class RoleWrapper{

private String roleName;
private boolean selected;

}

On your controller you want to add a ModelAttribute that will load all available roles. Note that you cannot bind to a raw collection so this list needs to be wrapped. So create another class to wrap your list:

public class RoleList{

private List<RoleWrapper> roles;

}

Construct an instance in your controller and set as a model attribute:

@ModelAttribute("availableRoles")
public RoleList getAvailableRoles(){
//load from db and construct list

}

In you JSP create a form backed by this list and with the checkboxes bound to the boolean field:

<form:form modelAttribute="roleList" method="post">
    <table>
        <thead>
            <tr>
                <th style="width: 80%;">Role</th>
                <th>Select</th>
            </tr>
        </thead>
        <tbody>
            <c:forEach items="${roleList.roles}"
                var="role" varStatus="status">
                <tr>
                    <td>role.name</td>
                    <td><form:checkbox
                            path="roles[${status.index}].selected" /></td>
                </tr>
            </c:forEach>
        </tbody>
    </table>
</form>

Update your controller to be passed this List on submit. Iterate and set the User Roles accordingly:

@RequestMapping(value= "/registeruser_action", method = RequestMethod.POST)
public String addUser(@ModelAttribute("AppUser") @Valid AppUser appUser, @ModelAttribute("roleList") RoleList roleList, BindingResult result){

    if(result.hasErrors()){
        return "register";
    }

    for(RoleWrapper role : roleList.getRoles()){
        if(role.isSelected()){
            //add to user
        }
    }

    appUserDetailsService.registerUser(appUser);


    return "redirect:/menu";    
}

Regarding the issue you have persisting you will need to do the following. Add cascade options to the relationship and, if persisting the non-owning side (AppUser in this case) ensure both sides of the relationship are set. You should encapsulate add/remove operations to ensure relationships between domain objects are consistent. Thus:

@Entity
@Table(name="appusers")
public class AppUser {

    @NotEmpty(message="The user must have at least one defined role")
    @OneToMany(fetch = FetchType.LAZY, cascade = cascadeType.ALL, mappedBy = "appUser")
    private Set<AppUserRole> userRoles = new HashSet<AppUserRole>(0);

    /**
    *   Force clients through add/remove. Plus, do not provide setter.
    */
    public Set<AppUserRole> getUserRoles() {
        return Collections.unmodifiableSet(userRoles);
    }

    public void addRole(AppUserRole userRole){
        userRole.setAppUser(this); //both sides set
        userRoles.add(userRole);
    }

    public void removeRole(AppUserRole userRole){
        userRoles.remove(userRole);
    }
}