Index: stripes/src/net/sourceforge/stripes/action/StreamingResolution.java =================================================================== --- stripes/src/net/sourceforge/stripes/action/StreamingResolution.java (revision 1127) +++ stripes/src/net/sourceforge/stripes/action/StreamingResolution.java Fri Apr 10 13:32:59 EDT 2009 @@ -25,7 +25,11 @@ import java.io.PrintWriter; import java.io.Reader; import java.io.StringReader; +import java.util.Date; +import java.util.Locale; +import java.text.SimpleDateFormat; + /** *
Resolution for streaming data back to the client (in place of forwarding the user to * another page). Designed to be used for streaming non-page data such as generated images/charts @@ -60,6 +64,9 @@ private String filename; private String contentType; private String characterEncoding; + private long lastModified; + private long length; + private boolean attachment = true; /** * Constructor only to be used when subclassing the StreamingResolution (usually using @@ -122,6 +129,47 @@ } /** + * Sets the modification-date timestamp. If this property is set, the browser may be able to + * apply it to the downloaded file. If this property is unset, the modification-date parameter + * will be omitted. + * + * @param lastModified The date-time (as a long) that the file was last modified. Optional. + * @return StreamingResolution so that this method call can be chained to the constructor and + * returned. + */ + public StreamingResolution setLastModified(long lastModified) { + this.lastModified = lastModified; + return this; + } + + /** + * Sets the file length. If this property is set, the file size will be reported in the HTTP + * header. This may help with file download progress indicators. If this property is unset, the + * size parameter will be omitted. + * + * @param length The length of the file in bytes. + * @return StreamingResolution so that this method call can be chained to the constructor and + * returned. + */ + public StreamingResolution setLength(long length) { + this.length = length; + return this; + } + + /** + * Indicates whether to use content-disposition attachment headers or not. (Defaults to true). + * + * @param attachment Whether the content should be treated as an attachment, or a direct + * download. + * @return StreamingResolution so that this method call can be chained to the constructor and + * returned. + */ + public StreamingResolution setAttachment(boolean attachment) { + this.attachment = attachment; + return this; + } + + /** * Sets the character encoding that will be set on the request when executing this * resolution. If none is set, then the current character encoding (either the one * selected by the LocalePicker or the container default one) will be used. @@ -134,28 +182,67 @@ /** * Streams data from the InputStream or Reader to the response's OutputStream or PrinterWriter, - * using a moderately sized buffer to ensure that the operation is reasonable efficient. - * Once the InputStream or Reader signaled the end of the stream, close() is called on it. + * using a moderately sized buffer to ensure that the operation is reasonable efficient. Once + * the InputStream or Reader signaled the end of the stream, close() is called on it. * * @param request the HttpServletRequest being processed * @param response the paired HttpServletResponse * @throws IOException if there is a problem accessing one of the streams or reader/writer - * objects used. + * objects used. */ - final public void execute(HttpServletRequest request, HttpServletResponse response) throws Exception { + final public void execute(HttpServletRequest request, HttpServletResponse response) throws + Exception { + applyHeaders(response); + stream(response); + } + + /** + * Sets the response headers, based on what is known about the file or stream being handled. + * + * @param response the current HttpServletResponse + */ + protected void applyHeaders(HttpServletResponse response) { response.setContentType(this.contentType); - if (this.characterEncoding != null) response.setCharacterEncoding(characterEncoding); + if (this.characterEncoding != null) { + response.setCharacterEncoding(characterEncoding); + } - // If a filename was specified, set the appropriate header - if (this.filename != null) { + // If properties were specified, set the appropriate header + if(attachment) { + // For Content-Disposition spec, see http://www.ietf.org/rfc/rfc2183.txt + if (filename != null || lastModified > 0 || length > 0) { + StringBuilder contentDisposition = new StringBuilder(); + contentDisposition.append("attachment; "); + if (filename != null) { - // Value of filename should be RFC 2047 encoded here (see RFC 2616) but few browsers - // support that, so just escape the quote for now - String escaped = this.filename.replace("\"", "\\\""); + // Value of filename should be RFC 2047 encoded here (see RFC 2616) but few browsers + // support that, so just escape the quote for now + String escaped = this.filename.replace("\"", "\\\""); - response.setHeader("Content-Disposition", "attachment; filename=\"" + escaped + "\""); + contentDisposition.append("filename=\"").append(escaped).append("\"; "); - } + } - - stream(response); + if (lastModified > 0) { + SimpleDateFormat rfc822Date = + new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss Z", Locale.getDefault()); + Date lastModifiedDate = new Date(lastModified); + contentDisposition.append("modification-date=\"") + .append(rfc822Date.format(lastModifiedDate)).append("\"; "); - } + } + if (length > 0) { + contentDisposition.append("size=").append(length).append("; "); + } + response.setHeader("Content-Disposition", contentDisposition.toString()); + } + } else { + if (lastModified > 0) { + response.setDateHeader("Last-Modified", lastModified); + } + if (length > 0) { + //Odd that ServletResponse.setContentLength is limited to int. + //requires downcast from long to int e.g. response.setContentLength((int)length); + //Workaround to allow large files: + response.addHeader("Content-Length", Long.toString(length)); + } + } + } /** *