View Javadoc

1   /*
2    * $Id: StreamResult.java 722124 2008-12-01 16:47:29Z musachy $
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>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   * &lt;result name="success" type="stream"&gt;
77   *   &lt;param name="contentType"&gt;image/jpeg&lt;/param&gt;
78   *   &lt;param name="inputName"&gt;imageStream&lt;/param&gt;
79   *   &lt;param name="contentDisposition"&gt;filename="document.pdf"&lt;/param&gt;
80   *   &lt;param name="bufferSize"&gt;1024&lt;/param&gt;
81   * &lt;/result&gt;
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         // Override any parameters using values on the stack
203         resolveParamsFromStack(invocation.getStack());
204 
205         OutputStream oOutput = null;
206 
207         try {
208             if (inputStream == null) {
209                 // Find the inputstream from the invocation variable stack
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             // Find the Response in context
221             HttpServletResponse oResponse = (HttpServletResponse) invocation.getInvocationContext().get(HTTP_RESPONSE);
222 
223             // Set the content type
224             oResponse.setContentType(conditionalParse(contentType, invocation));
225 
226             // Set the content length
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             // Set the content-disposition
242             if (contentDisposition != null) {
243                 oResponse.addHeader("Content-Disposition", conditionalParse(contentDisposition, invocation));
244             }
245 
246             // Set the cache control headers if neccessary
247             if (!allowCaching) {
248                 oResponse.addHeader("Pragma", "no-cache");
249                 oResponse.addHeader("Cache-Control", "no-cache");
250             }
251 
252             // Get the outputstream
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             // Copy input to output
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             // Flush
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 }