consider this:
- user clicks on a link
- request goes to DisplayLoginAction
- Login.jsp is displayed
- user enters his credentials
- form is mapped to ValidateLoginAction
- validation fails in ValidateLoginAction
- ValidateLoginAction stores the errors and returns "input"
- redirecting to DisplayLoginAction..
- DisplayLoginAction retrieves the errors from the interceptor and shows them to the user above the login form
- user refreshes the page
- The errors are gone
How should I save the errors on page refresh?
<action name="displayLoginPage" class="DisplayLoginAction">
<interceptor-ref name="store">
<param name="operationMode">RETRIEVE</param>
</interceptor-ref>
<interceptor-ref name="customStack"/>
<result name="success">Login.jsp</result>
<result name="input">Login.jsp</result>
</action>
<action name="validateloginForm" class="ValidateLoginAction">
<interceptor-ref name="store">
<param name="operationMode">STORE</param>
</interceptor-ref>
<interceptor-ref name="customStack"/>
<result name="input" type="redirectAction">displayLoginPage</result>
<result name="success">LoginConfirmation.jsp</result>
</action>
user refreshes the page
The errors are gone
How should I save the errors on page refresh?
That is the way MessageStoreInterceptor works. It is actually a feature, not a bug.
Page refresh is an action triggered by the user, that means it can be assumed he has already read the result of the previous operation (the login attempt), unless he is pressing F5 with the eyes closed.
You should WANT a message to expire after the first read.
Consider a page with a lot of non-ajax operations, like combobox depending on others, etc... If the error message is persistent, it would popup after each submit operation that does not involve going to another page.
You don't want that. You should want to get the message saying that one operation has gone wrong (or right) just after that operation. If the user then proceed with other operations, like a refresh, if those operations aren't going in error (or in a specific success state), no message should be shown.
After that, there's also the problem of when deleting from session a persistent message, because otherwise you would get that message in the next action with a RETRIEVE
or AUTOMATIC
operationMode. Then you could (but shouldn't) customize the MessageStore Interceptor, or writing / reading / deleting messages from and to the session on your own, basically reinventing the wheel. Or even put two MessageStore Interceptors, one RETRIEVE
and one STORE
for displayLoginPage
Action, getting the pitfalls just mentioned.
You are also using the PRG pattern (Post/Redirect/Get), to avoid re-sending the data when refreshing the page. At the same way, you should avoid re-reading the same messages twice.
To see how this specifically works, you can take a look at the MessageStore Interceptor source code, that is quite simple:
- Before Invocation, if Action is
ValidationAware
and operationMode
is RETRIEVE
or AUTOMATIC
:
- read
actionMessages
, actionErrors
, fieldErrors
from session;
- merge them with current action's
actionMessages
, actionErrors
, fieldErrors
;
- remove
actionMessages
, actionErrors
, fieldErrors
from session.
/**
* Handle the retrieving of field errors / action messages / field errors, which is
* done before action invocation, and the <code>operationMode</code> is 'RETRIEVE'.
*
* @param invocation
* @throws Exception
*/
protected void before(ActionInvocation invocation) throws Exception {
String reqOperationMode = getRequestOperationMode(invocation);
if (RETRIEVE_MODE.equalsIgnoreCase(reqOperationMode) ||
RETRIEVE_MODE.equalsIgnoreCase(operationMode) ||
AUTOMATIC_MODE.equalsIgnoreCase(operationMode)) {
Object action = invocation.getAction();
if (action instanceof ValidationAware) {
// retrieve error / message from session
Map session = (Map) invocation.getInvocationContext().get(ActionContext.SESSION);
if (session == null) {
if (LOG.isDebugEnabled()) {
LOG.debug("Session is not open, no errors / messages could be retrieve for action ["+action+"]");
}
return;
}
ValidationAware validationAwareAction = (ValidationAware) action;
if (LOG.isDebugEnabled()) {
LOG.debug("retrieve error / message from session to populate into action ["+action+"]");
}
Collection actionErrors = (Collection) session.get(actionErrorsSessionKey);
Collection actionMessages = (Collection) session.get(actionMessagesSessionKey);
Map fieldErrors = (Map) session.get(fieldErrorsSessionKey);
if (actionErrors != null && actionErrors.size() > 0) {
Collection mergedActionErrors = mergeCollection(validationAwareAction.getActionErrors(), actionErrors);
validationAwareAction.setActionErrors(mergedActionErrors);
}
if (actionMessages != null && actionMessages.size() > 0) {
Collection mergedActionMessages = mergeCollection(validationAwareAction.getActionMessages(), actionMessages);
validationAwareAction.setActionMessages(mergedActionMessages);
}
if (fieldErrors != null && fieldErrors.size() > 0) {
Map mergedFieldErrors = mergeMap(validationAwareAction.getFieldErrors(), fieldErrors);
validationAwareAction.setFieldErrors(mergedFieldErrors);
}
session.remove(actionErrorsSessionKey);
session.remove(actionMessagesSessionKey);
session.remove(fieldErrorsSessionKey);
}
}
}
- After Invocation, if Action is
ValidationAware
and operationMode
is STORE
or (operationMode
is AUTOMATIC
and Result is of type redirectAction
):
- read
actionMessages
, actionErrors
, fieldErrors
from action;
- store
actionMessages
, actionErrors
, fieldErrors
in the session.
/**
* Handle the storing of field errors / action messages / field errors, which is
* done after action invocation, and the <code>operationMode</code> is in 'STORE'.
*
* @param invocation
* @param result
* @throws Exception
*/
protected void after(ActionInvocation invocation, String result) throws Exception {
String reqOperationMode = getRequestOperationMode(invocation);
boolean isRedirect = invocation.getResult() instanceof ServletRedirectResult;
if (STORE_MODE.equalsIgnoreCase(reqOperationMode) ||
STORE_MODE.equalsIgnoreCase(operationMode) ||
(AUTOMATIC_MODE.equalsIgnoreCase(operationMode) && isRedirect)) {
Object action = invocation.getAction();
if (action instanceof ValidationAware) {
// store error / messages into session
Map session = (Map) invocation.getInvocationContext().get(ActionContext.SESSION);
if (session == null) {
if (LOG.isDebugEnabled()) {
LOG.debug("Could not store action ["+action+"] error/messages into session, because session hasn't been opened yet.");
}
return;
}
if (LOG.isDebugEnabled()) {
LOG.debug("store action ["+action+"] error/messages into session ");
}
ValidationAware validationAwareAction = (ValidationAware) action;
session.put(actionErrorsSessionKey, validationAwareAction.getActionErrors());
session.put(actionMessagesSessionKey, validationAwareAction.getActionMessages());
session.put(fieldErrorsSessionKey, validationAwareAction.getFieldErrors());
}
else if(LOG.isDebugEnabled()) {
LOG.debug("Action ["+action+"] is not ValidationAware, no message / error that are storeable");
}
}
}
Note 1: The login operation is also self-explanatory: if after logging in you land on the login page again, it can only mean that the login failed... BTW the above explanation applies to every page/functionality
Note 2: There are sites producing messages that expire automatically after X seconds, not caring about the user having read them or not... and that is against the usability, IMHO.