PDA

View Full Version : Redirect after Post


laenzlinger
Mar 20th, 2006, 09:55 AM
First of all I would like to thank all the people who helped to develop such a great framework as Spring is! We were using Spring with great success in several projects!

So far we have used the Struts in the web layer but we are investigating using Spring MVC (and Spring Web Flow) instead.

One requirement we face is the "Redirect after Post" pattern. The SimpleFormController does allow that pattern on submit (e.g. by using a redirect: view).
Is there also a proper way of redirecting back to the from on error (binding- or validation-error)?

I was thinking of a RedirectFormController (see below) that implements a similar workflow than the SimpleFormController (by extending the AbstractFormController). On error it persists the errors in the session and then redirects back to the form. Before the form is presentet, the stored errors are retrieved from the session again.

Is there a better solution for this or should one use Spring Web Flow for such pageflows?

Thanks for any suggestions




package example;

import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.validation.BindException;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.AbstractFormCo ntroller;

public class RedirectingFormController extends AbstractFormController {

// -------------------------------------------------------------------------
// PUBLIC CONSTANTS
// -------------------------------------------------------------------------

// -------------------------------------------------------------------------
// PROTECTED AND PRIVATE VARIABLES AND CONSTANTS
// -------------------------------------------------------------------------
protected static Log log = LogFactory
.getLog(RedirectingFormController.class);

private String formRedirect;
private String formView;
private String successView;

// -------------------------------------------------------------------------
// CONSTRUCTORS
// -------------------------------------------------------------------------

// -------------------------------------------------------------------------
// PUBLIC METHODS
// -------------------------------------------------------------------------

// -------------------------------------------------------------------------
// PROTECTED METHODS
// -------------------------------------------------------------------------
protected ModelAndView showForm(HttpServletRequest request,
HttpServletResponse response, BindException errors)
throws Exception {

return showForm(request, response, errors, null);
}

protected ModelAndView showForm(HttpServletRequest request,
HttpServletResponse response, BindException errors, Map controlModel)
throws Exception {

log.debug("showForm");
String errorsAttrName = getErrorsSessionAttributeName(request);
BindException sessionErrors = (BindException) request.getSession()
.getAttribute(errorsAttrName);
if (sessionErrors != null) {
if (log.isDebugEnabled()) {
log.debug("redirected error request");
}
errors.addAllErrors(sessionErrors);
request.getSession().removeAttribute(errorsAttrNam e);
}

return showForm(request, errors, getFormView(), controlModel);
}

protected ModelAndView processFormSubmission(HttpServletRequest request,
HttpServletResponse response, Object command, BindException errors)
throws Exception {
if (errors.hasErrors()) {
if (log.isDebugEnabled()) {
log.debug("Data binding errors: " + errors.getErrorCount());
}
return redirectForm(request, response, command, errors);
} else {
log.debug("No errors -> processing submit");
return onSubmit(request, response, command, errors);
}
}

// redirect view -----------------------------------------------------------
protected ModelAndView redirectForm(HttpServletRequest request,
HttpServletResponse response, Object command, BindException errors)
throws Exception {

ModelAndView mv = new ModelAndView(getFormRedirect());

// store the form into the session
if (isSessionForm()) {
String formAttrName = getFormSessionAttributeName(request);
if (log.isDebugEnabled()) {
log.debug("Setting form session attribute [" + formAttrName
+ "] to: " + errors.getTarget());
}
request.getSession().setAttribute(formAttrName, errors.getTarget());
}

// store the errors into the session
String errorsAttrName = getErrorsSessionAttributeName(request);
if (log.isDebugEnabled()) {
log.debug("Setting errors session attribute [" + errorsAttrName
+ "] to: " + errors);
}
request.getSession().setAttribute(errorsAttrName, errors);

if (log.isDebugEnabled()) {
log.debug("send redirect to [" + getFormRedirect() + "]");
}

return mv;
}

protected Object formBackingObject(HttpServletRequest request)
throws Exception {

// required when presenting the errors on redirect
Object sessionForm = request.getSession().getAttribute(
getFormSessionAttributeName(request));
if (sessionForm != null) {
return sessionForm;
}
return super.formBackingObject(request);
}

protected String getErrorsSessionAttributeName(HttpServletRequest request) {
return getErrorsSessionAttributeName();
}

protected String getErrorsSessionAttributeName() {
return getClass().getName() + ".ERRORS." + getCommandName();
}

// on submit chain ---------------------------------------------------------
protected ModelAndView onSubmit(HttpServletRequest request,
HttpServletResponse response, Object command, BindException errors)
throws Exception {

return onSubmit(command, errors);
}

protected ModelAndView onSubmit(Object command, BindException errors)
throws Exception {
ModelAndView mv = onSubmit(command);
if (mv != null) {
// simplest onSubmit version implemented in custom subclass
return mv;
} else {
// default behavior: render success view
if (getSuccessView() == null) {
throw new ServletException("successView isn't set");
}
return new ModelAndView(getSuccessView(), errors.getModel());
}
}

protected ModelAndView onSubmit(Object command) throws Exception {
doSubmitAction(command);
return null;
}

protected void doSubmitAction(Object command) throws Exception {
}

// -------------------------------------------------------------------------
// PRIVATE METHODS
// -------------------------------------------------------------------------

// -------------------------------------------------------------------------
// PUBLIC ACCESSORS (GETTERS / SETTERS)
// -------------------------------------------------------------------------
/**
* @return Returns the formView.
*/
public String getFormView() {
return formView;
}
/**
* @param formView The formView to set.
*/
public void setFormView(String formView) {
this.formView = formView;
}
/**
* @return Returns the successView.
*/
public String getSuccessView() {
return successView;
}
/**
* @param successView The successView to set.
*/
public void setSuccessView(String successView) {
this.successView = successView;
}
/**
* @return Returns the formRedirect.
*/
public String getFormRedirect() {
return formRedirect;
}

/**
* @param formRedirect The formRedirect to set.
*/
public void setFormRedirect(String formRedirect) {
this.formRedirect = formRedirect;
}

}



Christof

Proner
Mar 20th, 2006, 11:34 AM
The Class SimpleFormController already implement the redirecting back to form on Binding/Validation error.

See http://static.springframework.org/spring/docs/1.2.x/api/index.html
Workflow section.

Steph

laenzlinger
Mar 20th, 2006, 11:56 AM
Hallo Steph,

Thanks for your reply.

I agree with you that the SimpleFormController already support the workflow of presenting the form page again in case validation errors were detected.

Nevertheless it usually does it by forwarding to the success view.
This means that after posting to the FormController the form is directly rendered again as a result of the POST request.

Now when the User presses the back button (or the reload button) he will be asked if he wants to send the post data again. (Which is perfectly correct, but unfortuntly our customer does not like that). Therefore we usually send back a redirect response (http status code 302) to browser to request the form page again. Of course we need to store the errors (an maybe also the form backing bean) into the session in order to be able to present the errors on the form again.

Please let me know if I missed something or if I am completely wrong

Christof

dgynn
Mar 21st, 2006, 12:01 AM
I believe that if you set the cacheSeconds property on your controller to any number greater than zero you'll be able to navigate back to the post results page. cacheSeconds is a property of AbstractController.

laenzlinger
Mar 21st, 2006, 03:24 AM
Thanks for this suggestion. I have set cacheSeconds to 30 * 60 and as you correctly mentioned now I can navigate back without having the browser pup-up windows asking for re-post.

The next problem I have is now is the following. My form contains dynamic data that is loaded befor the form is displayed. Now if the User navigates back to the form (e.g. since he wants to change the data again after a success) he would still see the previously chached form, wich is also not what I want.

Or would you suggest that the cacheSeconds should only be set after a POST request but not after a GET request to the form?

I still think that the best solution to avoid back-button or reload-button problems is to always Redirect after Post (not only on success, but also on error).

This pattern is described for example here:
http://www.theserverside.com/articles/article.tss?l=RedirectAfterPost

The Redirect after Post pattern is also suggested in the very good new book "Expert Spring MVC and Web Flow" http://www.springframework.org/node/235. Nevertheless it only suggests to use the redirect after a successful post (no errors). IMHO it would also make sense to redirect after errors.

Please let me know if I missed something.

Christof

dgynn
Mar 21st, 2006, 04:20 AM
I'm not sure what the problem is. If the user navigates back, I would think that she would see the data as it was when they navigated forward via the submit. That should be the same for a GET or a POST.

In any case, the Redirect after Post pattern sounds like what you want to do. I'm not sure if WebFlow would solve this.

laenzlinger
Mar 21st, 2006, 03:55 PM
The behviour can be demonstrated for example in the equinox demo application:

http://demo.raibledesigns.com/equinox/editUser.html

1. enter firstname, lastname and an invalid date
2. click save (a validation error will be presented)
3. click browser back button
4. click browser forward button => browser message will pop up.

To solve this kind of problem and still follow the very nice and design of the SimpleFormController, something like the above mentioned RedirectFormController could be used.

Did anybody find a better solution to this issue?

Christof