Spring MVC and redirecting (P/R/G )

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?

Answers


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.


Need Your Help

cygwin path usage. I just can't figure this out

python bash path cygwin

I'm using cygwin and trying to run a python script. When I'm in the script's folder it runs fine however when I try to run it using the whole path it doesn't work. I get the following error No suc...