0

I'm trying to implement P/R/G (POST/Redirect/GET) pattern in my Spring MVC application in order to avoid duplicate form submissions, only instead of showing some success view (GET), I'm redirecting to another URL (redirect:/essays/main/student/{studentId}/activity/add/existing) for which I need to pass the complete model too. Spring docs for org.springframework.web.servlet.view.RedirectView says:

"View that redirects to an absolute, context relative, or current request relative URL, exposing all model attributes as HTTP query parameters."

so I can retrieve serialized objects from the request and it works fine with Strings, but it does not work if I want to pass more complex objects, like in my case, couple of lists of objects (activityList, courseList, teacherList).

This is how it's suppose to work: First I show my searchActivity view and this works fine:

@RequestMapping(value="/{studentId}/activity/search", method = RequestMethod.GET)
public String getSearchActivity(@PathVariable Integer studentId, Model model) {

    StudentActivityDTO studentActivityDTO = new StudentActivityDTO();
    Student student = studentService.get(studentId);
    studentActivityDTO.setStudent(student);
    model.addAttribute("studentActivityDTO", studentActivityDTO);

    return "searchActivity";
}

This is the important part of my searchActivity view:

<c:url var="studentUrl" value="/essays/main/student/${studentActivityDTO.student.studentId}/activity/search" />
<form:form modelAttribute="studentActivityDTO" id="myForm" method="POST" action="${studentUrl}">
...
<label for="activityDescription">Eneter search string:</label>
<input type="text" id="activityDescription" name="activityDescription">
<input type="submit" value="Submit" id="submit"/>
...
</form:form>

Then I enter a searching string (activityDescription) and submit my form to do the actual searching (POST), which also works fine, except for the last line of code (the redirecting part):

@RequestMapping(value="/{studentId}/activity/search", method = RequestMethod.POST)
public String postSearchActivity(@PathVariable Integer studentId,
        @RequestParam(value="activityDescription") String activityDescription,
        @ModelAttribute("studentActivityDTO") StudentActivityDTO studentActivityDTO,
        Model model) {

    List<Activity> activityList = activityService.search(activityDescription);
    model.addAttribute("activityList", activityList);

    Student student = studentService.get(studentId);
    studentActivityDTO.setStudent(student);
    model.addAttribute("studentActivityDTO", studentActivityDTO);
    model.addAttribute("activityDescription", activityDescription);
    model.addAttribute("courseList", courseService.getAll());
    model.addAttribute("teacherList", teacherService.getAll());

    return "redirect:/essays/main/student/{studentId}/activity/add/existing";
    // return "addExistingActivity"; <-- If I use this it works fine!
}

Now I need to pass model to some controller GET method:

@RequestMapping(value="/{studentId}/activity/add/existing", method = RequestMethod.GET)
public String getAddExistingActivity(@PathVariable Integer studentId, Model model) {
    // some stuff
    return "addExistingActivity";
}

The important part of addExistingActivity view:

<c:url var="studentUrl" value="/essays/main/student/${studentActivityDTO.student.studentId}/activity/add/existing" />
<form:form modelAttribute="studentActivityDTO" id="myForm" method="POST" action="${studentUrl}">
...
<label for="activityId">Activity Id:</label>
<input type="text" id="activityId" name="activityId">
<input type="submit" id="submit" value="Submit"/>

<c:if test="${!empty activityList}">
...
</c:if>
<c:if test="${empty activityList}">
    <div style="color: #ff0000">No results!</div>
</c:if>
...
</form:form>

But my lists (activityList, courseList, teacherList) are not present and I always get message "No results!". I only get this in my stack trace:

[DEBUG] [http-bio-8080-exec-7 08:32:44] (AbstractView.java:exposeModelAsRequestAttributes:373) Added model object 'studentId' of type [java.lang.Integer] to request in view with name 'addExistingActivity'
[DEBUG] [http-bio-8080-exec-7 08:32:44] (AbstractView.java:exposeModelAsRequestAttributes:373) Added model object 'studentActivityDTO' of type [rs.ac.uns.tfzr.zpupin.dto.StudentActivityDTO] to request in view with name 'addExistingActivity'
[DEBUG] [http-bio-8080-exec-7 08:32:44] (AbstractView.java:exposeModelAsRequestAttributes:373) Added model object 'org.springframework.validation.BindingResult.studentActivityDTO' of type [org.springframework.validation.BeanPropertyBindingResult] to request in view with name 'addExistingActivity'

But if I use return "addExistingActivity" instead of redirect:... everything works fine and I have this in my stack trace:

[DEBUG] [http-bio-8080-exec-6 07:42:25] (AbstractView.java:exposeModelAsRequestAttributes:373) Added model object 'studentId' of type [java.lang.Integer] to request in view with name 'addExistingActivity'
[DEBUG] [http-bio-8080-exec-6 07:42:25] (AbstractView.java:exposeModelAsRequestAttributes:373) Added model object 'studentActivityDTO' of type [rs.ac.uns.tfzr.zpupin.dto.StudentActivityDTO] to request in view with name 'addExistingActivity'
[DEBUG] [http-bio-8080-exec-6 07:42:25] (AbstractView.java:exposeModelAsRequestAttributes:373) Added model object 'org.springframework.validation.BindingResult.studentActivityDTO' of type [org.springframework.validation.BeanPropertyBindingResult] to request in view with name 'addExistingActivity'
[DEBUG] [http-bio-8080-exec-6 07:42:25] (AbstractView.java:exposeModelAsRequestAttributes:373) Added model object 'activityList' of type [java.util.Collections$CheckedRandomAccessList] to request in view with name 'addExistingActivity'
[DEBUG] [http-bio-8080-exec-6 07:42:25] (AbstractView.java:exposeModelAsRequestAttributes:373) Added model object 'activityDescription' of type [java.lang.String] to request in view with name 'addExistingActivity'
[DEBUG] [http-bio-8080-exec-6 07:42:25] (AbstractView.java:exposeModelAsRequestAttributes:373) Added model object 'courseList' of type [java.util.Collections$CheckedRandomAccessList] to request in view with name 'addExistingActivity'
[DEBUG] [http-bio-8080-exec-6 07:42:25] (AbstractView.java:exposeModelAsRequestAttributes:373) Added model object 'teacherList' of type [java.util.Collections$CheckedRandomAccessList] to request in view with name 'addExistingActivity'

All lists present and accounted for!

What kind of custom implementation would I need in order for this to work? Any help will be much appreciated!

Also, I know I can use flash attributes for this, but won't they disappear after hitting F5?

just_a_girl
  • 643
  • 4
  • 13
  • 28
  • If I remember correctly, you have to add any model attributes as a "flash attribute" in Spring MVC to pass model attributes between controllers using a redirect. This may help you: http://stackoverflow.com/questions/7429649/how-to-pass-model-attributes-from-one-spring-mvc-controller-to-another-controlle – CodeChimp Jan 30 '14 at 21:03
  • @CodeChimp yes, but after hitting F5 on the page, flash attributes are gone, no? I wouldn't want that to happen... – just_a_girl Jan 30 '14 at 21:25
  • If you are redirecting to another controller or app server that doesn't share session data with the one doing the redirect, there really isn't any other way outside of putting the data somewhere "in common". For instance, you could modify the recipient of the redirect to look for some ID, write the data to a fileshare/database with that ID, then redirect passing in the ID. Now the other end knows where to look. This would required the ability to modify the code at both ends, though. – CodeChimp Feb 03 '14 at 15:51
  • @CodeChimp I'm redirecting to the same controller class. I guess I can use `@SessionAttributes`, but then I don't need flash attributes at all... – just_a_girl Feb 03 '14 at 16:19
  • Yes, if your redirect is to the same controller, then `@SessionAttributes` is the way to go. My assumption was that you were redirecting to a separate location. Out of curiosity, why use a redirect at all if it's the same controller? – CodeChimp Feb 03 '14 at 17:55
  • @CodeChimp because of implementing the P/R/G pattern, or I'm having a wrong understanding of the pattern? It's a POST method, unless I redirect, on refresh, form gets submitted again. Please tell me if I'm wrong, I just begun learning this and any advice will be much appreciated. – just_a_girl Feb 03 '14 at 18:05
  • It sounds like you are doing P/R/G just fine, but I am not sure where the F5 refresh comes in. One of the known deficits in P/R/G is that it doesn't handle the issue where the user refreshes before submission. – CodeChimp Feb 03 '14 at 18:41
  • @CodeChimp not before submission, but after, when the G part of P/R/G happens (redirected page loads), then when you hit F5, everything that came as flash attribute is lost. – just_a_girl Feb 03 '14 at 18:55

1 Answers1

1

RedirectView exposes all primitive model attributes or collections containing primitives as HTTP query parameters by default. This is why, when you add a String as a model attribute it is exposed and when you add an object - it is not. In fact, this is what current RedirectView documentation says (Spring 4.0.1):

...By default all primitive model attributes (or collections thereof) are exposed as HTTP query parameters (assuming they've not been used as URI template variables)...

So in fact the following method:

@RequestMapping(value = "post", method = RequestMethod.POST)
public String post(@ModelAttribute PrgForm form, Model model) {
    model.addAttribute("testString", "Some string");
    model.addAttribute("testCollection", Lists.newArrayList("Element 1", "Element 2"));
    model.addAttribute("testObject", form);
    return "redirect:/demo/get";
}

Will result in a redirect to: demo/get?testString=Some+string&testCollection=Element+1&testCollection=Element+2

As you can see, testObject is not in the query parameters.

If you look closely at RedirectView source code you will find that there is a method isEligibleProperty(String, Object) that determines whether the given model element should be exposed as a query property.

Behavior of this method may be changed. So in fact, you could implement your own RedirectView as follows:

private class CustomRedirectView extends RedirectView {

    public CustomRedirectView(String url) {
        super(url);
    }

    @Override
    protected boolean isEligibleProperty(String key, Object value) {
        if ("testObject".equals(key)) {
            return true;
        } else {
            return super.isEligibleProperty(key, value);
        }
    }

    @Override
    protected void appendQueryProperties(StringBuilder targetUrl, Map<String, Object> model, String encodingScheme) throws UnsupportedEncodingException {
        // do some stuff
    }
}

And return it from @Controller's method:

@RequestMapping(value = "post", method = RequestMethod.POST)
public View post(@ModelAttribute PrgForm form, Model model) {
    model.addAttribute("testString", "Some string");
    model.addAttribute("testCollection", Lists.newArrayList("Element 1", "Element 2"));
    model.addAttribute("testObject", form);
    return new CustomRedirectView("/demo/get");
}

I have never done this kind of implementation before, so I am not sure how complex it can be to fully implement the scenario you need to support. I think it would be better to utilize flash attributes though.

I hope it helps.

Rafal Borowiec
  • 5,124
  • 1
  • 24
  • 20