ALF-17387: Support For HTTP Range Requests in Repository WebScripts

- Added HttpRangeProcessor.processRange which takes a WebScriptResponse parameter instead of HttpServletResponse
   - Changed HttpRangeProcessor.processSingeRange and HttpRangeProcessor.processMultiRange to accept a generic Object parameter then cast to the appropriate WebScriptResponse or HttpServletResponse
   - Added Javadoc to HttpRangeProcessor.processRange
   - Changed StreamContent.streamContentImpl to add code from BaseDownloadContentServlet which does the work of processing the range header from the request
   - Changed StreamContent.streamContentImpl method signature to accept nodeRef and propertyQName parameters needed for multi-range requests
   - Modified methods which override or call StreamContent.streamContentImpl for new method signature, passing in nodeRef and propertyQName or nulls where appropriate


git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@45222 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
This commit is contained in:
Ray Gauss
2013-01-08 21:49:13 +00:00
parent 33a711d71e
commit 251b36454c
4 changed files with 220 additions and 42 deletions

View File

@@ -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);

View File

@@ -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;
@@ -76,6 +77,12 @@ public class StreamContent extends AbstractWebScript implements ResourceLoaderAw
*/
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;
protected NodeService nodeService;
@@ -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)
{

View File

@@ -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

View File

@@ -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)
@@ -81,6 +93,34 @@ public class HttpRangeProcessor
}
}
/**
* 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
*/