0

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

Community
  • 1
  • 1
Charles Morin
  • 1,449
  • 4
  • 32
  • 50
  • It is related to your browser and has to do with caching. What I strongly suggest is using the post-redirect-get pattern. After the update do a redirect to the success page instead of rendering it. So instead of returning `user/success` return `redirect:/url/to/success.jsp-page`. The controller for the success page should retrieve the user from the database again (instead of adding it to the model), if you really don't want to put the user in flash scope that way it will survive the redirect. – M. Deinum Jun 23 '15 at 05:48
  • I used the flash attribute first, but if the user refreshes the success page for any reason, the content is being cleared, so that's not an option. Will try to implement the post-redirect-get pattern, and then reload the element from database like you have suggested. Thank you. – Charles Morin Jun 23 '15 at 11:14
  • Finally, I doubt it's related to caching. I have added a breakpoint in method "validateUpdate", in order to see right off the bat the submitted values. Even at this point, the value remained the same for the region. If it worked but not being displayed properly, it would stick to your caching idea... but it's not even being submitted properly... – Charles Morin Jun 23 '15 at 11:24
  • If it isn't updating you must have something wrong in your binding or the mapping in your UserForm (for which I would suggest a mapping framework instead of manual labor or simply use the User directly). – M. Deinum Jun 23 '15 at 12:12
  • Can you please elaborate about the "mapping framework"? Thank you for help. I have reviewed the entire log and I don't see anything from Spring that looks abnormal. I'm under the impression it's related to a filter, or maybe even worst... my server (pre-authentication).. But then again, I don't see any trace about that in the logs... Server is in debug mode. – Charles Morin Jun 23 '15 at 12:18
  • Something like [Dozer](http://dozer.sourceforge.net) or [MapStruct](http://mapstruct.org) (I would opt for the latter). – M. Deinum Jun 23 '15 at 12:20

1 Answers1

1
<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

praveenj
  • 349
  • 1
  • 2
  • 15
  • Just tested it and unfortunately the same exact glitch is present. – Charles Morin Jun 29 '15 at 11:55
  • Do you see an issue only with regionId or for all params? – praveenj Jul 01 '15 at 21:21
  • 1
    All params. The modelattribute gets cleared completely. That is caused by an authentication issue (or feature like they say..!) for IE only. See here: http://blogs.msdn.com/b/ieinternals/archive/2010/11/22/internet-explorer-post-bodies-are-zero-bytes-in-length-when-authentication-challenges-are-expected.aspx?CommentPosted=true#commentmessage%20Challenge-Response%20Authentication%20and%20Zero-Length%20Posts – Charles Morin Jul 02 '15 at 11:37