Running AJAX with jQuery in Stripes Framework
Common introduction to AJAX in Stripes
Stripes framework offers basic but sufficient support for AJAX that is covered with article at official web site. Article recommends using commonly known PrototypeJS AJAX client side library. On the server side, request is processed by Stripes themselves by standard population and execution as any other http request (that means that data from client to server are sent as a standard URL encoded parameters). What you get is correctly populated and validated action bean - and until now you haven't even recognize, that request is made by JavaScript on the client side not even you have to care of it. You can access session, exchange cookies and so on.
After you process incoming request you have several possibilities how to return result:
- you can return RedirectResolution or ForwardResolution as you are used to when processing standard browser request - then, if the redirect or forward targets JSP or such, typically text/html content is returned as a response of the AJAX request. This output you can easily inject into appropriate container in DOM via setting innerHTML property (prototype has optimized object for this approach - Ajax.Updater)
- you can return JavaScriptResolution giving it Java POJO (could be called DTO too) in a constructor - Stripes then take care of marshalling Java object into JSON snippet of code, which could be easily (see chapter Stripes non-standard JSON result for more details) evaluated on a client side
- you can return StreamingResolution directly creating resulting content along with setting specific content type - this choice gives you most freedom but usually means much more effort than previous two options
Describing above possibilities you can deduce typical use cases for them:
- when you are not used to code on the client side or you want to save a lot of your time, you would probably choose first option - returning to client whole HTML snippet replacing or other way modifying part of the original page; on the other hand this option is probably the slowest of all involving JSP interpretation (or other type of presentation layer template)
- when you want to keep size of interchanged data small or you want to modify more different parts of the displayed page at once, second option would be better choice allowing you to access data from server in further interpretable form (simple JavaScript object); on the other hand you would have to make a lot of the work on the client depending on amount of needed modifications
- only when you need something special or you need to precisely polish resulting response, StreamingResolution should be taken into consideration; it gives you most possibilities for customizing the output but would involve most work
Why Stripes and not DWR?
Although I like using DWR, I must agree that it is worthy to use native Stripes support. DWR is oriented for direct access to Java objects and invoking their methods. When you have ActionBeans prepared for standard web application, you can use huge part of your code at the controller level (eg. validation, message rendering, interceptors, Spring bean injection, field populating protection and so on). With DWR you'd have to solve a lot of things again.
I think that DWR has better support for AJAX itself - I haven't ever run into an encoding problem with DWR as with Stripes & jQuery combination (see below), nor I have to solve the format in which DWR is transfering its data (DWR consists of both server and client side library). DWR also supports "server push", has more developed security (request forgery, crossite attack) and so on (I am not aware of having these in Stripes). But seamless integration with web framework is more important if you don't need such advanced features. It just can save a lot of time.
National characters encoding issue
This was the first problem I had to solve. You'd run into this issue when sending national characters from your browser while having configured non UTF-8 encoding in LocalePicker in Stripes configuration in web.xml. Let's say, you have your JSPs written in cp1250 encoding, you should then have the same locale set in Stripes LocalePicker configuration, so the Stripes will call request.setCharacterEncoding before any data will have been read from the request. In that combination national characters in request parameters should be ok.
What is the difference with AJAX? The difference is that frameworks such as jQuery or Prototypejs call, when preparing AJAX request, method encodeURIComponent() that will convert all special characters into unicode. This means that data that come in AJAX request are always (or at least this is what I realized) in UTF-8 encoding. So the AJAX request must have been treated differently than standard browser request.
What to do?
For me the solution was quite simple (although I don't like having Stripes configuration twice in my web.xml). This is what I had to do:
Configure Stripes in web.xml twice:
<filter>
<display-name>Stripes Filter</display-name>
<filter-name>StripesFilter</filter-name>
<filter-class>net.sourceforge.stripes.controller.StripesFilter</filter-class>
<init-param><param-name>ActionResolver.UrlFilters</param-name><param-value>WEB-INF/classes</param-value>
</init-param>
<init-param><param-name>LocalePicker.Locales</param-name><param-value>cs_CZ:windows-1250</param-value>
</init-param>
... other configuration ...
</filter>
<filter>
<display-name>Stripes Filter Ajax</display-name>
<filter-name>StripesFilterAjax</filter-name>
<filter-class>net.sourceforge.stripes.controller.StripesFilter</filter-class>
<init-param><param-name>ActionResolver.UrlFilters</param-name><param-value>WEB-INF/classes</param-value>
</init-param>
<init-param><param-name>LocalePicker.Locales</param-name><param-value>cs_CZ:UTF-8</param-value>
</init-param>
... other configuration ...
</filter>
<filter-mapping>
<filter-name>StripesFilter</filter-name>
<url-pattern>*.x</url-pattern>
<dispatcher>REQUEST</dispatcher>
</filter-mapping>
<filter-mapping>
<filter-name>StripesFilterAjax</filter-name>
<url-pattern>*.ajax</url-pattern>
<dispatcher>REQUEST</dispatcher>
</filter-mapping>
This configuration divides Stripes to treat two request differently accodring to extension of the script (*.x or *.ajax). Each of these requests will have character encoding set differently. Unfortunately I didn't find out better way how to distinguish these requests - they look the same to the server.
Until now it is ugly solution. But it allows me to neatly implement action beans. Basic action bean oriented for standard non AJAX access looks like this (there is a lot of code cut from the example):
@UrlBinding("/consultation/newQuery.x")
@StrictBinding
public class NewQueryAction extends AbstractAction implements ValidationErrorHandler {
@SpringBean
protected QueryManager queryManager;
@ValidateNestedProperties({
@Validate(field = "idIssue", on = "createQuery", required = true),
@Validate(field = "author", on = "createQuery", required = false),
@Validate(field = "idUser", on = "createQuery", required = false),
@Validate(field = "title", on = "createQuery", required = true),
@Validate(field = "query", on = "createQuery", required = true)
})
protected Query query = new Query();
@AllowBinding
protected Integer idIssue;
@AllowBinding
... some code has been cut ...
public Resolution createQuery() {
//create it
try {
queryManager.store(query, idIssue, false);
ctx.getMessages().add(new SimpleMessage("Query was succesfully stored."));
}
catch(IssueClosedException e) {
ValidationErrors errors = ctx.getValidationErrors();
errors.addGlobalError(new SimpleError("Issue has been already closed. Cannot add another query."));
return new ForwardResolution("/consultation/newQuery.jsp");
}
//query was created
return new RedirectResolution("/consultation/newQueryConfirmation.jsp");
}
}
And this is its extension for AJAX call.
@UrlBinding("/consultation/newQuery.ajax")
public class NewQueryAjaxAction extends NewQueryAction {
AjaxPostFormResult result;
public Resolution handleValidationErrors(ValidationErrors errors) throws Exception {
return new ActionContextJavaScriptResolution(this);
}
public Resolution createQuery() {
//create it
try {
queryManager.store(query, issue, false);
ctx.getMessages().add(new SimpleMessage(getAccessor().getMessage("consultation.ok.newQuery")));
}
catch(IssueClosedException e) {
ctx.getMessages().add(new SimpleError(getAccessor().getMessage("consultation.issueClosed")));
}
//query was created
return new ActionContextJavaScriptResolution(this);
}
}
Validation and other settings are inherited from the original action bean and only createQuery method is overwritten in AJAX implementation returning different result. If you wonder what ActionContextJavaScriptResolution class is - it is simple extension of JavaScriptResolution building POJO object containing basic information from ActionBean context to be sent to the client (I will include complete code of this resolution at the end of the article along with some demo, so you can use it if you want).
Stripes non-standard JSON result
Next thing I have encoutered when trying to execute AJAX calls to Stripes is, that Stripes JavaScriptResolution returns weird JSON code not compatible with jQuery nor Prototypejs. Stripes response contains serveral lines of code that has to be evaluated on the client by calling eval(response), but jQuery as well as Prototypejs looks for one declaration that they execute this way: eval("(" + response + ")"). The response returned by Stripes are not compatible with this way of manipulaiton.
Example of Stripes returned JSON
var _sj_root_884441334;
var _sj_27542048 = {actionPath:"/consultation/newQuery.ajax", fieldName:"query.title", fieldValue:""};
var _sj_2509847 = {};
var _sj_31941445 = {errorResult:"
What is wrong:- Title must not be empty.- Captcha was not recognized correctly..- Your name must not be empty- Query content must not be empty
", ok:false, okResult:""};
var _sj_18314597 = [null];
var _sj_5084615 = [null];
var _sj_10206935 = {actionPath:"/consultation/newQuery.ajax", fieldName:"query.query", fieldValue:""};
var _sj_15892877 = {actionPath:"/consultation/newQuery.ajax", fieldName:"captcha", fieldValue:""};
var _sj_126384 = {actionPath:"/consultation/newQuery.ajax", fieldName:"query.query", fieldValue:""};
var _sj_18632438 = {actionPath:"/consultation/newQuery.ajax", fieldName:"query.author", fieldValue:""};
var _sj_25434853 = [null, null];
var _sj_15818864 = [null];
_sj_2509847["captcha"] = _sj_15818864;
_sj_2509847["query.title"] = _sj_5084615;
_sj_5084615[0] = _sj_27542048;
_sj_15818864[0] = _sj_15892877;
_sj_25434853[0] = _sj_126384;
_sj_18314597[0] = _sj_18632438;
_sj_root_884441334 = _sj_31941445;
_sj_31941445.validationErrors = _sj_2509847;
_sj_2509847["query.query"] = _sj_25434853;
_sj_2509847["query.author"] = _sj_18314597;
_sj_25434853[1] = _sj_10206935;
_sj_root_884441334;
Example of expected JSON
{
errorResult:"cutted HTML content",
ok:false,
okResult:"",
validationErrors: {
"query.query": [{actionPath:"/consultation/newQuery.ajax", fieldName:"query.query", fieldValue:""}],
"query.author": [{actionPath:"/consultation/newQuery.ajax", fieldName:"query.author", fieldValue:""}],
"query.captcha": [{actionPath:"/consultation/newQuery.ajax", fieldName:"captcha", fieldValue:""}],
"query.title": [{actionPath:"/consultation/newQuery.ajax", fieldName:"query.title", fieldValue:""}]
}
}
What to do?
So in jQuery you have to execute requests in following format to be able to process response:
var callback = function(dataFromServer) {
onFormResult(formDiv, resultDiv, dataFromServer);
};
$.post(url, params, callback, "text");
function onFormResult(formDiv, resultDiv, data) {
var result = eval(data);
if (result.ok) alert("OK") else alert("Error");;
}
By fourth argument of the jQuery.post method you specify, that in return you expect plain text. This means that jQuery will no further process received response before handling it to your callback method. Processing of the returned data will be then made by you.
I wonder why the Stripes behave like that? Is there anyone who could explain?
Submitting standard HTML Form via AJAX - Example
At the end of this article I'd like to show you some simple example with snippets of code that you can you if you like it. It is simple HTML form submit in AJAX way. You can see how it looks like in the clip below.
Full resolution of the clip is here.
Demo leverages jQuery effects so it looks like quite well (at least I hope so). JavaScript on the client is pretty sraightforward - you just call one method giving it ids of the form and div, that will render errors / ok messages from the server and name of the event that should be called on the server side. Url itself is taken from the form action property.
<stripes:submit name="createQuery" onclick="return invoke('queryForm', 'ajaxResultContainer', 'createQuery');"/>
Then there si this JavaScript code, that will make an AJAX request and modify page after receiving an response:
var defaultStripesExenstion = ".x";
var ajaxStripesExenstion = ".ajax";
/*
* Function that uses jQuery to invoke an action of a form. Slurps the values
* from the form using prototype's 'form.serialize()' method, and then submits
* them to the server using prototype's 'jQuery.post' which transmits the request
* and then renders the resposne text into the named container.
*
* @param formDiv reference to the form object being submitted
* @param resultDivthe name of the HTML container to insert the result into
* @param event the name of the event to be triggered, or null
*/
function invoke(formDiv, resultDiv, event) {
try {
var form = $("#" + formDiv + " form");
var action = form.get(0).action;
var extIndex = action.indexOf(defaultStripesExenstion);
//compose ajax url from original form action replacing original stripes script extension
//with stripes ajax extension configured in web.xml
var url = action.substr(0, extIndex) +
ajaxStripesExenstion +
action.substr(extIndex + defaultStripesExenstion.length, action.length);
//create params from serializing form inputs and add param simulating button click in order to
//Stripes could find actionBean event handler
var params = form.serialize();
if(event != null) params = event + '&' + params;
//hide errors if visible
$("#" + resultDiv).hide("slow");
//fade out the form
$("#" + formDiv).fadeOut("slow");
//fire ajax request
var callback = function(dataFromServer) {
onFormResult(formDiv, resultDiv, dataFromServer);
};
//set up what to do if AJAX request fails
$("").ajaxError(onServerError);
$.post(url, params, callback, "text");
//return false to button to stop standard submit of the form
return false;
} catch(ex) {
return true;
}
}
/*
* Method is executed when server returns exception.
*/
function onServerError(event, request, settings, error){
alert("Server returned an error, form was not probably processed by the server.");
}
/*
* Method is called when server returns non JSON data.
*/
function onServerDataError(data){
alert("Server has returned data in unknown format.");
}
/*
* Method is called when server returns reply to ajax call.
*
* @param data contains reply from the server
*/
function onFormResult(formDiv, resultDiv, data) {
try {
var result = eval(data);
} catch (ex) {
onServerDataError(data);
}
if (result.ok) {
onSuccess(formDiv, resultDiv, result);
} else {
onError(formDiv, resultDiv, result);
}
}
/*
* Method is executed, when form was submited with success.
*/
function onSuccess(formDiv, resultDiv, result) {
$("#" + formDiv).queue(function() {
$("#" + resultDiv).html(result.okResult).fadeIn("slow");
$(this).dequeue();
});
}
/*
* Method is executed, when form was submitted and server returned error.
*/
function onError(formDiv, resultDiv, result) {
$("#" + resultDiv).html(result.errorResult).show("slow");
$("#" + formDiv).queue(function () {
var callback = function() {
colorizeLabel(this, result);
};
$("#" + formDiv + " label").each(callback);
$("#" + formDiv + " #captchaImage").each(reloadCaptcha);
$(this).dequeue();
}).fadeIn("slow");
}
/*
* Reloads captcha image.
*/
function reloadCaptcha() {
var newSrc = $(this).attr("src");
var randomAtrribute = "?random=";
var index = newSrc.indexOf(randomAtrribute);
if (index > -1) {
newSrc = newSrc.substring(0, index) + randomAtrribute + new Date().getTime();
} else {
newSrc = newSrc + randomAtrribute + new Date().getTime();
}
$(this).attr("src", newSrc);
}
/*
* Method will add class "error" to each label, whose input was found in validationErrors object.
* In the oposite way, it removes any possible "error" class from any labes, that currently is not in error.
*/
function colorizeLabel(label, result) {
var error = result.validationErrors[label.htmlFor];
var errorIndex = -1;
if (label.className != null) errorIndex = label.className.indexOf('error');
if (error != null && errorIndex == -1) {
label.className = label.className + " error";
}
if (error == null && errorIndex > -1) {
label.className = label.className.substring(0, errorIndex)
+ label.className.substring(errorIndex + 5, label.className.length);
}
}
On the server side there is action bean shown before. As a result it returns ActionContextJavaScriptResolution(actionBeanContext) that will create simple POJO java object:
/**
* Simple DTO containing information about form posting:
*
* - boolean state flag: ok / not ok
* - String containing HTML snippet rendering Stripes messages / errors
* - ValidationErrors object containing details on error fields
*/
public class AjaxPostFormResult {
private boolean ok;
private ValidationErrors validationErrors;
private String okResult = "";
private String errorResult = "";
public AjaxPostFormResult(String okResult, String errorResult) {
this.ok = (errorResult == null);
this.okResult = okResult;
this.errorResult = errorResult;
}
public AjaxPostFormResult(ValidationErrors validationErrors, String errorResult) {
this.ok = false;
this.validationErrors = validationErrors;
this.errorResult = errorResult;
}
public boolean isOk() {
return ok;
}
public ValidationErrors getValidationErrors() {
return validationErrors;
}
public String getOkResult() {
return okResult;
}
public String getErrorResult() {
return errorResult;
}
}
ActionContextJavaScriptResolution is simple extension of JavaScript resultions that only creates result POJO object based on information that can be found in standard ActionBeanContext. ActionContextJavaScriptResolution uses standard Stripes properties to render HTML containing errors or messages it finds in context - so you needn't to do nothing more than you have to do when creating standard Stripes application. This HTML snippet prepared by the server is then used by Javascript to simply pasting it into specified div.
/**
*
Resolution that will convert a Java object web to a web of JavaScript objects and arrays, and
* stream the JavaScript back to the client. The output of this resolution can be evaluated in
* JavaScript using the eval() function, and will return a reference to the top level JavaScript
* object. For more information see {@link JavaScriptBuilder}
*
*
Resolution builds up result from the object of current ActionBean context returning simple
* DTO {@link com.fg.mojefirma.consulting.web.dto.AjaxPostFormResult} with error or ok messages,
* containing involved validation errors if they occur.
*/
public class ActionContextJavaScriptResolution implements Resolution {
private JavaScriptBuilder builder;
/**
* Constructs a new JavaScriptResolution that will convert the supplied object to JavaScript.
*
* @param actionBean
*/
public ActionContextJavaScriptResolution(ActionBean actionBean) {
ActionBeanContext ctx = actionBean.getContext();
Object result;
HttpServletRequest request = ctx.getRequest();
ValidationErrors validationErrors = ctx.getValidationErrors();
if (!validationErrors.isEmpty()) {
result = new AjaxPostFormResult(
validationErrors,
composeErrorsHtml(validationErrors, request)
);
} else {
result = new AjaxPostFormResult(
composeMessagesHtml(ctx.getMessages(), request),
composeErrorsHtml(ctx.getMessages(), request)
);
}
this.builder = new JavaScriptBuilder(result);
}
/**
* Converts the object passed in to JavaScript and streams it back to the client.
*/
public void execute(HttpServletRequest request, HttpServletResponse response) throws Exception {
response.setContentType("text/javascript");
this.builder.build(response.getWriter());
response.flushBuffer();
}
/**
* Compose HTML snippet with messages based on standard properties enveloping used by Stripes
* tag.
* @param messages
* @param request
* @return
*/
private String composeMessagesHtml(List messages, ServletRequest request) {
String[] properties = getStandardMessageProperties(request);
return composeResult(request, messages, properties,
new MessageFilter() {
public boolean includeMessage(Message message) {
return !(message instanceof ValidationError);
}
}
);
}
/**
* Compose HTML snippet with messages based on standard properties enveloping used by Stripes
* tag.
* @param messages
* @param request
* @return
*/
private String composeErrorsHtml(List messages, ServletRequest request) {
String[] properties = getStandardErrorProperties(request);
return composeResult(request, messages, properties,
new MessageFilter() {
public boolean includeMessage(Message message) {
return (message instanceof ValidationError);
}
}
);
}
/**
* Compose HTML snippet with messages based on standard properties enveloping used by Stripes
* tag.
* @param errors
* @param request
* @return
*/
private String composeErrorsHtml(ValidationErrors errors, ServletRequest request) {
String[] properties = getStandardErrorProperties(request);
List messages = new ArrayList();
for(List validationErrors : errors.values()) {
messages.addAll(validationErrors);
}
return composeResult(request, messages, properties,
new MessageFilter() {
public boolean includeMessage(Message message) {
return true;
}
}
);
}
/**
* Returns standard message Stripes properties in array.
* @param request
* @return
*/
private String[] getStandardMessageProperties(ServletRequest request) {
Locale locale = request.getLocale();
ResourceBundle bundle = StripesFilter.getConfiguration()
.getLocalizationBundleFactory().getErrorMessageBundle(locale);
return new String[] {
bundle.getString("stripes.messages.header"),
bundle.getString("stripes.messages.footer"),
bundle.getString("stripes.messages.beforeMessage"),
bundle.getString("stripes.messages.afterMessage")
};
}
/**
* Returns standard message Stripes properties in array.
* @param request
* @return
*/
private String[] getStandardErrorProperties(ServletRequest request) {
Locale locale = request.getLocale();
ResourceBundle bundle = StripesFilter.getConfiguration()
.getLocalizationBundleFactory().getErrorMessageBundle(locale);
return new String[] {
bundle.getString("stripes.errors.header"),
bundle.getString("stripes.errors.footer"),
bundle.getString("stripes.errors.beforeError"),
bundle.getString("stripes.errors.afterError")
};
}
/**
* Renders HTML snippet result containing all messages conforming to MessageFilter implementation
* enveloped by properties strings.
* @param request
* @param messages
* @param properties
* @param messageFilter
* @return
*/
private String composeResult(ServletRequest request, List messages, String[] properties, MessageFilter messageFilter) {
boolean hasMessages = false;
StringBuffer result = new StringBuffer(properties[0]);
for(Message message : messages) {
if (messageFilter.includeMessage(message)) {
hasMessages = true;
result.append(properties[2])
.append(message.getMessage(request.getLocale()))
.append(properties[3]);
}
}
if (hasMessages) {
result.append(properties[1]);
return result.toString();
} else {
return null;
}
}
/**
* Interface used to filter messages going to output.
*/
private interface MessageFilter {
/**
* Returns true or false if message should render in output.
* @param message
* @return
*/
boolean includeMessage(Message message);
}
}
And that's all. I believe that this solution and code is transferable to any other HTML form. I hope that this will help you to AJAXize your plain HTML forms without much effort.
Komentáře