View Javadoc

1   /*
2    * $Id: StreamResult.java 817366 2009-09-21 18:40:56Z wesw $
3    *
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *  http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
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   * &lt;result name="success" type="stream"&gt;
82   *   &lt;param name="contentType"&gt;image/jpeg&lt;/param&gt;
83   *   &lt;param name="inputName"&gt;imageStream&lt;/param&gt;
84   *   &lt;param name="contentDisposition"&gt;attachment;filename="document.pdf"&lt;/param&gt;
85   *   &lt;param name="bufferSize"&gt;1024&lt;/param&gt;
86   * &lt;/result&gt;
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         // Override any parameters using values on the stack
223         resolveParamsFromStack(invocation.getStack(), invocation);
224 
225         OutputStream oOutput = null;
226 
227         try {
228             if (inputStream == null) {
229                 // Find the inputstream from the invocation variable stack
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             // Find the Response in context
241             HttpServletResponse oResponse = (HttpServletResponse) invocation.getInvocationContext().get(HTTP_RESPONSE);
242 
243             // Set the content type
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             // Set the content length
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             // Set the content-disposition
267             if (contentDisposition != null) {
268                 oResponse.addHeader("Content-Disposition", conditionalParse(contentDisposition, invocation));
269             }
270 
271             // Set the cache control headers if neccessary
272             if (!allowCaching) {
273                 oResponse.addHeader("Pragma", "no-cache");
274                 oResponse.addHeader("Cache-Control", "no-cache");
275             }
276 
277             // Get the outputstream
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             // Copy input to output
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             // Flush
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 }