We used Java 5, Tomcat 5, Xalan, and JSF 1 to build an application that used XSLT, XML, and a Tomcat Filter to enable users to export their data in Excel format. We recently upgraded to Java 1.7.0_07, Tomcat 7.022 and JSF 2.1 (jsf-api-2.1.0-b03.jar). Due to the effort involved we have not yet upgraded to facelets; we still use jsp's. We use an tag to display the Excel report in its own popup window. The problem is that after the upgrade the popup is now displaying raw xml in IE, rather than the popup opening in Excel directly. The raw xml can be saved from the browser to a file, and if that saved file is double clicked, it does open up in Excel correctly, but it would be best if users could avoid that work-around.
I believe that the problem is that the response in JSF 2 is now being committed earlier than it was in JSF 1. Our web.xml file defines the following filters for Tomcat:
<filter>
<filter-name>XSLT Processor</filter-name>
<filter-class>com.cs.common.jsf.util.XsltProcessorFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>XSLT Processor</filter-name>
<url-pattern>*.xml</url-pattern>
</filter-mapping>
<filter>
<filter-name>Hibernate Session Manager</filter-name>
<filter-class>com.cs.common.hibernate.HibernateSessionServletFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>Hibernate Session Manager</filter-name>
<url-pattern>*.faces</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>Hibernate Session Manager</filter-name>
<url-pattern>*.xml</url-pattern>
</filter-mapping>
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>0</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>*.faces</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>*.xml</url-pattern>
</servlet-mapping>
And our XsltProcesserFilter class contains the following lines:
fChain.doFilter(request, wrapper);
response.setContentType("application/vnd.ms-excel");
By using sysouts
, I determined that the contentType
is not being set under JSF 2, presumably because the response has already been committed. I have tried setting the contentType
in the jsp that outputs the xml, but JSF then throws many errors, so presumably I need to set it later in the process (like in the filter above). I have tried response.setBufferSize(6400000)
prior to the doFilter
in the XsltProcessorFilter
, since I have read that doing so might delay the commit, but that does not solve the problem either.
How can I set the contentType
to application/vnd.ms-excel after faces has completed its processing but before the commit so that the browser will open up into Excel?
The solution to the above problem involved two issues. The first issue was that Tomcat was flushing the buffer and committing the response prior to the return to XSLTProcessorFilter. This was overcome by setting the buffer size to a large value in XSLTProcessorFilter prior to handing control over to Faces. Next, the Faces class JspViewHandlingStrategy was flushing the output at two points. This was overcome by adding a request attribute of isExcelXML with a value of "true" from XSLTProcessorFilter. Then, in JspViewHandlingStrategy coding was added to check for the isXML attribute, and if its value was true, the flushing was bypassed. After these changes, the Excel window now is presented to the user with the desired formatting. Of course one could simply comment out the two flushes in JspViewHandlingStrategy, but presumably they serve some purpose, so the solution here only bypasses them if they cause the problem at hand (when the XSLTProcessorFilter is called).
The doFilter method of our XSLTProcessorFilter now contains fixes:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain fChain) throws IOException, ServletException {
String xslTemplatePath = request.getParameter(XSLT_REQUEST_PARAM);
File xslTemplate = (xslTemplatePath == null) ? null : new File(servletPath, xslTemplatePath);
if ((xslTemplatePath != null) && xslTemplate.exists()) {
// Run the standard request processing to obtain the XML output
CharacterResponseWrapper wrapper = new CharacterResponseWrapper((HttpServletResponse) response);
response.setBufferSize(6400000); // This line overcomes Tomcat buffer flushing
request.setAttribute("isExcelXML", "true"); // This line signals to JSF to bypass the flushing
fChain.doFilter(request, wrapper);
response.setContentType("application/vnd.ms-excel");
// Transform the XML using the XSL stylesheet specified on the URL
Source xmlSource = new StreamSource(new StringReader(wrapper.toString()));
StreamSource xslSource = new StreamSource(xslTemplate);
try {
Transformer transformer = tFactory.newTransformer(xslSource);
StreamResult out = new StreamResult(response.getWriter());
transformer.transform(xmlSource, out);
} catch (Throwable t) {
t.printStackTrace(response.getWriter());
}
} else { // standard processing
fChain.doFilter(request, response);
}
}
The altered part of JspViewHandlingStrategy class is at the end of it's public void renderView(FacesContext context, UIViewRoot view) method:
//For XML output to Excel, bypass later flushings
boolean bypassFlush = false;
if (((ServletRequest)FacesContext.getCurrentInstance().getExternalContext().getRequest()).getAttribute("isExcelXML")!=null
&& ((ServletRequest)FacesContext.getCurrentInstance().getExternalContext().getRequest()).getAttribute("isExcelXML").toString().equals("true")) {
bypassFlush = true;
}
// write any AFTER_VIEW_CONTENT to the response (This comment in original JSF file)
// side effect: AFTER_VIEW_CONTENT removed (This comment in original JSF file)
ViewHandlerResponseWrapper wrapper = (ViewHandlerResponseWrapper)
RequestStateManager.remove(context, RequestStateManager.AFTER_VIEW_CONTENT);
if (null != wrapper && !bypassFlush) { //fix to Excel issue involved bypassing flush if isExcelXML set to true
wrapper.flushToWriter(extContext.getResponseOutputWriter(),
extContext.getResponseCharacterEncoding());
}
if (!bypassFlush) { //fix to Excel issue involved bypassing flush if isExcelXML set to true
extContext.responseFlushBuffer();
}
Since this took weeks to solve, it may be worthwhile to include some detail on how this was debugged in case others may have a use for the technique used.
Debug started with a download of the JSF source code. System.out,println() statements were put in each source code file starting with FacesServlet.java in its service method. The goal of the debug was to see where the isCommitted boolean switched from "false" to "true" since that was what was causing the problem. Statements similar to the following were used throughout each analyzed source code file:
System.out.println("someClass someLineId - " + someObjectReference.getClass().getName() +
((ServletResponse)FacesContext.getCurrentInstance().getExternalContext().getResponse()).isCommitted() +
((ServletResponse)FacesContext.getCurrentInstance().getExternalContext().getResponse()).getContentType());
(Note that "import javax.servlet.ServletResponse;" typically had to be added to the import statements in the source file.) Once a line was found where the isCommitted value was false before it and true after it, then the debug effort switched to the instantiated class that was called by that line. This process was continued until the problematic buffer flush lines were found.
Of course, the changes just described had to be run from the project to find the problem. Each source file had to be compiled (with a classpath that contained the many jars of the end project). Once the class was compiled, the original faces jar was renamed as a zip file and the newly compiled class put inside the zip file by overwriting the version of that file that was already there. Then the zip file was renamed back to a jar. The jar was then put into Eclipse, the project recompiled, and the project was run. Output was observed on the Tomcat output window. When multiple classes resulted from a single compilation (as can occur with inner classes) then all newly compiled classes were put in their place. (Although this may not have been required.)