Strange behavior : Spring Session Attributes vs ModelAttribute

965 views Asked by At

I got a very strange issue with Spring MVC 4.0.6, hosted on JBoss 7.2. When updating an existing user, the submitted information sometimes get transferred to the POST RequestMapping controller method (validateUpdate method below).

UserController.java

@Controller
@RequestMapping(value = "/security/user")
@SessionAttributes(value = {"userForm", "user"})
public class UserController {

    private final String CREATE_VIEW = "user/createOrUpdate";
    private final String READ_VIEW = "user/readOrDelete";
    private final String UPDATE_VIEW = CREATE_VIEW;
    private final String DELETE_VIEW = READ_VIEW;    

    @Autowired
    private LocationService locationService;

    @Autowired
    private SecurityService securityService;

    @Autowired
    private UserValidator userValidator;

    @InitBinder("userForm")
    protected void initBinder(WebDataBinder binder) {
        binder.setValidator(userValidator);
        binder.setDisallowedFields("strId");
    }

    @ModelAttribute("regions")
    public List<Region> populateRegions() {

        Locale locale = LocaleContextHolder.getLocale();
        List<Region> regions = this.locationService.lstRegions(locale.getLanguage());

        return regions;
    }

    @RequestMapping(value = "/update/{intUserId}", method = RequestMethod.GET)
    public String updateUser(@PathVariable Integer intUserId, Model model) {

        User user = this.securityService.findUserByPk(intUserId);

        if (user != null) {
            UserForm userForm = new UserForm();
            userForm.setUser(user);

            model.addAttribute(userForm);
        }

        return UPDATE_VIEW;
    }

    @RequestMapping(value = "/update/validate",  method = RequestMethod.POST)
    public String validateUpdate(@Valid @ModelAttribute("userForm") UserForm userForm,
                                BindingResult result,
                                Model model,
                                RedirectAttributes redirectAttributes,
                                SessionStatus status) {

        return this.performCreateOrUpdateOperation(userForm, result, model, redirectAttributes, status);
    }

    private String performCreateOrUpdateOperation(
        UserForm userForm, 
        BindingResult result,
        Model model,
        SessionStatus status) {

        if(result.hasErrors()) {
            return UPDATE_VIEW;
        } else {

            User user = userForm.getUser();

            this.securityService.validateCreateOrUpdateUser(result, user);

            if (result.hasErrors() == false) {

                if (userForm.isNew()) {
                    this.securityService.addUser(user);
                } else {
                    this.securityService.updateUser(user);
                }

                model.addAttribute(user);

                status.setComplete();

                return "user/success";

            } else {
                return UPDATE_VIEW;
            }
        }
    }
}

Form Bean

public class UserForm {

    private String strUserIdToSearch = "";

    private String strId = "0";
    private String strUserId = "";
    private String strFirstName = "";
    private String strLastName = "";
    private String strEmail = "";

    private String strRegionId = "0";
    private boolean booArchived = false;

    public User getUser() {

        User user = new User();

        user.setIntId(Integer.valueOf(this.strId));
        user.setStrUserId(this.strUserId);
        user.setStrFirstName(this.strFirstName);
        user.setStrLastName(this.strLastName);
        user.setStrEmail(this.strEmail);

        Region region = new Region(Integer.valueOf(this.strRegionId));
        user.setRegion(region);

        user.setBooArchived(this.booArchived);

        return user;
    }

    public void setUser(User user) {

        this.strUserIdToSearch = user.getStrUserId();

        this.strId = String.valueOf(user.getIntId());
        this.strUserId = user.getStrUserId();
        this.strFirstName = user.getStrFirstName();
        this.strLastName = user.getStrLastName();
        this.strEmail = user.getStrEmail();

        this.strRegionId = String.valueOf(user.getRegion().getIntId());
        this.booArchived = user.getBooArchived();
    }

    ...getters and setters...
}

JSP (removed styling for clarity) crudMethod is a JSP tag returning "create", "read", "update" or "delete" depending on ${requestScope['javax.servlet.forward.request_uri']}

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

    <spring:url var="userFormAction" value="/rest/security/user/${crudMethod}/validate" />    

<form:form id="userForm" modelAttribute="userForm" action="${userFormAction}" method="POST">

<form:hidden path="strId" />
<form:hidden path="strUserId" id="strUserId" />
<form:hidden path="strLastName" id="strLastName" />
<form:hidden path="strFirstName" id="strFirstName" />
<form:hidden path="strEmail" id="strEmail" />

<form:select id="strRegionId" path="strRegionId">
    <form:option value="0" label="${strSelectRegionLabel}" />
    <form:options items="${regions}" itemValue="intId" itemLabel="${strRegionLabel}" />
</form:select>

</form:form>

So, when I submit the form, and for instance, change the region to another ID in the list (let's say from 1 to 6). Sometimes it works, sometimes not. By "works", I mean I hit the success page, then I go back to see the user again. Sometimes it stays at 1, sometimes it's changed to 6.

I have found a pattern/workaround that works all the time to reproduce the issue:

  1. Load the update form (UserController > updateUser)
  2. Change the region from 1 to 6
  3. Click Save. Form submits so the UserController.validateUser method is being invoked.
  4. Got the success page. On that success page I got a link to the read operation for the user. Clicking that link, I realize that the region did not change (the main problem). There is a link to update a user on the read page.
  5. Re-do the exact same change that I did at step #2.
  6. Click Save. Form submits and I got the success page view.
  7. Click the read hyperlink again, and now I see that the change worked.

Question is: WHY? Am I missing something??

Note: It's not related to the business layer. I have tested it and it's stable. That is certainly related to my use of the Spring MVC Framework.

Browser is IE11.

Thank you for help.

* UPDATE 2015-06-29 *

After a few more searches, I found that:

  • when it does NOT work, the request Content-Length header value is 0.
  • when it works, there is a value (eg: 146).
  • the request message body is always correct, like so:

    strId=THE_ID&strUserId=THE_USERID&strLastName=THE_LASTNAME&strFirstName=THE_FIRSTNAME&strEmail=THE_EMAILADDRESS&strRegionId=THE_REGIONID&booArchived=false

    Please note that "THE_REGIONID" is good every single time.

Related resources

1

There are 1 answers

3
praveenj On
<form:select id="intRegionId" path="strRegionId">

I think the id in the form:select is the culprit . It doesn't match the attribute name in UserForm, which is "strRegionId". And therefore it doesn't bind. Change the id value to strRegionId