I am using the Dojo ProgressBar to show a long running process in Struts2 using the execAndWait interceptor. The execAndWait interceptor puts the action on the value stack for each call that returns the wait result. However, when the result type is JSON, the action only has the default values of the action.
Here is my struts action config
(I tried to wild card the result names, but that didn’t work)
NOTE: I have to wrap my JSON in a textarea because Dojo’s iframe.send expects it to be wrapped in a textarea.
<action name="upload" class="ProcessFileAction" method="upload">
<interceptor-ref name="agfStack" />
<interceptor-ref name="execAndWait">
<param name="delay">1000</param>
<param name="delaySleepInterval">500</param>
</interceptor-ref>
<result name="wait" type="json">
<param name="noCache">true</param>
<param name="contentType">text/html</param>
<param name="wrapPrefix"><![CDATA[<html><body><textarea>]]></param>
<param name="wrapSuffix"><![CDATA[</textarea></body></html>]]></param>
<param name="includeProperties">percentComplete,processMessage,running</param>
</result>
<result name="success" type="json">
<param name="noCache">true</param>
<param name="contentType">text/html</param>
<param name="wrapPrefix"><![CDATA[<html><body><textarea>]]></param>
<param name="wrapSuffix"><![CDATA[</textarea></body></html>]]></param>
<param name="includeProperties">percentComplete,processMessage,running</param>
</result>
<result name="error" type="json">
<param name="noCache">true</param>
<param name="contentType">text/html</param>
<param name="wrapPrefix"><![CDATA[<html><body><textarea>]]></param>
<param name="wrapSuffix"><![CDATA[</textarea></body></html>]]></param>
<param name="includeProperties">percentComplete,processMessage,running</param>
</result>
</action>
Here is my JSP
NOTE: I have to use the iFrame.send action beacuse I am uploading a file. Since my struts result will be the same for the initial wait return as it will for each successive call, I have to use the iframe.send for the AJAX call too (as opposed to xhrGet). That is because iframe.send expects the JSON to be wrapped in a text area, and xhrGet doesn’t.
<%@taglib uri="http://www.springframework.org/security/tags" prefix="security"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<%@taglib uri="http://tiles.apache.org/tags-tiles" prefix="tiles"%>
<script type="text/javascript">
dojo.require("dojo.io.iframe");
dojo.require("dijit.ProgressBar");
dojo.require('dojox.timing');
var t = new dojox.timing.Timer(<tiles:getAsString name="refreshTime" />);
t.onTick = function() {
dojo.io.iframe.send({
url: '<s:url action="upload" namespace="/" />',
method: "POST",
handleAs: "json",
load: function(response, ioArgs){
if(response.running) {
var progressPercent = response.percentComplete + "%";
var reportText = response.processMessage;
var uploadProgesssBar = dijit.byId("uploadProgress");
if(uploadProgesssBar == null) {
return;
}
uploadProgesssBar.update({progress:progressPercent, report:function() {return reportText;}});
}
else {
showById("progressDialogButtonDiv");
t.stop();
}
},
error: function(response, ioArgs) {
t.stop();
var progressPercent = response.percentComplete + "%";
var reportText = response.processMessage;
var uploadProgesssBar = dijit.byId("uploadProgress");
if(uploadProgesssBar == null) {
return;
}
uploadProgesssBar.update({progress:progressPercent, report:function() {return reportText;}});
showById("progressOkButton");
}
});
};
function showProgressBar() {
hideById("uploadForm");
showById("progressBar");
}
function hideProgressBar() {
hideById("progressBar");
showById("uploadForm");
}
function submitAndShowProgress(submitForm) {
dojo.io.iframe.send({
form: submitForm,
handleAs: "json",
load: function(response, ioArgs) {
submitForm.reset();
var progressPercent = response.percentComplete + "%";
var reportText = response.processMessage;
if(response.running) {
showProgressBar();
var uploadProgesssBar = dijit.byId("uploadProgress");
if(uploadProgesssBar == null) {
return;
}
uploadProgesssBar.update({progress:progressPercent, report:function() {return reportText;}});
t.start();
}
else {
createAndShowAlertDialog(reportText, "");
}
return response;
},
error: function(response, ioArgs) {
t.stop();
hideProgressBar();
createAndShowAlertDialog(response.processMessage, "An Error Occurred");
return response;
}
});
return false;
}
</script>
<div id="progressBar" class="hidden loading">
<div dojoType="dijit.ProgressBar" style="width:400px" jsId="uploadProgress" id="uploadProgress" annotate="true"></div>
<div class="hidden actionButtons" id="progressOkButton">
<button style="position: relative; left: 42px;" dojoType="dijit.form.Button" onClick="hideProgressBar(); return false;">Ok</button>
</div>
</div>
<s:form action="upload" namespace="/" enctype="multipart/form-data" id="uploadForm" method="post" onsubmit="return submitAndShowProgress(this);">
<s:file name="file" id="file" label="File" />
<s:submit id="submit" name="submit">Upload</s:submit>
</s:form>
Here is my action:
(trimmed)
public class ProcessFileAction implements LongRunning {
String processMessage = "Uploading...";
Integer percentComplete = 0;
Boolean running = true;
private FileProcessor fileProcessor;
private File file;
private String fileContentType;
private String fileFileName;
public String upload() throws Exception {
setProcessMessage("Processing File...");
setPercentComplete(10);
try {
if(!getFile().exists()) {
getFile().createNewFile();
}
File processedFile = getFileProcessor().process(getFile(), this);
} catch(Exception e) {
e.printStackTrace();
setProcessMessage("An error occurred while processing your file.");
setPercentComplete(100);
setRunning(false);
return ERROR;
}
setProcessMessage("Process Complete!");
setPercentComplete(100);
setRunning(false);
return SUCCESS;
}
...
}
public interface LongRunning {
public void setProcessMessage(String processMessage);
public String getProcessMessage();
public void setPercentComplete(Integer percentComplete);
public Integer getPercentComplete();
public void setRunning(boolean running);
public boolean isRunning();
}
I am going to look at the code for the JSON result type and ExecAndWait interceptor for more clues.
After looking at
JSONResult, I found the problem.When you set a ‘root’ object on the JSON, it looks for the values on the value stack. If not, it looks at the ActionInvocations action (which is not used with the ExecAndWait Intercepror).
So, I created a LongRunningImpl class to hold the LongRunning information
I used that object in my action:
And then set a root object on my results:
Now everything works! I might list this as a defect for Struts2, but I am not sure how it’ll be received. I think the JSON Result object should check for the action on the value stack before defaulting back to the action on the ActionInvocation.