View Full Version : Redirect after post (again, sorry!)
jonmor
Apr 27th, 2006, 05:54 AM
OK, a very simple one. I have not been sufficiently diligent in following this practice for the webapp I'm working on and want to make some changes. What I currently have in a couple of places is a situation where a user POSTs a form which changes some data in the database and then displays a page showing the results of what has changed. I have simply returned a ModelAndView with the result view from the onSubmit in the controller. Problem is, of course, that the user clicks the refresh button and the webapp tries to make the same changes again...
So, I am assuming the right thing to do here is to redirect instead in the onSubmit. It gets messy, though, because there is a lot of data to be displayed in the results page, and as I understand it, if I am using a redirect then this model data has to be tacked on as request parameters to the URL, which could quickly become unworkable.
The way I am thinking this perhaps should be done is to store the results model data in the session and then call it up from the session (and subsequently clear it) when the redirected request is handled. Is this what others do, in which case do you have a nice straightforward Springy pattern for doing it and ensuring debris data isn't left lying around in the session? Or is there another way of doing this which I haven't thought of?
Colin Yates
Apr 27th, 2006, 06:07 AM
So, I am assuming the right thing to do here is to redirect instead in the onSubmit. It gets messy, though, because there is a lot of data to be displayed in the results page, and as I understand it, if I am using a redirect then this model data has to be tacked on as request parameters to the URL, which could quickly become unworkable.
You might also be interested in reading this thread, although it won't solve any of your problems :) http://forum.springframework.org/showthread.php?t=22111
The way I am thinking this perhaps should be done is to store the results model data in the session and then call it up from the session (and subsequently clear it) when the redirected request is handled. Is this what others do, in which case do you have a nice straightforward Springy pattern for doing it and ensuring debris data isn't left lying around in the session? Or is there another way of doing this which I haven't thought of?
I find using the session is always a painful experience :( If it is the only option; then fine.
To ensure the view is removed from the session I would probably write a wrapper around the view which removes it from the session:
public class SessionCleaningView implements View {
private List<String> namesToRemove;
private View delegateView;
public String getContentType() {
return delegateView.getContentType();
}
public void render(Map model, HttpServletRequest request, HttpServletResponse response) throws Exception {
try {
delegateView.render(Map model, HttpServletRequest request, HttpServletResponse response);
} finally {
Session session = request.getSession();
for (String name: namesToRemove) {
session.removeAttribute(name);
}
}
}
// setters hidden
}
Depending on how you define your views (I use ResourceBundleViewResolver):
<bean id="viewResolver" class="org.springframework.web.servlet.view.ResourceBundl eViewResolver">
<description>View resolver to convert between view names and actual resources.</description>
<property name="basename"><value>config/views</value></property>
<property name="defaultParentView"><value>defaultView</value></property>
<property name="order" value="1"/>
</bean>
you would wire up like:
innerResultDoneView.(class)=org.springframework.we b.servlet.view.JstlView
innerResultDoneView.url=/WEB-INF/jsps/..../resultDone.jsp
resultDoneView.(class)=uk......YourWrappingView
resultDoneView.delegateView(ref)=innerResultDoneVi ew
resultDoneView.namesToRemove)=nameA,nameB,nameC
jonmor
Apr 27th, 2006, 08:40 AM
Thanks for that. It's a good idea to wrap the view, the kind of 'Springy' thing I was talking about. I was actually thinking to use a standard prefix (such as 'redirect:') for the attribute names, which would save having to populate the list with specific names.
I would actually have thought that this requirement was sufficiently common for Spring to have something built-in to deal with it. Who knows, maybe this is a chance for me to make my mark! :)
Colin Yates
Apr 27th, 2006, 08:49 AM
You might also want to look at Spring Web Flow which provides an excellent mechanism for managing state associated with a usercase rather than state associated with a browser's interaction with the server (i.e. the session).
http://www.springframework.org/node/235 is an excellent book if you want to read about Spring Web Flow (shameless self promotion :))
laenzlinger
May 7th, 2006, 05:59 AM
Hi all,
yatesco's SessionCleaningView inspired me to write a RedirectAfterPostView.
This view checks if the HTTP method was POST. After such a POST request, the view stores the model into the session and delegates to a redirect view. After a GET request - on the other hand - it restores any previously stored model from the session and delegates to an other view.
/**
* The RedirectAfterPostView is intended to be used with form controllers to
* avoid known issues with the usage of browser back buttons or reload buttons.
* It can be used to redirect to a view after a POST request instead of to
* directly present the result in the same request. <br/>
*
* @author Christof Laenzlinger
* @version $Id$
*/
public class RedirectAfterPostView implements View {
protected static Log log = LogFactory.getLog(RedirectAfterPostView.class);
private View delegatingView_;
private RedirectView redirectView_;
private List namesToStore_;
public void render(Map model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
if (shouldRedirect(model, request)) {
storeModel(model, request);
redirectView_.render(model, request, response);
} else {
model = restoreModel(model, request);
delegatingView_.render(model, request, response);
}
}
/**
* @return true if the view should redirect.
* The default implementation will redirect after a POST request.
*/
protected boolean shouldRedirect(Map model, HttpServletRequest request) {
return "POST".equals(request.getMethod());
}
/**
* Store the given model into the session.
* @param model the model to store.
* @param request the HTTP request.
*/
protected void storeModel(Map model, HttpServletRequest request) {
if (model == null) {
return;
}
Map modelToStore = new HashMap();
if (hasNamesToStore()) {
for (Iterator iter = namesToStore_.iterator(); iter.hasNext();) {
Object key = iter.next();
Object value = model.get(key);
modelToStore.put(key, value);
model.remove(key);
}
} else {
modelToStore.putAll(model);
model.clear();
}
HttpSession session = request.getSession();
if (log.isInfoEnabled()) {
log.info("store [" + modelToStore + "]");
}
session.setAttribute(getSessionAttributeName(), modelToStore);
}
/**
* Restore a previously stored model from the session into the given model.
* @param model the model to restore the stored model into.
* @param request the HTTP request.
*/
protected Map restoreModel(Map model, HttpServletRequest request) {
HttpSession session = request.getSession();
Map modelToRestore = (Map) session.getAttribute(getSessionAttributeName());
if (modelToRestore != null) {
if (hasNamesToStore()) {
for (Iterator iter = namesToStore_.iterator(); iter.hasNext();) {
Object key = iter.next();
Object value = modelToRestore.get(key);
if (model == null) {
model = new HashMap();
}
model.put(key, value);
}
} else {
if (model == null) {
model = new HashMap();
}
model.putAll(modelToRestore);
}
}
if (log.isInfoEnabled()) {
log.info("restore [" + modelToRestore + "]");
}
session.removeAttribute(getSessionAttributeName()) ;
return model;
}
protected String getSessionAttributeName() {
return "" + redirectView_.getBeanName() + "redirect.model";
}
private boolean hasNamesToStore() {
return namesToStore_ != null && namesToStore_.size() > 0;
}
// accessors hidden
This RedirectAfterPostView can be used for example with the SimpleFormController for both the formView and the successView. For example:
--------- ------------ ------------
|Web | |Form | |View |
|Browser| |Controller| |Controller|
--------- ------------ -------- ------------
|-------GET form------->| | Form | |
| |--------> | View | |
| | -------- |
|<------------OK form------------------| |
|---POST form (error)-->| | |
| |------------->| |
|<------------REDIRECT-----------------| |
|---GET form----------->| |
| |------------->| |
|<------------OK form------------------| | ---------
|-------POST form ----->| | | |Success|
| |-------------------------------->|View |
| | | | ---------
|<------------------------REDIRECT----------------------------|
|--------------------GET success view-------------->| |
| | | |-------->|
|<-----------OK success---------------------------------------|
| | | | |
With the namesToStore List one can define which elements of the model are stored into the session. If namesToStore is empty, the whole model is stored into the session.
In my example I configured it with an XmlViewResolver to declare the views:
<beans>
<bean name="newPersonForm" class="example.RedirectAfterPostView">
<property name="delegatingView">
<bean class="org.springframework.web.servlet.view.JstlView">
<property name="url" value="/WEB-INF/jsp/newPerson.jsp"/>
</bean>
</property>
<property name="redirectView">
<bean class="org.springframework.web.servlet.view.RedirectView">
<property name="url" value="/example/person"/>
<property name="contextRelative" value="true"/>
</bean>
</property>
</bean>
<bean name="newPersonSuccess" class="example.RedirectAfterPostView">
<property name="delegatingView">
<bean class="org.springframework.web.servlet.view.JstlView">
<property name="url" value="/WEB-INF/jsp/newPersonSuccess.jsp"/>
</bean>
</property>
<property name="redirectView">
<bean class="org.springframework.web.servlet.view.RedirectView">
<property name="url" value="/example/personSuccess"/>
<property name="contextRelative" value="true"/>
</bean>
</property>
</bean>
</beans>
As already mentioned the view can be used for both the formView and the successView. The advantage of using it as well for the formView is, that pressing browser's back-buttons or refresh-buttons does not result in a pop-up for re-posting.
Please let me know if you have an opinion about this proposal. I think it is a nicer solution than the one I proposed in an other thread before: http://forum.springframework.org/showthread.php?t=23346&highlight=redirect+post
Colin Yates
May 7th, 2006, 08:23 AM
I see.
So in effect you are defining a single view which consists of a redirect to a second view. The data used by the second view is persisted in session between the redirect and then immediately restored before doing the forward.
Nice. Like it.
There are probably a couple of improvements I might make, obviously feel free to disregard :)
- make the selection of objects a strategy, not just name based; I might want selection based on annotations etc.
- make the storage a strategy; I might want to persist things on the filesystem instead
- enforce the concept that this class is really a single view which consists of the "normal" view wrapped in a POST.... that is the intention, but it isn't really that clear from the documentation....it needs to be clear that this view isn't very long lived.
- do the null check for the model in the initial render, not in the delegate methods
- use constructor args to illustrate the mandatory collaborators
I would end up with something like:
public class RedirectGuardingView implements View {
private final View view_;
private final View redirectView_;
private ModelSelectionStrategy modelSelectionStrategy;
private Storage storage;
public RedirectGuardingView(final View theView, final View theRedirectView, final ModelSelectionStrategy theSelectionStrategy, final Storage theStorage) {
....
}
public void render(Map specifiedModel, HttpServletRequest request,
HttpServletResponse response) throws Exception {
Map model = (specifiedModel == null? new HashMap() : specifiedModel);
if (shouldRedirect(request)) {
storeModel(model, request);
redirectView_.render(model, request, response);
} else {
model = restoreModel(model, request);
view_.render(model, request, response);
}
}
protected boolean shouldRedirect(HttpServletRequest request) {
return "POST".equals(request.getMethod());
}
protected void storeModel(Map model, HttpServletRequest request) {
Map modelToStore = new HashMap();
// we expect retrieveObjectsToStore to remove objects out of the model as well.
Map modelToStore = modelSelectionStrategy.retrieveObjectsToStore(mode l, request);
storage.store(modelToStore, request);
}
protected Map restoreModel(Map model, HttpServletRequest request) {
Map map = storage.restore(request);
return map;
}
Maybe it isn't an improvement. Not sure :) This is actually starting to look like the basis of a generic ModelProcessingView....
Just my lazy thoughts on a Sunday afternoon :)
Colin Yates
May 7th, 2006, 08:26 AM
Actually I do like your original idea; whilst not as flexible, it is probably sufficient for 90% use-case and is very easy to configure.
There is always that ease-of-use/flexibility argument going on in my head, and it started moaning about all the collaborators that my "improvements" require to be configured ;) :)
laenzlinger
May 7th, 2006, 09:16 AM
yatesco, thanks a lot for your suggestions. They make perfect sense to me.
Factoring out the selection and storage strategy is a good idea. I just would provide default strategies to reduce configuration effort under "normal" circumstances.
There are some additional aspects that are not considered by this solutions:
1. what should happen in case of a refresh of the success page (the model would have been removed from the session then)
2. what should happen in case of a refresh of the form view page.
Those are probably more Controller related concerns than View concerns.
Before I have seen your SessionCleaningView I was never thinking about a solution based on a View implementation. I was very impressed about Spring's flexibility to customize every part of the framework :-)
I am pretty new to Spring-MVC (I had to use Struts with Spring, due to non-technical reasons), but I was a bit surprised that there is no "out-of-the-box" solution for the Redirect-After-Post pattern. (Please let me know if I missed something). I know that Spring Web Flow offers a very nice solution. Would it be better to use SWF even for such simple Form-Result scenarios?
Thanks again a lot for you time on this sunday afternoon...
Colin Yates
May 7th, 2006, 09:28 AM
Those are probably more Controller related concerns than View concerns.
Well, the fundamental problem is keeping information around that *isn't* passed via the request (which the original Redirect->Post solution assumes).
Not sure what the solution is ...
Before I have seen your SessionCleaningView I was never thinking about a solution based on a View implementation. I was very impressed about Spring's flexibility to customize every part of the framework :-)
Yeah; Spring rocks :)
I am pretty new to Spring-MVC (I had to use Struts with Spring, due to non-technical reasons), but I was a bit surprised that there is no "out-of-the-box" solution for the Redirect-After-Post pattern. (Please let me know if I missed something).
Well, you can still do the redirect after post; but as you initially mentioned in this thread, there is too much information to go into the redirect URL....
I know that Spring Web Flow offers a very nice solution. Would it be better to use SWF even for such simple Form-Result scenarios?
SWF certainly has a very nice storage mechanism. Rather than just a request scope and session scope you have the concept of a conversation scope and flow scope which are extremely powerful.
I am not sure whether I would recommend SWF for trivial form implementations, but in this case, where the problem is one of *context* it might be worth looking at.
Check out the samples and take it from there ....
Thanks again a lot for you time on this sunday afternoon...
Yeah, my wife is starting to shout :)
karthi.vaiyapuri
Sep 21st, 2006, 02:15 AM
Hello all,
I', facing the same problem in my application.
but you have solved.
i got the concepts in theoretically,
can you post the wholes message again or send me the idea,
it will be help full to me.
vBulletin® v3.7.3, Copyright ©2000-2008, Jelsoft Enterprises Ltd.