Spring MVC Validation - Avoiding POST-back - spring-mvc

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 :)

Related

Accessing actual request in Message Handlers and Action filters in Web API

This is more of a design related question, and any help/pointers in the right direction is highly appreciated.
I am working on a ASP.NET Web API2 application, and have an Authorization filter and other Action filters. Within these filters, I need to access the Request object that comes as a part of the HttpPost request body.
I use the following code to read the request body content and deserialize into the desired object. It works fine.
//actionContext is HttpActionContext
var requestContent = actionContext.Request.Content.ReadAsStringAsync();
var request = JsonSerializer.GetObject<BaseOTARequestModel>(requestContent.Result);
To serve a particular request, I am deserializing the request content twice (I have 2 filters). Once the request reaches the controller action, it is deserialized again by the Web API framework. I feel guilty that every request is deserialized 3 times, and have a feeling there is a better way to handle this.
How can I avoid deserializing the request multiple times in a request?
I took this on as a challenge and came up with this solution. Here's a base filter attribute class:
public abstract class BaseOTARequestFilterAttribute : ActionFilterAttribute
{
private HttpActionContext _actionContext;
protected BaseOTARequestModel RequestModel
{
get
{
if (_actionContext.Request.Properties.ContainsKey("RequestModel"))
{
return _actionContext.Request.Properties["RequestModel"] as BaseOTARequestModel;
}
return null;
}
set
{
_actionContext.Request.Properties["RequestModel"] = value;
}
}
public override void OnActionExecuting(HttpActionContext actionContext)
{
_actionContext = actionContext;
if (RequestModel == null)
{
//actionContext is HttpActionContext
var requestContent = actionContext.Request.Content.ReadAsStringAsync();
RequestModel = JsonSerializer.GetObject<BaseOTARequestModel>(requestContent.Result);
}
}
}
This base class handles your deserialization and uses the Request.Properties collection to store it. (OK, I know a Web API is stateless but this state only exists during the execution of the request so fine imho.)
Your various attributes should all inherit this base class and can use the derialized object as follows:
public override void OnActionExecuting(HttpActionContext actionContext)
{
base.OnActionExecuting(actionContext);
var data = RequestModel;
// etc.
}
This may not be the most elegant solution, but I believe it works. Interested to hear the views of others.

Web Api - how to stop the web pipeline directly from an OnActionExecuting Filter

I have a pre-action web api hook that will check ModelState.IsValid. If the ModelState is not valid I do not want to execute the action and just return my message immediately. How exactly do I do this?
public class ValidateModelStateAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(System.Web.Http.Controllers.HttpActionContext actionContext) {
if (!actionContext.ModelState.IsValid)
{
var msg = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState);
// Now What?
}
base.OnActionExecuting(actionContext);
}
}
set the Response.Result. If the result is not null it will not execute the action. the exact syntax is escaping me right now, but it's as simple as
if(actionContext.ModelState.IsValid == false)
{
var response = actionContext.Request.CreateErrorResponse(...);
actionContext.Response = response;
}
Have you actually seen the example on the ASP.NET WebApi page?
Looks very much like what you're trying to achieve and all they do is setting the Response of the Context object:
If model validation fails, this filter returns an HTTP response that contains the validation errors. In that case, the controller action is not invoked.
http://www.asp.net/web-api/overview/formats-and-model-binding/model-validation-in-aspnet-web-api
see: Handling Validation Errors
My guess is that you should throw a HttpResponseException

spring auto populate user details for every request

I have a spring MVC based web application. Currently in my web page i am showing the user first name and last name after user logs in. The way i am doing this is, for every HttpServletRequest that comes into #Controller#RequestMapping, i get the Principal object and get the user details from it, then populate the ModelMap with firstname and lastname attribute. For example here is the sample code
#Autowired
private SecurityDetails securityDetails;
#RequestMapping(method = RequestMethod.GET)
public String showWelcomePage(HttpServletRequest request,
HttpServletResponse response, ModelMap model, Principal principal)
{
securityDetails.populateUserName(model, principal);
... lot of code here;
return "home";
}
public boolean populateUserName(ModelMap model, Principal principal) {
if (principal != null) {
Object ob = ((Authentication)principal).getPrincipal();
if(ob instanceof MyUserDetails)
{
MyUserDetails ud = (MyUserDetails)ob;
model.addAttribute("username", ud.getFirstName() + " " + ud.getLastName());
}
return true;
}
else
{
logger.debug("principal is null");
return false;
}
}
My problem is i am having to call the populateUserName method for every RequestMapping. Is there a elegant way, like populating this in Interceptor method, which will result in this method being called just in one place for entire application?
Its good that you want to prevent duplication of code. Here is how you can do it.
Create a custom HandlerInterceptor http://static.springsource.org/spring/docs/2.5.x/api/org/springframework/web/servlet/HandlerInterceptor.html
Post handle is the only method of interest for us, for the others return defaults.
In the post handle method, you have access to the model and view returned from your controller, go ahead and add whatever you want.
The Principal will not be available directly here, you will have to look it up using some code like SecurityContextHolder.getContext().getAuthentication().getPrincipal()
Wire the handler interceptor to intercept all or some of your controllers.
Hope this helps.
You can use either Servlet Filters or Spring Interceptors.
BTW, where do you populate the Principal from?
In any case, thats where you should do this populating stuff.

How do I call an Index action and conditionally pass it a value in an ASP.NET MVC app

I have an index action on a controller as follows...
public ActionResult Index(string errorMsg = "")
{
//do stuff
ViewBag.ErrorMsg=erorMsg;
return View();
}
I have another action that is an http post for Index.
When there is something wrong I want to reload the Index page and show the error...
I have my view already conditionally showing errorMsg. But I cannot figure out how to call Index and pass in the error string?
Typically, you'd just share the view between the two actions. I'm guessing you have actions that look something like this (the more info you provide about what index does, the better my example will be):
public ActionResult Index()
{
return View();
}
[HttpPost, ActionName("Index")]
public ActionResult IndexPost()
{
if (!ModelState.IsValid)
{
ViewBag.ErrorMsg = "Your error message"; // i don't know what your error condition is, so I'm just using a typical example, where the model, which you didn't specify in your question, is valid.
}
return View("Index");
}
And Index.cshtml
#if(!string.IsNullOrEmpty(ViewBag.ErrorMsg))
{
#ViewBag.ErrorMsg
}
#using(Html.BeginForm())
{
<!-- your form here. I'll just scaffold the editor since I don't know what your view model is -->
#Html.EditorForModel()
<button type="Submit">Submit</button>
}
If I understand you correctly you just need to hit the url with the errorMsg in the query string:
/*controllername*/index?errorMsg=*errormessage*
However, when there is something wrong you don't necessarily need to reload the page. Seems like you might be approaching this in the wrong way..?
You can use RedirectToAction to redirect to the page, with a querystring for errorMsg value.
[HttpPost]
public ActionResult Index(YourViewModel model)
{
try
{
//try to save and then redirect (PRG pattern)
}
catch(Exception ex)
{
//Make sure you log the error message for future analysis
return RedirectToAction("Index",new { errorMs="something"}
}
}
RedirectToAction issues a GET request. So your form values will be gone, because HTTP is stateless. If you want to keep the form values as it is in the form, return the posted viewmodel object again. I would get rid of ViewBag and add a new property called ErrorMsg to my ViewModel and set the value of that.
[HttpPost]
public ActionResult Index(YourViewModel model)
{
try
{
//try to save and then redirect (PRG pattern)
}
catch(Exception ex)
{
//Make sure you log the error message for future analysis
model.ErrorMsg="some error";
return View(model);
}
}
and in the view you can check this model property and show the message to user.

ASP.Net MVC 3 Strange Session Behaviour

I have an mvc 3 app for which I'm implementing authorization using my own login view which checks if the users name and password are allowed and then sets a variable in the session to say that the user is loggged in. This kind of works but for one particular view it is behaving in a strange undesirable way. The said view contains a form which I use to input some data and upload a file. For some reason which I can't figure out, after this form is posted a new session is started and therefore the variable which remembered that the user was logged in is reset to false and subsequently the login page is displayed again.
I'm lost as to why the application is starting a new session at this point? I have not instructed it to do this. Can anyone recommend solutions to stop this behaviour and get it to keep the old session?
Thanks.
UPDATE - Some Code:
Note the session seems to be terminated immediately after the response to the posted Create form
CMS controller which uses a custom Autorize attribute called "RDAutorize" on all actions:
[RDAuthorize]
public class PhotoCMSController : Controller
{
public ActionResult Create()
{
/* Code omitted: set up a newPhoto object with default state */
/* Display view containing form to upload photo and set title etc. */
return View("../Views/PhotoCMS/Create", newPhoto);
}
[HttpPost]
public ContentResult Upload(int pPhotoId)
{
/* Code ommited: receive and store image file which was posted
via an iframe on the Create view */
string thumbnail = "<img src='/path/to/thumb.jpg' />";
return Content(thumbnail);
}
[HttpPost]
public ActionResult Create(string pPhotoTitle, string pCaption etc...)
{
/*Code omitted: receive the rest of the photo data and save
it along with a reference to the image file which was uploaded
previously via the Upload action above.*/
/* Display view showing list of all photo records created */
return View("../Views/PhotoCMS/Index", qAllPhotos.ToList<Photo>());
/* **Note: after this view is returned the Session_End() method fires in
the Global.asax.cs file i.e. this seems to be where the session is
being lost** */
}
}/*End of CMS Controller*/
Custom Authorize action filter:
public class RDAuthorize : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
Boolean authorized = Convert.ToBoolean(
HttpContext.Current.Session["UserIsAuthorized"]
);
if (!authorized) {
/* Not logged in so send user to the login page */
filterContext.HttpContext.Response.Redirect("/Login/Login");
}
}
public override void OnActionExecuted(ActionExecutedContext filterContext) {}
public override void OnResultExecuting(ResultExecutingContext filterContext) {}
public override void OnResultExecuted(ResultExecutedContext filterContext) {}
}/*End of Authorize Action Filter*/
Login controller:
public class LoginController : Controller
{
private PhotoDBContext _db = new PhotoDBContext();
public ActionResult Login()
{
string viewName = "";
Boolean authorized = Convert.ToBoolean(Session["UserIsAuthorized"]);
if (authorized)
{
viewName = "../Views/Index";
}
else
{
viewName = "../Views/Login/Login";
}
return View(viewName);
}
[HttpPost]
public ActionResult Login(string pUsername, string pPassword)
{
string viewName = "";
List<Photo> model = new List<Photo>();
var qUsers = from u in _db.Users
select u;
foreach (User user in qUsers.ToList<User>())
{
/* If authorized goto CMS pages */
if (pUsername == user.Username && pPassword == user.Password)
{
Session["UserIsAuthorized"] = true;
var qPhotos = from p in _db.Photos
where p.IsNew == false
select p;
model = qPhotos.ToList<Photo>();
viewName = "../Views/PhotoCMS/Index";
break;
}
}
return View(viewName, model);
}
}/* End of Login controller */
Turns out the whole ASP.Net application was restarting because as part of the photo upload I was storing the image file in a temporary folder and then deleting the directory after moving the file to a permanent location. Apparently its default behaviour for ASP.Net to restart if a directory within the web site is deleted. I found this post
which describes the problem and offers a solution whereby the following code is added to the Global.asax.cs file. Implementing this solution has fixed the problem. The fix is applied by calling FixAppDomainRestartWhenTouchingFiles() from the Application_Start() event:
protected void Application_Start()
{
FixAppDomainRestartWhenTouchingFiles();
}
private void FixAppDomainRestartWhenTouchingFiles()
{
if (GetCurrentTrustLevel() == AspNetHostingPermissionLevel.Unrestricted)
{
/*
From: http://www.aaronblake.co.uk/blog/2009/09/28/bug-fix-application-restarts-on-directory-delete-in-asp-net/
FIX disable AppDomain restart when deleting subdirectory
This code will turn off monitoring from the root website directory.
Monitoring of Bin, App_Themes and other folders will still be
operational, so updated DLLs will still auto deploy.
*/
PropertyInfo p = typeof(HttpRuntime).GetProperty(
"FileChangesMonitor", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);
object o = p.GetValue(null, null);
FieldInfo f = o.GetType().GetField(
"_dirMonSubdirs", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.IgnoreCase);
object monitor = f.GetValue(o);
MethodInfo m = monitor.GetType().GetMethod(
"StopMonitoring", BindingFlags.Instance | BindingFlags.NonPublic);
m.Invoke(monitor, new object[] { });
}
}
private AspNetHostingPermissionLevel GetCurrentTrustLevel()
{
foreach (AspNetHostingPermissionLevel trustLevel in
new AspNetHostingPermissionLevel[] {
AspNetHostingPermissionLevel.Unrestricted,
AspNetHostingPermissionLevel.High,
AspNetHostingPermissionLevel.Medium,
AspNetHostingPermissionLevel.Low,
AspNetHostingPermissionLevel.Minimal }
)
{
try
{
new AspNetHostingPermission(trustLevel).Demand();
}
catch (System.Security.SecurityException)
{
continue;
}
return trustLevel;
}
return AspNetHostingPermissionLevel.None;
}
Since sessions are associated with cookies, they are available for a specific domain.
It's a common mistake to ask for a session variable in the same application while the domain has changed (i.e. redirecting to a subdomain).
Does the controller action that you are posting the form contains any [Authorize] attribute. You need to post some code.
Verify a new session is really started every time. Check Trace output for the user's session id to ensure it realllly has changed.
Ensure the cookie getting sent over is actually getting set and sent over. (called ASPsessionIDSOMETHING ) and if that is being sent by the browser. Download the tool Fiddler to check the cookies easily (set cookie header coming from the server and the request cookies going back to the server from the browser. Make sure your browser is accepting the cookie and you dont say... have cookies turned off.
If your session id is changing at every request then your session isn't properly getting set the first time, set a break point on that code if you havent already.
You can log when the worker process resets - ensure that isn't the case. see http://www.microsoft.com/technet/prodtechnol/windowsserver2003/library/IIS/87892589-4eda-4003-b4ac-3879eac4bf48.mspx
I had the same problem. The problem only occured when a post request was send to the server and the session was not modified during that request. What I did as a workaround was, to write a custom filter which does nothing more than writing a key / value into the session on each request and added that filter to the GlobalFilter collection in the global.asax.
public class KeepSessionAlive : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext filterContext)
{
if(filterContext.HttpContext.Session != null)
{
filterContext.HttpContext.Session["HeartBeat"] = DateTime.Now.ToShortDateString();
}
}
public void OnActionExecuted(ActionExecutedContext filterContext) { }
}
And in the global.asax:
protected override void AddCustomGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new KeepSessionAlive());
}
This might be not the best solution but it helped me in my case.

Resources