spring webflow - end flow and execution snapshots - spring-webflow

I have this sort of security requirement where the user enters a url like this
http://webserver.com/someapp/test/test-flow?roomId=12345
when entering that url the flow is created and then if user deliberately changes roomId parameter some security filter will check if user has access to that room in particular, if it has access user can proceed, but if not the flow must be terminated and it is desirable to remove all flow snapshots(if several exist). So the code is like this
Extract from filter:
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
String roomId = req.getParameter("roomId");
if (roomId != null) {
if (currentUserHasAccess(roomId)) {
chain.doFilter(request, response);
} else {
flowExecutionManager.endFlow();
return;
}
}
chain.doFilter(request, response);
}
Now flowExecutionManager is like this
public class FlowExecutionManager extends FlowExecutionListenerAdapter {
private RequestControlContext context;
private FlowDefinition definition;
#Override
public void sessionCreating(RequestContext context,
FlowDefinition definition) {
super.sessionCreating(context, definition);
this.context = (RequestControlContext) context;
this.definition = definition;
}
public void endFlow() {
if (context != null && definition != null) {
context.removeAllFlowExecutionSnapshots();
context.endActiveFlowSession(definition.getId(), definition.getAttributes());
Flow flow = (Flow)definition;
flow.destroy();
}
}
In method endFlow i've tried switching the order of these lines
context.endActiveFlowSession(definition.getId(), definition.getAttributes());
context.removeAllFlowExecutionSnapshots();
and no matter the order of those 2 lines i always get a NPE like this (showing just an extract of stacktrace)
java.lang.NullPointerException
at org.springframework.webflow.conversation.impl.SessionBindingConversationManager.getConversationContainer(SessionBindingConversationManager.java:140)
at org.springframework.webflow.conversation.impl.SessionBindingConversationManager.getConversation(SessionBindingConversationManager.java:116)
at org.springframework.webflow.execution.repository.support.AbstractFlowExecutionRepository.getConversation(AbstractFlowExecutionRepository.java:183)
at org.springframework.webflow.execution.repository.support.AbstractFlowExecutionRepository.getConversation(AbstractFlowExecutionRepository.java:170)
at org.springframework.webflow.execution.repository.impl.DefaultFlowExecutionRepository.removeAllFlowExecutionSnapshots(DefaultFlowExecutionRepository.java:156)
at org.springframework.webflow.engine.impl.FlowExecutionImpl.removeAllFlowExecutionSnapshots(FlowExecutionImpl.java:431)
at org.springframework.webflow.engine.impl.RequestControlContextImpl.removeAllFlowExecutionSnapshots(RequestControlContextImpl.java:230)
at com.ags.blackcorp.finances.web.FlowExecutionManager.endFlow(FlowExecutionManager.java:26)
at com.ags.blackcorp.finances.web.RoomFilter.doFilter(RoomFilter.java:100)
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:237)
at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:167)
at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1084)
at org.springframework.security.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:378)
at org.springframework.security.intercept.web.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:109)
at org.springframework.security.intercept.web.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:83)
at org.springframework.security.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:390)
at org.springframework.security.ui.ExceptionTranslationFilter.doFilterHttp(ExceptionTranslationFilter.java:101)
at org.springframework.security.ui.SpringSecurityFilter.doFilter(SpringSecurityFilter.java:53)
at org.springframework.security.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:390)
at org.springframework.security.wrapper.SecurityContextHolderAwareRequestFilter.doFilterHttp(SecurityContextHolderAwareRequestFilter.java:91)
at org.springframework.security.ui.SpringSecurityFilter.doFilter(SpringSecurityFilter.java:53)
at org.springframework.security.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:390)
at com.ags.blackcorp.security.ui.webapp.AfterAuthenticationProcess.doFilterHttp(AfterAuthenticationProcess.java:55)
at org.springframework.security.ui.SpringSecurityFilter.doFilter(SpringSecurityFilter.java:53)
at org.springframework.security.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:390)
at org.springframework.security.ui.preauth.AbstractPreAuthenticatedProcessingFilter.doFilterHttp(AbstractPreAuthenticatedProcessingFilter.java:69)
at org.springframework.security.ui.SpringSecurityFilter.doFilter(SpringSecurityFilter.java:53)
at org.springframework.security.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:390)
at org.springframework.security.context.HttpSessionContextIntegrationFilter.doFilterHttp(HttpSessionContextIntegrationFilter.java:235)
at org.springframework.security.ui.SpringSecurityFilter.doFilter(SpringSecurityFilter.java:53)
at org.springframework.security.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:390)
at org.springframework.security.util.FilterChainProxy.doFilter(FilterChainProxy.java:175)
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:237)
at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:167)
at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1084)
Apparently the line context.endActiveFlowSession(definition.getId(), definition.getAttributes()); is ending the flow but i cant remove execution snapshots.
Any ideas what i might be doing wrong, or any idea how to remove execution snapshots.
Any idea regarding a best approach.
Thank u all in advance.

Trying to answer my own question led me to the following piece of code:
public void endFlow() {
if (context != null && definition != null) {
ExternalContextHolder.setExternalContext(contex.getExternalContext());
context.removeAllFlowExecutionSnapshots();
context.endActiveFlowSession(definition.getId(), definition.getAttributes());
Flow flow = (Flow)definition;
flow.destroy();
}
}
The ExternalContextHolder... line avoids the NPE, and snapshots are removed and flowSession its also terminated, Yet new questions have arisen
why is ExternalContextHolder necessary? is it ok?.
Below is some weird(or is it normal?) behaviour im getting
Suppose i've started a flow e1s1 in browser tab1 and also e2s1 in another tab, now if im on e2s1 and click 'next' button on my wizard i get e2s2(this is right), now if i remove execution snapshots that belong to e1s1, they are removed without problems, but if go to tab where e2s2 is and i click 'previous' button so i can return to previous snapshot, snapshot e2s1 is also gone, i mean shouldnt snapshot removal be like something "per execution". I have tested new code to remove flow execution (using method removeFlowExecution from class FlowExecutionRepository) but right now i wont show it, instead ill wait if someone can throw some pointers. Anyway if nothing shows up ill be keeping anyone interested in the loop.
Once again im answering my question, hopefully this is the last answer.
Q: why is ExternalContextHolder necessary?
Ans: according to my little experience ExternalContextHolder probably among other things, is needed so spring has access(HttpServletRequest and HttpServletResponse) to the data sent from whoever is doing the request.
In the end removing flowexecution from a filter might sound like a good idea, but webflow gives us a better approach yet, i mean subclassing FlowExecutionListenerAdapter and in this case we i overrode method "void requestSubmitted(RequestContext context)" and here i check wheter or not current user has access to roomId and then i will call method endFlow(see code below)
public void endFlow(ExternalContext externalContext) {
FlowUrlHandler handler = flowController.getFlowUrlHandler();
HttpServletRequest request = (HttpServletRequest) externalContext.getNativeRequest();
String stringKey = handler.getFlowExecutionKey(request);
if (stringKey != null) {
FlowExecutorImpl flowExecutor = (FlowExecutorImpl) flowController.getFlowExecutor();
FlowExecutionRepository repository = flowExecutor.getExecutionRepository();
FlowExecutionKey key = repository.parseFlowExecutionKey(stringKey);
ExternalContextHolder.setExternalContext(externalContext);
FlowExecutionLock lock = null;
try{
lock = repository.getLock(key);
}catch(NoSuchFlowExecutionException nsfee){
return;
}
lock.lock();
try {
FlowExecution flowExecution = repository.getFlowExecution(key);
repository.removeFlowExecution(flowExecution);
} finally {
lock.unlock();
}
}
}
flowController(org.springframework.webflow.mvc.servlet.FlowController) is injected by spring and its added in our webflow config file.
The above code removes flow Execution completely and if user tries to return to previous flow, lets say e1s1 then webflow automatically creates a new flow execution e2s1, and thats it.
In case u want to use the filter approach all u need in doFilter method its this
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
String roomId = req.getParameter("roomId");
ExternalContext externalContext = new ServletExternalContext(
this.servletContext, req, resp);
if (roomId != null) {
if (!currentUserHasAccess(roomId)) {
flowExecutionManager.endFlow();
return;
}
}
chain.doFilter(request, response);
this.servletContext its obtained through filterConfig.getServletContext()

I'm sorry, I can't comment because I don't have enough points to do so, but your implementation to your use case seems to be way more complicated than it should be. As suggested by #Patrick in the comment section of your question, why couldn't you just store the roomId argument in a flowScope var? You could perform your validation in the flow file or invoke a validation method and perform necessary transitions for successful or failed validations. You shouldn't have to care about snapshots, as those are WebFlow artifacts meant to be handled by the framework...

Related

How do I tell when a request is a forward in preHandle using Spring MVC?

I have the following...
#GetMapping("signup")
public String get(){
return "forward:/";
}
#Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws IOException {
...
if(!per.isPresent() && !request.getRequestURL().toString().contains("signup")){
response.sendRedirect("/signup");
return false;
}
}
The problem here is when the forward comes it isn't signup so I redirect back. However, I also want to intercept requests that go directly to the root.
Is there any way to tell if a request is a forward and what the original url was?
Here is the solution I came up with that works. I don't like this answer and I would prefer not attaching a custom header when it seems like the info that it is a forward should be available. However, since I couldn't find that info in the docs I just added the header...
#GetMapping("")
public String get(HttpServletResponse response){
response.setHeader("signup","true");
return "forward:/";
}
Then I checked for it
private static boolean isSignupForward(HttpServletResponse response){
return Boolean.valueOf(response.getHeader("signup"));
}
...
if(
!isSignupForward(response) &&
!per.isPresent() &&
!isSignup(request) &&
!isJsFile(request)
){
response.sendRedirect("/signup");
return false;
}
I will leave the bounty in case someone has a better answer.

Custom Error message with #Preauthorize and ##ControllerAdvice

We are using spring and spring-security-3.2. Recently We are adding annotations #PreAuthorize to RestAPIs(earlier it was URL based).
#PreAuthorize("hasPermission('salesorder','ViewSalesOrder')")
#RequestMapping(value = "/restapi/salesorders/", method = RequestMethod.GET)
public ModelAndView getSalesOrders(){}
We already have Global exception handler which annotated with - #ControllerAdvice and custom PermissionEvaluator in place, everything works fine except the error message.
Lets say some user is accessing API At moment without having 'ViewSalesOrder' permission then spring by default throws the exception 'Access is denied',but didn't tell which permission is missing (Its our requirement to mention which permission is missing).
Is it possible to throw an exception which also include the permission name, so final error message should be look like "Access is denied, you need ViewSalesOrder permission"(here permission name should be from #PreAuthorize annotation)?
Please note that we have 100 such restAPI in place so generic solution will be highly appreciated.
There is no pretty way of achieving what you expect since PermissionEvaluator interface doesn't let you pass the missing permission along with the result of the evaluation.
In addition, AccessDecisionManager decides on the final authorization with respect to the votes of the AccessDecisionVoter instances, one of which is PreInvocationAuthorizationAdviceVoter which votes with respect to the evaluation of #PreAuthorize value.
Long story short, PreInvocationAuthorizationAdviceVoter votes against the request (giving the request –1 point) when your custom PermissionEvaluator returns false to hasPermission call. As you see there is no way to propagate the cause of the failure in this flow.
On the other hand, you may try some workarounds to achieve what you want. One way can be to throw an exception within your custom PermissionEvaluator when permission check fails. You can use this exception to propagate the missing permission to your global exception handler. There, you can pass the missing permission to your message descriptors as a parameter. Beware that this will halt execution process of AccessDecisionManager which means successive voters will not be executed (defaults are RoleVoter and AuthenticatedVoter). You should be careful if you choose to go down this path.
Another safer but clumsier way can be to implement a custom AccessDeniedHandler and customize the error message before responding with 403. AccessDeniedHandler provides you current HttpServletRequest which can be used to retrieve the request URI. However, bad news in this case is, you need a URI to permission mapping in order to locate the missing permission.
I have implemented the second possible solution mentioned by Mert Z. My solution works only for #PreAuthorize annotations used in the API layer (e.g. with #RequestMapping). I have registered a custom AccessDeniedHandler bean in which I get the value of the #PreAuthorize annotation of the forbidden API method and fills it into error message.
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
private DispatcherServlet dispatcherServlet;
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException,
ServletException {
if (!response.isCommitted()) {
List<HandlerMapping> handlerMappings = dispatcherServlet.getHandlerMappings();
if (handlerMappings != null) {
HandlerExecutionChain handler = null;
for (HandlerMapping handlerMapping : handlerMappings) {
try {
handler = handlerMapping.getHandler(request);
} catch (Exception e) {}
if (handler != null)
break;
}
if (handler != null && handler.getHandler() instanceof HandlerMethod) {
HandlerMethod method = (HandlerMethod) handler.getHandler();
PreAuthorize methodAnnotation = method.getMethodAnnotation(PreAuthorize.class);
if (methodAnnotation != null) {
response.sendError(HttpStatus.FORBIDDEN.value(),
"Authorization condition not met: " + methodAnnotation.value());
return;
}
}
}
response.sendError(HttpStatus.FORBIDDEN.value(),
HttpStatus.FORBIDDEN.getReasonPhrase());
}
}
#Inject
public void setDispatcherServlet(DispatcherServlet dispatcherServlet) {
this.dispatcherServlet = dispatcherServlet;
}
}
The handler is registered in WebSecurityConfigurerAdapter:
#EnableGlobalMethodSecurity(jsr250Enabled = true, prePostEnabled = true)
#EnableWebSecurity
public abstract class BaseSecurityInitializer extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
...
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler());
...
}
#Bean
public AccessDeniedHandler accessDeniedHandler() {
return new CustomAccessDeniedHandler();
}
}
Beware that if there is also a global resource exception handler with #ControllerAdvice the CustomAccessDeniedHandler won't be executed. I solved this by rethrowing the exception in the global handler (as advised here https://github.com/spring-projects/spring-security/issues/6908):
#ControllerAdvice
public class ResourceExceptionHandler {
#ExceptionHandler(AccessDeniedException.class)
public ResponseEntity accessDeniedException(AccessDeniedException e) throws AccessDeniedException {
log.info(e.toString());
throw e;
}
}
You can throw an org.springframework.security.access.AccessDeniedException from a method that was called inside an EL-Expression:
#PreAuthorize("#myBean.myMethod(#myRequestParameter)")
Ideally, the #PreAuthorize annotation should be supporting String message(); in addition to the SpEl value. But, for whatever reason, it does not. Most of the suggestions here seem unnecessarily cumbersome and elaborate. As #lathspell has suggested, the simplest way to provide your own error message - along with any custom access validation logic - would be to add a simple method that performs the check and throws the AccessDeniedException in case the check fails, and then reference that method in the SpEl expression. Here's an example:
#RestController
#RequiredArgsConstructor // if you use lombok
public class OrderController {
private final OrderService orderService;
...
#GetMapping(value = "/salesorders", produces = MediaType.APPLICATION_JSON_VALUE)
#PreAuthorize("#orderController.hasPermissionToSeeOrders(#someArgOfThisMethod)")
public Page<OrderDto> getSalesOrders(
// someArgOfThisMethod here, perhaps HttpRequest, #PathVariable, #RequestParam, etc.
int pageIndex, int pageSize, String sortBy, String sortOrder) {
Pageable pageRequest = PageRequest.of(pageIndex, pageSize, Sort.Direction.fromString(sortOrder), sortBy);
return ordersService.retrieveSalesOrders(..., pageRequest);
}
public static Boolean hasPermissionToSeeOrders(SomeArgOfTheTargetMethod argToEvaluate) {
//check eligibility to perform the operation based on some data from the incoming objects (argToEvaluate)
if (condition fails) {
throw new AccessDeniedException("Your message");
}
return true;
}

Access a ViewScoped Managedbean in a Servlet

do anyone know a way to access a ViewScoped ManagedBean in a Servlet?
I can access a SessionScoped ManagedBean for example that way:
MyBean bean = (MyBean) request.getSession().getAttribute("myBean");
But if I set the scope to ViewScoped it returns null. I know that the reason is that the Servlet try to access the bean to early. But how can I fix this?
The backing bean:
#ManagedBean(name = "statistikHandler")
#SessionScoped //or ViewScoped
public class StatistikHandler {
private Object someAttribute
//Do something nice here
//getter and setter
}
The Servlet:
public class ImageStreamServlet extends HttpServlet {
protected void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
System.out.println("+++++ CALL THE IMAGESERVLET +++++");
//try to "inject" the Bean here
StatistikHandler handler = (StatistikHandler) request.getSession().getAttribute("statistikHandler");
try {
if (handler != null) {
//Do something with the ManagedBean
} else {
System.out.println("HANDLER NOT FOUND");
}
} finally {
}
}
#Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
#Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
}
Thats it. If I set the StatistikHandler to SessionScope it works fine. If I set the Handler to ViewScoped it doesn't work.
first: THX for your awnser.
Arjan Tijms:
The second thing is that the view scope only exists when there's a view being processed. It can't come out of thin air.
That make sense and I know that. I try to explain the flow and hope you will understand me. My english is not the best but I think its enough. So lets try :
I set an request to the view and thus an instance of the view scoped bean. So the view and the bean exist but the servlet isn't required jet.
Now I interact with the view and have to render an other part. Now the servlet is needed for this part and I setup an request to the servlet.
So: View and bean exists as an instance and than ( after an partial reload ) I request the servlet.
Arjan Tijms:
You will have to have some code that stores the reference in request scope, where the Servlet can find it and pick it up.
IMHO thats the important part. As u say i cant pick up a view scoped bean as an session attribute. I thank you very mutsh for this fact because I didn't know that before.
Now I can go on and think about a solution.
Thanks and regards
There are two things to be aware of.
The first is that you can't get an instance of the view scoped bean by asking for a session attribute. Those beans are simply not (directly) stored there.
The second thing is that the view scope only exists when there's a view being processed. It can't come out of thin air.
An example in Java code to illustrate that last statement:
// How to access i here???
while (foo) {
int i = 1;
// ...
}
As i is declared inside the while loop, it doesn't make sense to access it before that loop.
In case of the Servlet, if your Servlet is dispatching within the same request to the Faces Servlet, then you access the view scoped bean afterwards only, and still not directly. You will have to have some code that stores the reference in request scope, where the Servlet can find it and pick it up.
To use the Java analogy again, this would be like:
int bar = 0;
while (foo) {
int i = 1;
// ...
bar = i;
}
// use bar here
If you need the Servlet to set something up that the view scoped bean uses, then store that something in request scope and let the view scoped bean pick it up there. Again the Java analogy of this:
int bar = 23;
while (foo) {
int i = bar;
// ...
}
In other words, use a common "channel" to let those two communicate with each other.

Are there any restrictions in writing multiple http responses?

I am building a HTTP proxy with netty, which supports HTTP pipelining. Therefore I receive multiple HttpRequest Objects on a single Channel and got the matching HttpResponse Objects. The order of the HttpResponse writes is the same than I got the HttpRequest. If a HttpResponse was written, the next one will be written when the HttpProxyHandler receives a writeComplete event.
The Pipeline should be convenient:
final ChannelPipeline pipeline = Channels.pipeline();
pipeline.addLast("decoder", new HttpRequestDecoder());
pipeline.addLast("encoder", new HttpResponseEncoder());
pipeline.addLast("writer", new HttpResponseWriteDelayHandler());
pipeline.addLast("deflater", new HttpContentCompressor(9));
pipeline.addLast("handler", new HttpProxyHandler());
Regarding this question only the order of the write calls should be important, but to be sure I build another Handler (HttpResponseWriteDelayHandler) which suppresses the writeComplete event until the whole response was written.
To test this I enabled network.http.proxy.pipelining in Firefox and visited a page with many images and connections (a news page). The problem is, that the browser does not receive some responses in spite of the logs of the proxy consider them as sent successfully.
I have some findings:
The problem only occurs if the connection from proxy to server is faster than the connection from proxy to browser.
The problem occurs more often after sending a larger image on that connection, e.g. 20kB
The problem does not occur if only 304 - Not Modified responses were sent (refreshing the page considering browser cache)
Setting bootstrap.setOption("sendBufferSize", 1048576); or above does not help
Sleeping a timeframe dependent on the responses body size in before sending the writeComplete event in HttpResponseWriteDelayHandler solves the problem, but is a very bad solution.
I found the solution and want to share it, if anyone else has a similar problem:
The content of the HttpResponse is too big. To analyze the content the whole HTML document was in the buffer. This must be splitted in Chunks again to send it properly. If the HttpResponse is not chunked I wrote a simple solution to do it. One needs to put a ChunkedWriteHandler next to the logic handler and write this class instead of the response itself:
public class ChunkedHttpResponse implements ChunkedInput {
private final static int CHUNK_SIZE = 8196;
private final HttpResponse response;
private final Queue<HttpChunk> chunks;
private boolean isResponseWritten;
public ChunkedHttpResponse(final HttpResponse response) {
if (response.isChunked())
throw new IllegalArgumentException("response must not be chunked");
this.chunks = new LinkedList<HttpChunk>();
this.response = response;
this.isResponseWritten = false;
if (response.getContent().readableBytes() > CHUNK_SIZE) {
while (CHUNK_SIZE < response.getContent().readableBytes()) {
chunks.add(new DefaultHttpChunk(response.getContent().readSlice(CHUNK_SIZE)));
}
chunks.add(new DefaultHttpChunk(response.getContent().readSlice(response.getContent().readableBytes())));
chunks.add(HttpChunk.LAST_CHUNK);
response.setContent(ChannelBuffers.EMPTY_BUFFER);
response.setChunked(true);
response.setHeader(HttpHeaders.Names.TRANSFER_ENCODING, HttpHeaders.Values.CHUNKED);
}
}
#Override
public boolean hasNextChunk() throws Exception {
return !isResponseWritten || !chunks.isEmpty();
}
#Override
public Object nextChunk() throws Exception {
if (!isResponseWritten) {
isResponseWritten = true;
return response;
} else {
HttpChunk chunk = chunks.poll();
return chunk;
}
}
#Override
public boolean isEndOfInput() throws Exception {
return isResponseWritten && chunks.isEmpty();
}
#Override
public void close() {}
}
Then one can call just channel.write(new ChunkedHttpResponse(response) and the chunking is done automatically if needed.

Spring MVC Validation - Avoiding POST-back

I'd like to validate a Spring 3 MVC form. When an element is invalid, I want to re-display the form with a validation message. This is pretty simple so far. The rub is, when the user hits refresh after an invalid submission, I don't want them to POST, I want them to GET. This means I need to do a redirect from the form POST (submission) to re-display the form with validation messages (the form is submitted via a POST).
I'm thinking the best way to do this is to use SessionAttributeStore.retrieveAttribute to test if the form is already in the user's session. If it is, use the store form, otherwise create a new form.
Does this sound right? Is there a better way to do this?
To solve this problem, I store the Errors object in the session after a redirect on a POST. Then, on a GET, I put it back in the model. There are some holes here, but it should work 99.999% of the time.
public class ErrorsRedirectInterceptor extends HandlerInterceptorAdapter {
private final static Logger log = Logger.getLogger(ErrorsRedirectInterceptor.class);
private final static String ERRORS_MAP_KEY = ErrorsRedirectInterceptor.class.getName()
+ "-errorsMapKey";
#Override
public void postHandle(HttpServletRequest request, HttpServletResponse response,
Object handler, ModelAndView mav)
throws Exception
{
if (mav == null) { return; }
if (request.getMethod().equalsIgnoreCase(HttpMethod.POST.toString())) {
// POST
if (log.isDebugEnabled()) { log.debug("Processing POST request"); }
if (SpringUtils.isRedirect(mav)) {
Map<String, Errors> sessionErrorsMap = new HashMap<String, Errors>();
// If there are any Errors in the model, store them in the session
for (Map.Entry<String, Object> entry : mav.getModel().entrySet()) {
Object obj = entry.getValue();
if (obj instanceof Errors) {
if (log.isDebugEnabled()) { log.debug("Adding errors to session errors map"); }
Errors errors = (Errors) obj;
sessionErrorsMap.put(entry.getKey(), errors);
}
}
if (!sessionErrorsMap.isEmpty()) {
request.getSession().setAttribute(ERRORS_MAP_KEY, sessionErrorsMap);
}
}
} else if (request.getMethod().equalsIgnoreCase(HttpMethod.GET.toString())) {
// GET
if (log.isDebugEnabled()) { log.debug("Processing GET request"); }
Map<String, Errors> sessionErrorsMap =
(Map<String, Errors>) request.getSession().getAttribute(ERRORS_MAP_KEY);
if (sessionErrorsMap != null) {
if (log.isDebugEnabled()) { log.debug("Adding all session errors to model"); }
mav.addAllObjects(sessionErrorsMap);
request.getSession().removeAttribute(ERRORS_MAP_KEY);
}
}
}
}
It's not clear from your question but it sounds like your GET and POST actions are mapped to the same handler. In that case you can do something like:
if ("POST".equalsIgnoreCase(request.getMethod())) {
// validate form
model.addAttribute(form);
return "redirect:/me.html";
}
model.addAttribute(new MyForm());
return "/me.html";
In the JSP check if there are any error on the form and display as needed.
Such approach is called PRG (POST/REdirect/GET) design pattern I explained it few days ago as one of the answers:
Spring MVC Simple Redirect Controller Example
Hope it helps :)

Resources