November 04, 2003

Sending Binary Data Via Struts

Struts is a MVC web framework. It allows you to specify the paths through a web application in an XML configuration file, and provides some nice tag libraries for JSP manipulation. The views for the states are usually JSPs, and the controller is a servlet provided for you.

This standard setup is fine, most of the time. But I've run into situations, as have others (1,2), where you need Struts to output a binary file. In my case, I'm sending a dynamic image to a non browser client, and I basically need to write a ByteArrayOutputStream to the response output stream.

Now, you'd think I'd be able to do this with a JSP. After all, a JSP is just a servlet turned inside out, right? Well, according to the specification, you'd be right. From page 42 of JSP 1.2 spec:

----------------
The JSP container should not invoke response.getWriter() until the time when the first portion of the content is to be sent to the client. This enables a number of uses of JSP, including using JSP as a language to glue actions that deliver binary content, or reliably forwarding to a servlet, or change dynamically the content type of the response before generating content. See Chapter JSP.3.
----------------

But, according to Tomcat 4.1.24 on the Linux platform, you'd be wrong. When calling 'java.io.OutputStream rs = response.getOutputStream();' in my JSP, I get this code generated:

---------------

....snip...
      application = pageContext.getServletContext();
      config = pageContext.getServletConfig();
      session = pageContext.getSession();
      out = pageContext.getOut();
      _jspx_out = out;
java.io.OutputStream rs = response.getOutputStream();
....snip...

---------------

The JSP is taking my stream before my code has a chance. Therefore, I get an "getOutputStream() has already been called for this response" error. The weird bit is that this doesn't seem to happen on Tomcat 4.1.24 on Windows (same version of struts).

So, what do you do? You write a servlet instead. That way you have utter control over the output stream:

---------------

import javax.servlet.http.*;
import javax.servlet.*;
import java.io.*;

public class BinaryStreamServlet extends HttpServlet {

   public void service(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
      String contentType =
(String)req.getAttribute("contentType");
      if (contentType == null || "".equals(contentType)) {
         contentType = "image/png"; // default
      }
      res.reset();
      res.setContentType(contentType);
      OutputStream sos = res.getOutputStream();
      ByteArrayOutputStream baos = (ByteArrayOutputStream)req.getAttribute("baos");
      baos.writeTo(sos);
   }
}

---------------

I set up my action classes to cache the ByteArrayOutputStream in the request, with the name "baos." I added these lines to my web.xml:

---------------

<servlet>
      <servlet-name>binaryFileServlet</servlet-name>
       <servlet-class>BinaryStreamServlet</servlet-class>
  </servlet>
....snip...
  <servlet-mapping>
    <servlet-name>binaryFileServlet</servlet-name>
    <url-pattern>/binaryFile</url-pattern>
  </servlet-mapping>

---------------

and this to my struts-config.xml for any actions that needed to be able to send binary data:

---------------

<forward name="success"         path="/binaryFile"/>

---------------

Works like a charm. Leave the JSPs to the character data, and use this simple servlet for binary delivery. Posted by moore at November 4, 2003 10:15 PM

Comments

Thank you for the solution for my problem. I have to do a generic download for Struts and tried the way of using getOutputStream(). It apparently worked, but generated 200 lines of stack traces for each buffer I sent to the browser. So I will try your servlet solution.

Posted by: Edson Watanabe at December 3, 2003 07:05 AM | Permalink

Hey Edson,

Glad I could help. I wish there was an easier way.

Dan

Posted by: Dan Moore at December 7, 2003 01:40 PM | Permalink

Thanks, *a lot*. I was completely stuck till I found this page mentioning that TC4 on Linux works differently.

J.

Posted by: Justin at February 27, 2004 06:29 AM | Permalink

It's nice to know that my code is infact compliant and TC4 is just being a bit retarded.

Maybe this will convince my smalltime ISP to upgrade.. :)

Posted by: Paul Richards at June 21, 2004 04:52 PM | Permalink

THIS IS NOT A SOLUTION

going thru servlet solves problem but it by-passes the whole struts framework. The question is how do we get it working thru struts?

Posted by: kuzya at July 20, 2004 09:55 PM | Permalink

Kuzya,

I agree that this is not the optimal solution. I won't say it bypasses struts entirely, since you're still using the struts-config.xml to map the destination. It does miss giving you the internationalization and automagic error handling of struts. But, if it's binary data, I can't see what features of struts you're needing.

Posted by: Dan at July 21, 2004 11:53 AM | Permalink

I had the same situation that the pdf always came back as TEXT/HTML. After number of tests, I got it work with the following. The key is to flush() before you write to it. Maybe this is just a bug of the current struts version (1.1):

public ActionForward execute(...) {
...
response.setContentType("application/pdf");
response.setHeader("Content-disposition", sbContentDispValue.toString());
response.setContentLength(baosPDF.size());
ServletOutputStream sos;
sos = response.getOutputStream();
sos.flush(); // this line made a big difference.
baosPDF.writeTo(sos);
sos.flush();
sos.close();

return null;
}

Posted by: Gang at July 29, 2004 01:39 PM | Permalink

I took the sos.fluch() off (the one before writeTo()) and tried again. Well, I could not reproduce the issue anymore. I got PDF generated directly from the Action class. It worked just like it was supposed to.

I guess the issue I had before was just some kind of random problem on my computer. So, I'd like to recall the previous message. Please disregard it.

Thanks,

Posted by: Gang at July 29, 2004 07:20 PM | Permalink

Everything in this article rings true to the issue I had just solved and the info in here was quite helpfull in finding out what was wrong. However, there was a simple solution to fix this using jsp, even in tomcat. I'm not sure if it applies to the exact same versions, but, as the mentioned article comes up in google quite high in the ranking and is used in many resolutions of the mentioned exception I thought I'd like you know what I came to find.
As most of us will have to import a few packages in the jsp page one may assume the first line of the jsp page is taken up by a page-directive, as was the case with me. Then after that follows a scriptlet section. Now the only problem is, most of us generally place a carriage return between the closing greater than '>' character and the starting lesser than '<' character of the page-directive and the scriptlet section. This will cause tomcat to open the JspWriter. Making sure no characters existed outside of my directives and scriptlet-tags solved the problem without the need for writing a seperate servlet.

Posted by: Martin Reurings at January 2, 2005 12:41 PM | Permalink

I would like to add a comment to your article "Sending Binary Data Via Struts". I'm having the same problem as you have (had) with
generating binary content from JSP in Struts. We're using IBM WebSphere and are about to upgrade from WebSphere4 to WebSphere6.

Everything worked fine in WAS4, but not now in WAS6. The binary data is generated and displayed on screen, but it is followed by ugly
stack traces on the server console: "OutputStream already obtained".

But by inspecting the stack trace, I realised that the exception was *not* thrown when my code "response.getOutputstream" was executed.
Instead, the exception was thrown after all the code in the JSP had finished executing, which explains why I first got the file and then
was followed by the ugly stack trace.

I also found the cause. WAS has a feature called "keepgenerated" which, if set to true, keeps the java code that is generated from the
JSP. By inspecting the generated java code I discovered that WAS6 causes a \r\n to be written to the implicit variable "out" (of type
JSPWriter):

<jsp:useBean
id="actionForm"
scope="request"
type="mypackage.MyActionForm">
</jsp:useBean>

The exception was thrown when WAS tried to flush "out" (containing the \r\n), *after* the binary data had been sent to the client.

It didn't help to write like this either:

<%@ page
...
%><jsp:useBean
...
</jsp:useBean><% /** Java code **/ %>

The solution was to replace the useBean tag with java code:

<%@ page
...
%><%

mypackage.MyActionForm actionForm = null;
actionForm = (mypackage.MyActionForm) request.getAttribute("actionForm");

/** Java code **/ %>

Posted by: Patrik Thornblad at February 9, 2006 08:41 AM | Permalink

I was actually wrong in my conclusion the useBean tag was resplonsible for the newline character. In fact, the JSP contained
a newline character between the opening and closing useBean tag. By removing this, it worked fine with the useBean tag!

Non-working jsp jsp:

<%@ page
...
%><jsp:useBean
id="actionForm"
scope="request"
type="mypackage.MyActionForm">
</jsp:useBean><% /** Java code **/ %>

Working jsp:

<%@ page
...
%><jsp:useBean
id="actionForm"
scope="request"
type="mypackage.MyActionForm"
></jsp:useBean><% /** Java code **/ %>

Posted by: Patrik Thornblad at February 15, 2006 09:08 AM | Permalink
© Moore Consulting, 2003-2006