Yesterday Christian asked a very interesting question: He had observed that the browser UI is blocked when clicking a button which generates a PDF on the server and sends the result. For about 30 seconds no button is working, no events are fired. My first thought was that this behaviour is caused by the submit locking during partial refreshs, and after testing a XSP.allowSubmit() in the debug console I could prove myself that I was right.
It looked first like an easy solution, but after thinking a little bit about it, I had no idea how to execute this little piece of CSJS code. Let’s have a look at the PDF generation:
public void exportPdfOutputStream(TemplateData pdfData, String templateFile, String filename) {
ExternalContext con = facescontext.getExternalContext();
XspHttpServletResponse response = (XspHttpServletResponse) con.getResponse();
try {
ServletOutputStream writer = response.getOutputStream();
// setting response headers for browser
response.setContentType(“application/pdf”);
response.setHeader(“Cache-Control”, “no-cache”);
response.setDateHeader(“Expires”, -1);
response.setHeader(“Content-Disposition”, “attachment; filename=\”" + filename + “\”");
PdfReader pdfTemplate = getPdfReader(templateFile);
PdfStamper stamper = new PdfStamper(pdfTemplate, writer);
stamper.setFormFlattening(true);
setDataToPdfDocument(pdfData, stamper);
stamper.close();
pdfTemplate.close();
writer.flush();
writer.close();
facescontext.responseComplete();
} catch (Exception e) {
String errorMessage = “PDF Exporter: An error occureed: ” + e.toString();
try {
response.sendError(500, errorMessage);
} catch (Exception e2) {
e2.printStackTrace();
}
}
}
A full refresh is submitted to the server, the code generates a PDF, adds the result to the output stream as attachment, and then closes the response. The browser receives a file to download and that’s it. Works fine. But how to execute some CSJS code? You cannot use view.postScript nor you cannot send an additional HTML element (a JS script block) and there is no onComplete event.
While I was musing I had the idea to use a repeating interval and check the server response in Javascript. But how can you access this information? When performing a XHR Call (a partial refresh) you can access the HTTP header of the response, but this is not possible when performing a full refresh.
I googled for an answer an found this article about a solution to block the UI: http://geekswithblogs.net/GruffCode/archive/2010/10/28/detecting-the-file-download-dialog-in-the-browser.aspx
It uses a cookie to send the information back to the browser, which is a really amazing idea. Here is my implementation for XPages and Dojo 1.8:
The button to download the PDF first executes a CSJS script which fills in the field fileDownloadToken with the current timestamp. This timestamp is sent to the server and then added to the cookie in the response.
The client checks every 500ms if the cookie exists and if the value equals the original token sent to the server. If the cookie is set correctly, the finishDownload() method is executed which calls XSP.allowSubmit() and removes the cookie.
This is the code of the XPage:
<?xml version="1.0" encoding="UTF-8"?>
<xp:view
xmlns:xp="http://www.ibm.com/xsp/core">
<xp:button
id="buttonDownload"
value="Download">
<xp:eventHandler
event="onclick"
submit="true">
<xp:this.action>
<![CDATA[#{javascript:
importPackage( ch.hasselba.tools );
new ch.hasselba.tools.PDFUtil().exportPdfOutputStream("output.pdf");}]]>
</xp:this.action>
<xp:this.script><![CDATA[
require(["dojo/cookie"], function(cookie){
var fileDownloadCheckTimer;
var tokenName = 'fileDownloadToken';
function finishDownload() {
window.clearInterval(fileDownloadCheckTimer);
cookie( tokenName, null, {expire: -1});
XSP.allowSubmit();
}
function startDownload(){
setToken();
fileDownloadCheckTimer = window.setInterval(function () {
var cookieValue = cookie( tokenName );
if ( cookieValue == getToken() )
finishDownload();
}, 500);
}
function getToken(){
return XSP.getElementById( tokenName ).value;
}
function setToken(){
XSP.getElementById( tokenName ).value = Date.now();
}
startDownload();
})
]]></xp:this.script>
</xp:eventHandler>
</xp:button>
<input type="hidden" id="fileDownloadToken" name="fileDownloadToken" />
</xp:view>
And this is the modified Java code:
package ch.hasselba.tools;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.Cookie;
import com.ibm.xsp.webapp.XspHttpServletResponse;
public class PDFUtil {
public void exportPdfOutputStream(String filename) {
FacesContext fc = FacesContext.getCurrentInstance();
ExternalContext ex = fc.getExternalContext();
XspHttpServletResponse response = (XspHttpServletResponse) ex
.getResponse();
// get the token from the request
String token = (String) ex.getRequestParameterMap().get(
"fileDownloadToken");
try {
ServletOutputStream writer = response.getOutputStream();
// setting response headers for browser
response.setContentType("application/pdf");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", -1);
response.setHeader("Content-Disposition", "attachment; filename=\""
+ filename + "\"");
// set the cookie to the response
response.addCookie(new Cookie("fileDownloadToken", token));
//
// generate the output
//
// close the writer and mark response as completed
writer.flush();
writer.close();
fc.responseComplete();
} catch (Exception e) {
e.printStackTrace();
}
}
}