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>attachment;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 *
67 * <li><b>contentCharSet</b> if set to a string, ';charset=value' will be added to the
68 * content-type header, where value is the string set. If set to an expression, the result
69 * of evaluating the expression will be used. If not set, then no charset will be set on
70 * the header</li>
71 * </ul>
72 *
73 * <p>These parameters can also be set by exposing a similarly named getter method on your Action. For example, you can
74 * provide <code>getContentType()</code> to override that parameter for the current action.</p>
75 *
76 * <!-- END SNIPPET: params -->
77 *
78 * <b>Example:</b>
79 *
80 * <pre><!-- START SNIPPET: example -->
81 * <result name="success" type="stream">
82 * <param name="contentType">image/jpeg</param>
83 * <param name="inputName">imageStream</param>
84 * <param name="contentDisposition">attachment;filename="document.pdf"</param>
85 * <param name="bufferSize">1024</param>
86 * </result>
87 * <!-- END SNIPPET: example --></pre>
88 *
89 */
90 public class StreamResult extends StrutsResultSupport {
91
92 private static final long serialVersionUID = -1468409635999059850L;
93
94 protected static final Logger LOG = LoggerFactory.getLogger(StreamResult.class);
95
96 public static final String DEFAULT_PARAM = "inputName";
97
98 protected String contentType = "text/plain";
99 protected String contentLength;
100 protected String contentDisposition = "inline";
101 protected String contentCharSet ;
102 protected String inputName = "inputStream";
103 protected InputStream inputStream;
104 protected int bufferSize = 1024;
105 protected boolean allowCaching = true;
106
107 public StreamResult() {
108 super();
109 }
110
111 public StreamResult(InputStream in) {
112 this.inputStream = in;
113 }
114
115 /***
116 * @return Returns the whether or not the client should be requested to allow caching of the data stream.
117 */
118 public boolean getAllowCaching() {
119 return allowCaching;
120 }
121
122 /***
123 * Set allowCaching to <tt>false</tt> to indicate that the client should be requested not to cache the data stream.
124 * This is set to <tt>false</tt> by default
125 *
126 * @param allowCaching Enable caching.
127 */
128 public void setAllowCaching(boolean allowCaching) {
129 this.allowCaching = allowCaching;
130 }
131
132
133 /***
134 * @return Returns the bufferSize.
135 */
136 public int getBufferSize() {
137 return (bufferSize);
138 }
139
140 /***
141 * @param bufferSize The bufferSize to set.
142 */
143 public void setBufferSize(int bufferSize) {
144 this.bufferSize = bufferSize;
145 }
146
147 /***
148 * @return Returns the contentType.
149 */
150 public String getContentType() {
151 return (contentType);
152 }
153
154 /***
155 * @param contentType The contentType to set.
156 */
157 public void setContentType(String contentType) {
158 this.contentType = contentType;
159 }
160
161 /***
162 * @return Returns the contentLength.
163 */
164 public String getContentLength() {
165 return contentLength;
166 }
167
168 /***
169 * @param contentLength The contentLength to set.
170 */
171 public void setContentLength(String contentLength) {
172 this.contentLength = contentLength;
173 }
174
175 /***
176 * @return Returns the Content-disposition header value.
177 */
178 public String getContentDisposition() {
179 return contentDisposition;
180 }
181
182 /***
183 * @param contentDisposition the Content-disposition header value to use.
184 */
185 public void setContentDisposition(String contentDisposition) {
186 this.contentDisposition = contentDisposition;
187 }
188
189 /***
190 * @return Returns the charset specified by the user
191 */
192 public String getContentCharSet() {
193 return contentCharSet;
194 }
195
196 /***
197 * @param contentCharSet the charset to use on the header when sending the stream
198 */
199 public void setContentCharSet(String contentCharSet) {
200 this.contentCharSet = contentCharSet;
201 }
202
203 /***
204 * @return Returns the inputName.
205 */
206 public String getInputName() {
207 return (inputName);
208 }
209
210 /***
211 * @param inputName The inputName to set.
212 */
213 public void setInputName(String inputName) {
214 this.inputName = inputName;
215 }
216
217 /***
218 * @see org.apache.struts2.dispatcher.StrutsResultSupport#doExecute(java.lang.String, com.opensymphony.xwork2.ActionInvocation)
219 */
220 protected void doExecute(String finalLocation, ActionInvocation invocation) throws Exception {
221
222
223 resolveParamsFromStack(invocation.getStack(), invocation);
224
225 OutputStream oOutput = null;
226
227 try {
228 if (inputStream == null) {
229
230 inputStream = (InputStream) invocation.getStack().findValue(conditionalParse(inputName, invocation));
231 }
232
233 if (inputStream == null) {
234 String msg = ("Can not find a java.io.InputStream with the name [" + inputName + "] in the invocation stack. " +
235 "Check the <param name=\"inputName\"> tag specified for this action.");
236 LOG.error(msg);
237 throw new IllegalArgumentException(msg);
238 }
239
240
241 HttpServletResponse oResponse = (HttpServletResponse) invocation.getInvocationContext().get(HTTP_RESPONSE);
242
243
244 if (contentCharSet != null && ! contentCharSet.equals("")) {
245 oResponse.setContentType(conditionalParse(contentType, invocation)+";charset="+contentCharSet);
246 }
247 else {
248 oResponse.setContentType(conditionalParse(contentType, invocation));
249 }
250
251
252 if (contentLength != null) {
253 String _contentLength = conditionalParse(contentLength, invocation);
254 int _contentLengthAsInt = -1;
255 try {
256 _contentLengthAsInt = Integer.parseInt(_contentLength);
257 if (_contentLengthAsInt >= 0) {
258 oResponse.setContentLength(_contentLengthAsInt);
259 }
260 }
261 catch(NumberFormatException e) {
262 LOG.warn("failed to recongnize "+_contentLength+" as a number, contentLength header will not be set", e);
263 }
264 }
265
266
267 if (contentDisposition != null) {
268 oResponse.addHeader("Content-Disposition", conditionalParse(contentDisposition, invocation));
269 }
270
271
272 if (!allowCaching) {
273 oResponse.addHeader("Pragma", "no-cache");
274 oResponse.addHeader("Cache-Control", "no-cache");
275 }
276
277
278 oOutput = oResponse.getOutputStream();
279
280 if (LOG.isDebugEnabled()) {
281 LOG.debug("Streaming result [" + inputName + "] type=[" + contentType + "] length=[" + contentLength +
282 "] content-disposition=[" + contentDisposition + "] charset=[" + contentCharSet + "]");
283 }
284
285
286 LOG.debug("Streaming to output buffer +++ START +++");
287 byte[] oBuff = new byte[bufferSize];
288 int iSize;
289 while (-1 != (iSize = inputStream.read(oBuff))) {
290 oOutput.write(oBuff, 0, iSize);
291 }
292 LOG.debug("Streaming to output buffer +++ END +++");
293
294
295 oOutput.flush();
296 }
297 finally {
298 if (inputStream != null) inputStream.close();
299 if (oOutput != null) oOutput.close();
300 }
301 }
302
303 /***
304 * Tries to lookup the parameters on the stack. Will override any existing parameters
305 *
306 * @param stack The current value stack
307 */
308 protected void resolveParamsFromStack(ValueStack stack, ActionInvocation invocation) {
309 String disposition = stack.findString("contentDisposition");
310 if (disposition != null) {
311 setContentDisposition(disposition);
312 }
313
314 String contentType = stack.findString("contentType");
315 if (contentType != null) {
316 setContentType(contentType);
317 }
318
319 String inputName = stack.findString("inputName");
320 if (inputName != null) {
321 setInputName(inputName);
322 }
323
324 String contentLength = stack.findString("contentLength");
325 if (contentLength != null) {
326 setContentLength(contentLength);
327 }
328
329 Integer bufferSize = (Integer) stack.findValue("bufferSize", Integer.class);
330 if (bufferSize != null) {
331 setBufferSize(bufferSize.intValue());
332 }
333
334 if (contentCharSet != null ) {
335 contentCharSet = conditionalParse(contentCharSet, invocation);
336 }
337 else {
338 contentCharSet = stack.findString("contentCharSet");
339 }
340 }
341
342 }