diff --git a/source/java/org/alfresco/repo/web/scripts/content/ContentInfo.java b/source/java/org/alfresco/repo/web/scripts/content/ContentInfo.java index 71bc7ea30b..1db104cf3d 100644 --- a/source/java/org/alfresco/repo/web/scripts/content/ContentInfo.java +++ b/source/java/org/alfresco/repo/web/scripts/content/ContentInfo.java @@ -63,7 +63,9 @@ public class ContentInfo extends StreamContent } @Override - protected void streamContentImpl(WebScriptRequest req, WebScriptResponse res, ContentReader reader, boolean attach, Date modified, String eTag, String attachFileName) + protected void streamContentImpl(WebScriptRequest req, WebScriptResponse res, + ContentReader reader, NodeRef nodeRef, QName propertyQName, + boolean attach, Date modified, String eTag, String attachFileName) throws IOException { setAttachment(res, attach, attachFileName); diff --git a/source/java/org/alfresco/repo/web/scripts/content/StreamContent.java b/source/java/org/alfresco/repo/web/scripts/content/StreamContent.java index 7097a7104f..7d1c615af1 100644 --- a/source/java/org/alfresco/repo/web/scripts/content/StreamContent.java +++ b/source/java/org/alfresco/repo/web/scripts/content/StreamContent.java @@ -36,6 +36,7 @@ import javax.servlet.http.HttpServletResponse; import org.alfresco.model.ContentModel; import org.alfresco.repo.content.MimetypeMap; import org.alfresco.repo.content.filestore.FileContentReader; +import org.alfresco.repo.web.util.HttpRangeProcessor; import org.alfresco.repo.webdav.WebDAVHelper; import org.alfresco.service.cmr.repository.ContentIOException; import org.alfresco.service.cmr.repository.ContentReader; @@ -75,6 +76,12 @@ public class StreamContent extends AbstractWebScript implements ResourceLoaderAw * format definied by RFC 822, see http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3 */ private static final SimpleDateFormat dateFormat = new SimpleDateFormat("EEE', 'dd' 'MMM' 'yyyy' 'HH:mm:ss' 'Z", Locale.US); + + private static final String HEADER_CONTENT_RANGE = "Content-Range"; + private static final String HEADER_CONTENT_LENGTH = "Content-Length"; + private static final String HEADER_ACCEPT_RANGES = "Accept-Ranges"; + private static final String HEADER_RANGE = "Range"; + private static final String HEADER_USER_AGENT = "User-Agent"; /** Services */ protected PermissionService permissionService; @@ -430,7 +437,7 @@ public class StreamContent extends AbstractWebScript implements ResourceLoaderAw } // Stream the content - streamContentImpl(req, res, reader, attach, modified, modified == null ? null : String.valueOf(modified.getTime()), attachFileName, model); + streamContentImpl(req, res, reader, nodeRef, propertyQName, attach, modified, modified == null ? null : String.valueOf(modified.getTime()), attachFileName, model); } /** @@ -668,7 +675,7 @@ public class StreamContent extends AbstractWebScript implements ResourceLoaderAw long lastModified = modifiedTime == null ? file.lastModified() : modifiedTime; Date lastModifiedDate = new Date(lastModified); - streamContentImpl(req, res, reader, attach, lastModifiedDate, String.valueOf(lastModifiedDate.getTime()), attachFileName, model); + streamContentImpl(req, res, reader, null, null, attach, lastModifiedDate, String.valueOf(lastModifiedDate.getTime()), attachFileName, model); } /** @@ -677,6 +684,8 @@ public class StreamContent extends AbstractWebScript implements ResourceLoaderAw * @param req The request * @param res The response * @param reader The reader + * @param nodeRef The content nodeRef if applicable + * @param propertyQName The content property if applicable * @param attach Indicates whether the content should be streamed as an attachment or not * @param modified Modified date of content * @param eTag ETag to use @@ -686,12 +695,14 @@ public class StreamContent extends AbstractWebScript implements ResourceLoaderAw protected void streamContentImpl(WebScriptRequest req, WebScriptResponse res, ContentReader reader, + NodeRef nodeRef, + QName propertyQName, boolean attach, Date modified, String eTag, String attachFileName) throws IOException { - streamContentImpl(req, res, reader, attach, modified, eTag, attachFileName, null); + streamContentImpl(req, res, reader, nodeRef, propertyQName, attach, modified, eTag, attachFileName, null); } /** * Stream content implementation @@ -699,6 +710,8 @@ public class StreamContent extends AbstractWebScript implements ResourceLoaderAw * @param req The request * @param res The response * @param reader The reader + * @param nodeRef The content nodeRef if applicable + * @param propertyQName The content property if applicable * @param attach Indicates whether the content should be streamed as an attachment or not * @param modified Modified date of content * @param eTag ETag to use @@ -708,6 +721,8 @@ public class StreamContent extends AbstractWebScript implements ResourceLoaderAw protected void streamContentImpl(WebScriptRequest req, WebScriptResponse res, ContentReader reader, + NodeRef nodeRef, + QName propertyQName, boolean attach, Date modified, String eTag, @@ -730,20 +745,58 @@ public class StreamContent extends AbstractWebScript implements ResourceLoaderAw } } - // set mimetype for the content and the character encoding + length for the stream - res.setContentType(mimetype); - res.setContentEncoding(reader.getEncoding()); - res.setHeader("Content-Length", Long.toString(reader.getSize())); - - // set caching - setResponseCache(res, modified, eTag, model); - - // get the content and stream directly to the response output stream - // assuming the repository is capable of streaming in chunks, this should allow large files - // to be streamed directly to the browser response stream. + res.setHeader(HEADER_ACCEPT_RANGES, "bytes"); try { - reader.getContent(res.getOutputStream()); + boolean processedRange = false; + String range = req.getHeader(HEADER_CONTENT_RANGE); + if (range == null) + { + range = req.getHeader(HEADER_RANGE); + } + if (range != null) + { + if (logger.isDebugEnabled()) + logger.debug("Found content range header: " + range); + + // ensure the range header is starts with "bytes=" and process the range(s) + if (range.length() > 6) + { + if (range.indexOf(',') != -1 && (nodeRef == null || propertyQName == null)) + { + if (logger.isInfoEnabled()) + logger.info("Multi-range only supported for nodeRefs"); + } + else { + HttpRangeProcessor rangeProcessor = new HttpRangeProcessor(contentService); + processedRange = rangeProcessor.processRange( + res, reader, range.substring(6), nodeRef, propertyQName, + mimetype, req.getHeader(HEADER_USER_AGENT)); + } + } + } + if (processedRange == false) + { + if (logger.isDebugEnabled()) + logger.debug("Sending complete file content..."); + + // set mimetype for the content and the character encoding for the stream + res.setContentType(mimetype); + res.setContentEncoding(reader.getEncoding()); + + // return the complete entity range + long size = reader.getSize(); + res.setHeader(HEADER_CONTENT_RANGE, "bytes 0-" + Long.toString(size-1L) + "/" + Long.toString(size)); + res.setHeader(HEADER_CONTENT_LENGTH, Long.toString(size)); + + // set caching + setResponseCache(res, modified, eTag, model); + + // get the content and stream directly to the response output stream + // assuming the repository is capable of streaming in chunks, this should allow large files + // to be streamed directly to the browser response stream. + reader.getContent( res.getOutputStream() ); + } } catch (SocketException e1) { diff --git a/source/java/org/alfresco/repo/web/scripts/solr/NodeContentGet.java b/source/java/org/alfresco/repo/web/scripts/solr/NodeContentGet.java index 986eedf6bd..3cad025a5f 100644 --- a/source/java/org/alfresco/repo/web/scripts/solr/NodeContentGet.java +++ b/source/java/org/alfresco/repo/web/scripts/solr/NodeContentGet.java @@ -227,7 +227,7 @@ public class NodeContentGet extends StreamContent else { res.setStatus(HttpStatus.SC_OK); - streamContentImpl(req, res, textReader, false, modified, String.valueOf(modified.getTime()), null); + streamContentImpl(req, res, textReader, null, null, false, modified, String.valueOf(modified.getTime()), null); } } finally diff --git a/source/java/org/alfresco/repo/web/util/HttpRangeProcessor.java b/source/java/org/alfresco/repo/web/util/HttpRangeProcessor.java index c2c3f99976..84e1fd44ff 100644 --- a/source/java/org/alfresco/repo/web/util/HttpRangeProcessor.java +++ b/source/java/org/alfresco/repo/web/util/HttpRangeProcessor.java @@ -20,6 +20,7 @@ package org.alfresco.repo.web.util; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -34,6 +35,7 @@ import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.namespace.QName; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.WebScriptResponse; /** * Generates HTTP response for "Range" scoped HTTP requests for content. @@ -64,7 +66,17 @@ public class HttpRangeProcessor } /** - * Process a range header - handles single and multiple range requests. + * Process a range header for a HttpServletResponse - handles single and multiple range requests. + * + * @param res the HTTP servlet response + * @param reader the content reader + * @param range the byte range + * @param ref the content NodeRef + * @param property the content property + * @param mimetype the content mimetype + * @param userAgent the user agent string + * @return whether or not the range could be processed + * @throws IOException */ public boolean processRange(HttpServletResponse res, ContentReader reader, String range, NodeRef ref, QName property, String mimetype, String userAgent) @@ -80,6 +92,34 @@ public class HttpRangeProcessor return processMultiRange(res, range, ref, property, mimetype, userAgent); } } + + /** + * Process a range header for a WebScriptResponse - handles single and multiple range requests. + * + * @param res the webscript response + * @param reader the content reader + * @param range the byte range + * @param ref the content NodeRef + * @param property the content property + * @param mimetype the content mimetype + * @param userAgent the user agent string + * @return whether or not the range could be processed + * @throws IOException + */ + public boolean processRange(WebScriptResponse res, ContentReader reader, String range, + NodeRef ref, QName property, String mimetype, String userAgent) + throws IOException + { + // test for multiple byte ranges present in header + if (range.indexOf(',') == -1) + { + return processSingleRange(res, reader, range, mimetype); + } + else + { + return processMultiRange(res, range, ref, property, mimetype, userAgent); + } + } /** * Process a single range request. @@ -91,9 +131,26 @@ public class HttpRangeProcessor * * @return true if processed range, false otherwise */ - private boolean processSingleRange(HttpServletResponse res, ContentReader reader, String range, String mimetype) + private boolean processSingleRange(Object res, ContentReader reader, String range, String mimetype) throws IOException { + // Handle either HttpServletResponse or WebScriptResponse + HttpServletResponse httpServletResponse = null; + WebScriptResponse webScriptResponse = null; + if (res instanceof HttpServletResponse) + { + httpServletResponse = (HttpServletResponse) res; + } + else if (res instanceof WebScriptResponse) + { + webScriptResponse = (WebScriptResponse) res; + } + if (httpServletResponse == null && webScriptResponse == null) + { + // Unknown response object type + return false; + } + // return the specific set of bytes as requested in the content-range header /* Examples of byte-content-range-spec values, assuming that the entity contains total of 1234 bytes: @@ -117,18 +174,38 @@ public class HttpRangeProcessor if (getLogger().isDebugEnabled()) getLogger().debug("Failed to parse range header - returning 416 status code: " + err.getMessage()); - res.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); - res.setHeader(HEADER_CONTENT_RANGE, "\"*\""); - res.getOutputStream().close(); + if (httpServletResponse != null) + { + httpServletResponse.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); + httpServletResponse.setHeader(HEADER_CONTENT_RANGE, "\"*\""); + httpServletResponse.getOutputStream().close(); + } + else if (webScriptResponse != null) + { + webScriptResponse.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); + webScriptResponse.setHeader(HEADER_CONTENT_RANGE, "\"*\""); + webScriptResponse.getOutputStream().close(); + } return true; } // set Partial Content status and range headers - res.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); - res.setContentType(mimetype); - String contentRange = "bytes " + Long.toString(r.start) + "-" + Long.toString(r.end) + "/" + Long.toString(reader.getSize()); - res.setHeader(HEADER_CONTENT_RANGE, contentRange); - res.setHeader(HEADER_CONTENT_LENGTH, Long.toString((r.end - r.start) + 1L)); + String contentRange = "bytes " + Long.toString(r.start) + + "-" + Long.toString(r.end) + "/" + Long.toString(reader.getSize()); + if (httpServletResponse != null) + { + httpServletResponse.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); + httpServletResponse.setContentType(mimetype); + httpServletResponse.setHeader(HEADER_CONTENT_RANGE, contentRange); + httpServletResponse.setHeader(HEADER_CONTENT_LENGTH, Long.toString((r.end - r.start) + 1L)); + } + else if (webScriptResponse != null) + { + webScriptResponse.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); + webScriptResponse.setContentType(mimetype); + webScriptResponse.setHeader(HEADER_CONTENT_RANGE, contentRange); + webScriptResponse.setHeader(HEADER_CONTENT_LENGTH, Long.toString((r.end - r.start) + 1L)); + } if (getLogger().isDebugEnabled()) getLogger().debug("Processing: Content-Range: " + contentRange); @@ -137,7 +214,15 @@ public class HttpRangeProcessor try { // output the binary data for the range - ServletOutputStream os = res.getOutputStream(); + OutputStream os = null; + if (httpServletResponse != null) + { + os = httpServletResponse.getOutputStream(); + } + else if (webScriptResponse != null) + { + os = webScriptResponse.getOutputStream(); + } is = reader.getContentInputStream(); streamRangeBytes(r, is, os, 0L); @@ -172,11 +257,28 @@ public class HttpRangeProcessor * @return true if processed range, false otherwise */ private boolean processMultiRange( - HttpServletResponse res, String range, NodeRef ref, QName property, String mimetype, String userAgent) + Object res, String range, NodeRef ref, QName property, String mimetype, String userAgent) throws IOException { final Log logger = getLogger(); + // Handle either HttpServletResponse or WebScriptResponse + HttpServletResponse httpServletResponse = null; + WebScriptResponse webScriptResponse = null; + if (res instanceof HttpServletResponse) + { + httpServletResponse = (HttpServletResponse) res; + } + else if (res instanceof WebScriptResponse) + { + webScriptResponse = (WebScriptResponse) res; + } + if (httpServletResponse == null && webScriptResponse == null) + { + // Unknown response object type + return false; + } + // return the sets of bytes as requested in the content-range header // the response will be formatted as multipart/byteranges media type message @@ -211,9 +313,18 @@ public class HttpRangeProcessor if (getLogger().isDebugEnabled()) getLogger().debug("Failed to parse range header - returning 416 status code: " + err.getMessage()); - res.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); - res.setHeader(HEADER_CONTENT_RANGE, "\"*\""); - res.getOutputStream().close(); + if (httpServletResponse != null) + { + httpServletResponse.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); + httpServletResponse.setHeader(HEADER_CONTENT_RANGE, "\"*\""); + httpServletResponse.getOutputStream().close(); + } + else if (webScriptResponse != null) + { + webScriptResponse.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); + webScriptResponse.setHeader(HEADER_CONTENT_RANGE, "\"*\""); + webScriptResponse.getOutputStream().close(); + } return true; } } @@ -257,11 +368,21 @@ public class HttpRangeProcessor } // output headers as we have at least one range to process - res.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); - res.setHeader(HEADER_CONTENT_TYPE, MULTIPART_BYTERANGES_HEADER); - res.setHeader(HEADER_CONTENT_LENGTH, Long.toString(length)); - - ServletOutputStream os = res.getOutputStream(); + OutputStream os = null; + if (httpServletResponse != null) + { + httpServletResponse.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); + httpServletResponse.setHeader(HEADER_CONTENT_TYPE, MULTIPART_BYTERANGES_HEADER); + httpServletResponse.setHeader(HEADER_CONTENT_LENGTH, Long.toString(length)); + os = httpServletResponse.getOutputStream(); + } + else if (webScriptResponse != null) + { + webScriptResponse.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); + webScriptResponse.setHeader(HEADER_CONTENT_TYPE, MULTIPART_BYTERANGES_HEADER); + webScriptResponse.setHeader(HEADER_CONTENT_LENGTH, Long.toString(length)); + os =webScriptResponse.getOutputStream(); + } InputStream is = null; try @@ -274,7 +395,8 @@ public class HttpRangeProcessor try { // output the header bytes for the range - r.outputHeader(os); + if (os instanceof ServletOutputStream) + r.outputHeader((ServletOutputStream) os); // output the binary data for the range // need a new reader for each new InputStream @@ -284,7 +406,8 @@ public class HttpRangeProcessor is = null; // section marker and flush stream - os.println(); + if (os instanceof ServletOutputStream) + ((ServletOutputStream) os).println(); os.flush(); } catch (IOException err) @@ -304,7 +427,8 @@ public class HttpRangeProcessor } // end marker - os.println(MULTIPART_BYTERANGES_BOUNDRY_END); + if (os instanceof ServletOutputStream) + ((ServletOutputStream) os).println(MULTIPART_BYTERANGES_BOUNDRY_END); os.close(); processedRange = true; } @@ -322,7 +446,7 @@ public class HttpRangeProcessor * * @return current InputStream position - so the stream can be reused if required */ - private void streamRangeBytes(final Range r, final InputStream is, final ServletOutputStream os, long offset) + private void streamRangeBytes(final Range r, final InputStream is, final OutputStream os, long offset) throws IOException { final Log logger = getLogger(); @@ -496,7 +620,6 @@ public class HttpRangeProcessor } } - /** * @return the logger */