diff --git a/source/java/org/alfresco/web/app/servlet/BaseDownloadContentServlet.java b/source/java/org/alfresco/web/app/servlet/BaseDownloadContentServlet.java index 36b3897082..4f2133ec00 100644 --- a/source/java/org/alfresco/web/app/servlet/BaseDownloadContentServlet.java +++ b/source/java/org/alfresco/web/app/servlet/BaseDownloadContentServlet.java @@ -19,23 +19,21 @@ package org.alfresco.web.app.servlet; import java.io.IOException; -import java.io.InputStream; import java.net.SocketException; import java.text.MessageFormat; import java.util.ArrayList; -import java.util.Collections; import java.util.Date; import java.util.List; import java.util.StringTokenizer; import javax.servlet.ServletException; -import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.model.ContentModel; import org.alfresco.repo.content.filestore.FileContentReader; +import org.alfresco.repo.web.util.HttpRangeProcessor; import org.alfresco.service.ServiceRegistry; import org.alfresco.service.cmr.model.FileInfo; import org.alfresco.service.cmr.model.FileNotFoundException; @@ -72,12 +70,6 @@ public abstract class BaseDownloadContentServlet extends BaseServlet private static final String POWER_POINT_DOCUMENT_MIMETYPE = "application/vnd.ms-powerpoint"; private static final String POWER_POINT_2007_DOCUMENT_MIMETYPE = "application/vnd.openxmlformats-officedocument.presentationml.presentation"; - private static final String MULTIPART_BYTERANGES_BOUNDRY = ""; - private static final String MULTIPART_BYTERANGES_HEADER = "multipart/byteranges; boundary=" + MULTIPART_BYTERANGES_BOUNDRY; - private static final String MULTIPART_BYTERANGES_BOUNDRY_SEP = "--" + MULTIPART_BYTERANGES_BOUNDRY; - private static final String MULTIPART_BYTERANGES_BOUNDRY_END = MULTIPART_BYTERANGES_BOUNDRY_SEP + "--"; - - private static final String HEADER_CONTENT_TYPE = "Content-Type"; 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"; @@ -88,9 +80,6 @@ public abstract class BaseDownloadContentServlet extends BaseServlet private static final String HEADER_USER_AGENT = "User-Agent"; private static final String HEADER_CONTENT_DISPOSITION = "Content-Disposition"; - /** size of a multi-part byte range output buffer */ - private static final int CHUNKSIZE = 64*1024; - protected static final String MIMETYPE_OCTET_STREAM = "application/octet-stream"; protected static final String MSG_ERROR_CONTENT_MISSING = "error_content_missing"; @@ -336,7 +325,8 @@ public abstract class BaseDownloadContentServlet extends BaseServlet // ensure the range header is starts with "bytes=" and process the range(s) if (range.length() > 6) { - processedRange = processRange( + HttpRangeProcessor rangeProcessor = new HttpRangeProcessor(contentService); + processedRange = rangeProcessor.processRange( res, reader, range.substring(6), nodeRef, propertyQName, mimetype, req.getHeader(HEADER_USER_AGENT)); } @@ -407,440 +397,7 @@ public abstract class BaseDownloadContentServlet extends BaseServlet } res.setHeader(HEADER_CONTENT_DISPOSITION, attachmentValue.toString()); } - - /** - * Process a range header - handles single and multiple range requests. - */ - private boolean processRange(HttpServletResponse 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. - * - * @param res HttpServletResponse - * @param reader ContentReader to retrieve content - * @param range Range header value - * @param mimetype Content mimetype - * - * @return true if processed range, false otherwise - */ - private boolean processSingleRange(HttpServletResponse res, ContentReader reader, String range, String mimetype) - throws IOException - { - // 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: - The first 500 bytes: - bytes 0-499/1234 - The second 500 bytes: - bytes 500-999/1234 - All except for the first 500 bytes: - bytes 500-1233/1234 */ - /* 'Range' header example: - bytes=10485760-20971519 */ - - boolean processedRange = false; - Range r = null; - try - { - r = Range.constructRange(range, mimetype, reader.getSize()); - } - catch (IllegalArgumentException err) - { - 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(); - 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)); - - if (getLogger().isDebugEnabled()) - getLogger().debug("Processing: Content-Range: " + contentRange); - - InputStream is = null; - try - { - // output the binary data for the range - ServletOutputStream os = res.getOutputStream(); - is = reader.getContentInputStream(); - - streamRangeBytes(r, is, os, 0L); - - os.close(); - processedRange = true; - } - catch (IOException err) - { - if (getLogger().isDebugEnabled()) - getLogger().debug("Unable to process single range due to IO Exception: " + err.getMessage()); - throw err; - } - finally - { - if (is != null) is.close(); - } - - return processedRange; - } - - /** - * Process multiple ranges. - * - * @param res HttpServletResponse - * @param range Range header value - * @param ref NodeRef to the content for streaming - * @param property Content Property for the content - * @param mimetype Mimetype of the content - * @param userAgent User Agent of the caller - * - * @return true if processed range, false otherwise - */ - private boolean processMultiRange( - HttpServletResponse res, String range, NodeRef ref, QName property, String mimetype, String userAgent) - throws IOException - { - final Log logger = getLogger(); - - // return the sets of bytes as requested in the content-range header - // the response will be formatted as multipart/byteranges media type message - - /* Examples of byte-ranges-specifier values (assuming an entity-body of length 10000): - - - The first 500 bytes (byte offsets 0-499, inclusive): bytes=0-499 - - The second 500 bytes (byte offsets 500-999, inclusive): - bytes=500-999 - - The final 500 bytes (byte offsets 9500-9999, inclusive): - bytes=-500 - - Or bytes=9500- - - The first and last bytes only (bytes 0 and 9999): bytes=0-0,-1 - - Several legal but not canonical specifications of byte offsets 500-999, inclusive: - bytes=500-600,601-999 - bytes=500-700,601-999 */ - - boolean processedRange = false; - - // get the content reader - ContentService contentService = getServiceRegistry(getServletContext()).getContentService(); - ContentReader reader = contentService.getReader(ref, property); - - final List ranges = new ArrayList(8); - long entityLength = reader.getSize(); - for (StringTokenizer t=new StringTokenizer(range, ", "); t.hasMoreTokens(); /**/) - { - try - { - ranges.add(Range.constructRange(t.nextToken(), mimetype, entityLength)); - } - catch (IllegalArgumentException err) - { - 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(); - return true; - } - } - - if (ranges.size() != 0) - { - // merge byte ranges if possible - IE handles this well, FireFox not so much - if (userAgent == null || userAgent.indexOf("MSIE ") != -1) - { - Collections.sort(ranges); - - for (int i=0; i= second.start) - { - if (logger.isDebugEnabled()) - logger.debug("Merging byte range: " + first + " with " + second); - - if (first.end < second.end) - { - // merge second range into first - first.end = second.end; - } - // else we simply discard the second range - it is contained within the first - - // delete second range - ranges.remove(i + 1); - // reset loop index - i--; - } - } - } - - // calculate response content length - long length = MULTIPART_BYTERANGES_BOUNDRY_END.length() + 2; - for (Range r : ranges) - { - length += r.getLength(); - } - - // 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(); - - InputStream is = null; - try - { - for (Range r : ranges) - { - if (logger.isDebugEnabled()) - logger.debug("Processing: " + r.getContentRange()); - - try - { - // output the header bytes for the range - r.outputHeader(os); - - // output the binary data for the range - // need a new reader for each new InputStream - is = contentService.getReader(ref, property).getContentInputStream(); - streamRangeBytes(r, is, os, 0L); - is.close(); - is = null; - - // section marker and flush stream - os.println(); - os.flush(); - } - catch (IOException err) - { - if (getLogger().isDebugEnabled()) - getLogger().debug("Unable to process multiple range due to IO Exception: " + err.getMessage()); - throw err; - } - } - } - finally - { - if (is != null) - { - is.close(); - } - } - - // end marker - os.println(MULTIPART_BYTERANGES_BOUNDRY_END); - os.close(); - processedRange = true; - } - - return processedRange; - } - - /** - * Stream a range of bytes from the given InputStream to the ServletOutputStream - * - * @param r Byte Range to process - * @param is InputStream - * @param os ServletOutputStream - * @param offset Assumed InputStream position - to calculate skip bytes from - * - * @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) - throws IOException - { - final Log logger = getLogger(); - final boolean trace = logger.isTraceEnabled(); - - // TODO: investigate using getFileChannel() on ContentReader - - if (r.start != 0L && r.start > offset) - { - long skipped = offset + is.skip(r.start - offset); - if (skipped < r.start) - { - // Nothing left to download! - return; - } - } - long span = (r.end - r.start) + 1L; - long bytesLeft = span; - int read = 0; - byte[] buf = new byte[((int)bytesLeft) < CHUNKSIZE ? (int)bytesLeft : CHUNKSIZE]; - while ((read = is.read(buf)) > 0 && bytesLeft != 0L) - { - os.write(buf, 0, read); - - bytesLeft -= (long)read; - - if (bytesLeft != 0L) - { - int resize = ((int)bytesLeft) < CHUNKSIZE ? (int)bytesLeft : CHUNKSIZE; - if (resize != buf.length) - { - buf = new byte[resize]; - } - } - if (trace) logger.trace("...wrote " + read + " bytes, with " + bytesLeft + " to go..."); - } - } - - - /** - * Representation of a single byte range. - */ - private static class Range implements Comparable - { - private long start; - private long end; - private long entityLength; - private String contentType; - private String contentRange; - - /** - * Constructor - * - * @param contentType Mimetype of the range content - * @param start Start position in the parent entity - * @param end End position in the parent entity - * @param entityLength Length of the parent entity - */ - Range(String contentType, long start, long end, long entityLength) - { - this.contentType = HEADER_CONTENT_TYPE + ": " + contentType; - this.start = start; - this.end = end; - this.entityLength = entityLength; - } - - /** - * Factory method to construct a byte range from a range header value. - * - * @param range Range header value - * @param contentType Mimetype of the range - * @param entityLength Length of the parent entity - * - * @return Range - * - * @throws IllegalArgumentException for an invalid range - */ - static Range constructRange(String range, String contentType, long entityLength) - { - if (range == null) - { - throw new IllegalArgumentException("Range argument is mandatory"); - } - - // strip total if present - it does not give us anything useful - if (range.indexOf('/') != -1) - { - range = range.substring(0, range.indexOf('/')); - } - - // find the separator - int separator = range.indexOf('-'); - if (separator == -1) - { - throw new IllegalArgumentException("Invalid range: " + range); - } - - try - { - // split range and parse values - long start = 0L; - if (separator != 0) - { - start = Long.parseLong(range.substring(0, separator)); - } - long end = entityLength - 1L; - if (separator != range.length() - 1) - { - end = Long.parseLong(range.substring(separator + 1)); - } - - // return object to represent the byte-range - return new Range(contentType, start, end, entityLength); - } - catch (NumberFormatException err) - { - throw new IllegalArgumentException("Unable to parse range value: " + range); - } - } - - /** - * Output the header bytes for a multi-part byte range header - */ - void outputHeader(ServletOutputStream os) throws IOException - { - // output multi-part boundry separator - os.println(MULTIPART_BYTERANGES_BOUNDRY_SEP); - // output content type and range size sub-header for this part - os.println(this.contentType); - os.println(getContentRange()); - os.println(); - } - - /** - * @return the length in bytes of the byte range content including the header bytes - */ - int getLength() - { - // length in bytes of range plus it's header plus section marker and line feed bytes - return MULTIPART_BYTERANGES_BOUNDRY_SEP.length() + 2 + - this.contentType.length() + 2 + - getContentRange().length() + 4 + (int)(this.end - this.start + 1L) + 2; - } - - /** - * @return the Content-Range header string value for this byte range - */ - private String getContentRange() - { - if (this.contentRange == null) - { - this.contentRange = "Content-Range: bytes " + Long.toString(this.start) + "-" + - Long.toString(this.end) + "/" + Long.toString(this.entityLength); - } - return this.contentRange; - } - - @Override - public String toString() - { - return this.start + "-" + this.end; - } - - /** - * @see java.lang.Comparable#compareTo(java.lang.Object) - */ - public int compareTo(Range o) - { - return this.start > o.start ? 1 : -1; - } - } - /** * Helper to generate a URL to a content node for downloading content from the server. *