1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 package org.apache.struts2.dispatcher;
23
24 import java.io.InputStream;
25 import java.io.OutputStream;
26
27 import javax.servlet.http.HttpServletResponse;
28
29 import com.opensymphony.xwork2.ActionInvocation;
30 import com.opensymphony.xwork2.util.logging.Logger;
31 import com.opensymphony.xwork2.util.logging.LoggerFactory;
32 import com.opensymphony.xwork2.util.ValueStack;
33
34 /***
35 * <!-- START SNIPPET: description -->
36 *
37 * A custom Result type for sending raw data (via an InputStream) directly to the
38 * HttpServletResponse. Very useful for allowing users to download content.
39 *
40 * <!-- END SNIPPET: description -->
41 * <p/>
42 * <b>This result type takes the following parameters:</b>
43 *
44 * <!-- START SNIPPET: params -->
45 *
46 * <ul>
47 *
48 * <li><b>contentType</b> - the stream mime-type as sent to the web browser
49 * (default = <code>text/plain</code>).</li>
50 *
51 * <li><b>contentLength</b> - the stream length in bytes (the browser displays a
52 * progress bar).</li>
53 *
54 * <li><b>contentDisposition</b> - the content disposition header value for
55 * specifing the file name (default = <code>inline</code>, values are typically
56 * <i>filename="document.pdf"</i>.</li>
57 *
58 * <li><b>inputName</b> - the name of the InputStream property from the chained
59 * action (default = <code>inputStream</code>).</li>
60 *
61 * <li><b>bufferSize</b> - the size of the buffer to copy from input to output
62 * (default = <code>1024</code>).</li>
63 *
64 * <li><b>allowCaching</b> if set to 'false' it will set the headers 'Pragma' and 'Cache-Control'
65 * to 'no-cahce', and prevent client from caching the content. (default = <code>true</code>)
66 * </ul>
67 *
68 * <p>These parameters can also be set by exposing a similarly named getter method on your Action. For example, you can
69 * provide <code>getContentType()</code> to override that parameter for the current action.</p>
70 *
71 * <!-- END SNIPPET: params -->
72 *
73 * <b>Example:</b>
74 *
75 * <pre><!-- START SNIPPET: example -->
76 * <result name="success" type="stream">
77 * <param name="contentType">image/jpeg</param>
78 * <param name="inputName">imageStream</param>
79 * <param name="contentDisposition">filename="document.pdf"</param>
80 * <param name="bufferSize">1024</param>
81 * </result>
82 * <!-- END SNIPPET: example --></pre>
83 *
84 */
85 public class StreamResult extends StrutsResultSupport {
86
87 private static final long serialVersionUID = -1468409635999059850L;
88
89 protected static final Logger LOG = LoggerFactory.getLogger(StreamResult.class);
90
91 public static final String DEFAULT_PARAM = "inputName";
92
93 protected String contentType = "text/plain";
94 protected String contentLength;
95 protected String contentDisposition = "inline";
96 protected String inputName = "inputStream";
97 protected InputStream inputStream;
98 protected int bufferSize = 1024;
99 protected boolean allowCaching = true;
100
101 public StreamResult() {
102 super();
103 }
104
105 public StreamResult(InputStream in) {
106 this.inputStream = in;
107 }
108
109 /***
110 * @return Returns the whether or not the client should be requested to allow caching of the data stream.
111 */
112 public boolean getAllowCaching() {
113 return allowCaching;
114 }
115
116 /***
117 * Set allowCaching to <tt>false</tt> to indicate that the client should be requested not to cache the data stream.
118 * This is set to <tt>false</tt> by default
119 *
120 * @param allowCaching Enable caching.
121 */
122 public void setAllowCaching(boolean allowCaching) {
123 this.allowCaching = allowCaching;
124 }
125
126
127 /***
128 * @return Returns the bufferSize.
129 */
130 public int getBufferSize() {
131 return (bufferSize);
132 }
133
134 /***
135 * @param bufferSize The bufferSize to set.
136 */
137 public void setBufferSize(int bufferSize) {
138 this.bufferSize = bufferSize;
139 }
140
141 /***
142 * @return Returns the contentType.
143 */
144 public String getContentType() {
145 return (contentType);
146 }
147
148 /***
149 * @param contentType The contentType to set.
150 */
151 public void setContentType(String contentType) {
152 this.contentType = contentType;
153 }
154
155 /***
156 * @return Returns the contentLength.
157 */
158 public String getContentLength() {
159 return contentLength;
160 }
161
162 /***
163 * @param contentLength The contentLength to set.
164 */
165 public void setContentLength(String contentLength) {
166 this.contentLength = contentLength;
167 }
168
169 /***
170 * @return Returns the Content-disposition header value.
171 */
172 public String getContentDisposition() {
173 return contentDisposition;
174 }
175
176 /***
177 * @param contentDisposition the Content-disposition header value to use.
178 */
179 public void setContentDisposition(String contentDisposition) {
180 this.contentDisposition = contentDisposition;
181 }
182
183 /***
184 * @return Returns the inputName.
185 */
186 public String getInputName() {
187 return (inputName);
188 }
189
190 /***
191 * @param inputName The inputName to set.
192 */
193 public void setInputName(String inputName) {
194 this.inputName = inputName;
195 }
196
197 /***
198 * @see org.apache.struts2.dispatcher.StrutsResultSupport#doExecute(java.lang.String, com.opensymphony.xwork2.ActionInvocation)
199 */
200 protected void doExecute(String finalLocation, ActionInvocation invocation) throws Exception {
201
202
203 resolveParamsFromStack(invocation.getStack());
204
205 OutputStream oOutput = null;
206
207 try {
208 if (inputStream == null) {
209
210 inputStream = (InputStream) invocation.getStack().findValue(conditionalParse(inputName, invocation));
211 }
212
213 if (inputStream == null) {
214 String msg = ("Can not find a java.io.InputStream with the name [" + inputName + "] in the invocation stack. " +
215 "Check the <param name=\"inputName\"> tag specified for this action.");
216 LOG.error(msg);
217 throw new IllegalArgumentException(msg);
218 }
219
220
221 HttpServletResponse oResponse = (HttpServletResponse) invocation.getInvocationContext().get(HTTP_RESPONSE);
222
223
224 oResponse.setContentType(conditionalParse(contentType, invocation));
225
226
227 if (contentLength != null) {
228 String _contentLength = conditionalParse(contentLength, invocation);
229 int _contentLengthAsInt = -1;
230 try {
231 _contentLengthAsInt = Integer.parseInt(_contentLength);
232 if (_contentLengthAsInt >= 0) {
233 oResponse.setContentLength(_contentLengthAsInt);
234 }
235 }
236 catch(NumberFormatException e) {
237 LOG.warn("failed to recongnize "+_contentLength+" as a number, contentLength header will not be set", e);
238 }
239 }
240
241
242 if (contentDisposition != null) {
243 oResponse.addHeader("Content-Disposition", conditionalParse(contentDisposition, invocation));
244 }
245
246
247 if (!allowCaching) {
248 oResponse.addHeader("Pragma", "no-cache");
249 oResponse.addHeader("Cache-Control", "no-cache");
250 }
251
252
253 oOutput = oResponse.getOutputStream();
254
255 if (LOG.isDebugEnabled()) {
256 LOG.debug("Streaming result [" + inputName + "] type=[" + contentType + "] length=[" + contentLength +
257 "] content-disposition=[" + contentDisposition + "]");
258 }
259
260
261 LOG.debug("Streaming to output buffer +++ START +++");
262 byte[] oBuff = new byte[bufferSize];
263 int iSize;
264 while (-1 != (iSize = inputStream.read(oBuff))) {
265 oOutput.write(oBuff, 0, iSize);
266 }
267 LOG.debug("Streaming to output buffer +++ END +++");
268
269
270 oOutput.flush();
271 }
272 finally {
273 if (inputStream != null) inputStream.close();
274 if (oOutput != null) oOutput.close();
275 }
276 }
277
278 /***
279 * Tries to lookup the parameters on the stack. Will override any existing parameters
280 *
281 * @param stack The current value stack
282 */
283 protected void resolveParamsFromStack(ValueStack stack) {
284 String disposition = stack.findString("contentDisposition");
285 if (disposition != null) {
286 setContentDisposition(disposition);
287 }
288
289 String contentType = stack.findString("contentType");
290 if (contentType != null) {
291 setContentType(contentType);
292 }
293
294 String inputName = stack.findString("inputName");
295 if (inputName != null) {
296 setInputName(inputName);
297 }
298
299 String contentLength = stack.findString("contentLength");
300 if (contentLength != null) {
301 setContentLength(contentLength);
302 }
303
304 Integer bufferSize = (Integer) stack.findValue("bufferSize", Integer.class);
305 if (bufferSize != null) {
306 setBufferSize(bufferSize.intValue());
307 }
308 }
309
310 }