You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by fm...@apache.org on 2010/10/02 02:13:30 UTC
svn commit: r1003719 -
/sling/trunk/bundles/servlets/get/src/main/java/org/apache/sling/servlets/get/impl/helpers/StreamRendererServlet.java
Author: fmeschbe
Date: Sat Oct 2 00:13:29 2010
New Revision: 1003719
URL: http://svn.apache.org/viewvc?rev=1003719&view=rev
Log:
SLING-1814 Implementing support for Range requests for streamed data.
Modified:
sling/trunk/bundles/servlets/get/src/main/java/org/apache/sling/servlets/get/impl/helpers/StreamRendererServlet.java
Modified: sling/trunk/bundles/servlets/get/src/main/java/org/apache/sling/servlets/get/impl/helpers/StreamRendererServlet.java
URL: http://svn.apache.org/viewvc/sling/trunk/bundles/servlets/get/src/main/java/org/apache/sling/servlets/get/impl/helpers/StreamRendererServlet.java?rev=1003719&r1=1003718&r2=1003719&view=diff
==============================================================================
--- sling/trunk/bundles/servlets/get/src/main/java/org/apache/sling/servlets/get/impl/helpers/StreamRendererServlet.java (original)
+++ sling/trunk/bundles/servlets/get/src/main/java/org/apache/sling/servlets/get/impl/helpers/StreamRendererServlet.java Sat Oct 2 00:13:29 2010
@@ -20,15 +20,20 @@ import static javax.servlet.http.HttpSer
import static org.apache.sling.api.servlets.HttpConstants.HEADER_IF_MODIFIED_SINCE;
import static org.apache.sling.api.servlets.HttpConstants.HEADER_LAST_MODIFIED;
+import java.io.BufferedInputStream;
+import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
+import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
+import java.util.StringTokenizer;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
+import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -58,6 +63,18 @@ public class StreamRendererServlet exten
private static final long serialVersionUID = -1L;
+ /**
+ * MIME multipart separation string
+ */
+ private static final String mimeSeparation = "SLING_MIME_BOUNDARY";
+
+ /**
+ * Full range marker.
+ */
+ private static ArrayList<Range> FULL = new ArrayList<Range>(0);
+
+ private static final int IO_BUFFER_SIZE = 2048;
+
/** default log */
private final Logger log = LoggerFactory.getLogger(getClass());
@@ -118,7 +135,7 @@ public class StreamRendererServlet exten
InputStream stream = resource.adaptTo(InputStream.class);
if (stream != null) {
- streamResource(resource, stream, included, response);
+ streamResource(resource, stream, included, request, response);
} else {
@@ -175,29 +192,69 @@ public class StreamRendererServlet exten
private void streamResource(final Resource resource,
final InputStream stream, final boolean included,
+ final SlingHttpServletRequest request,
final SlingHttpServletResponse response) throws IOException {
// finally stream the resource
try {
- // set various response headers, unless the request is included
- if (!included) {
+ final ArrayList<Range> ranges;
+ if (included) {
+
+ // no range support on included requests
+ ranges = FULL;
+
+ } else {
+
+ // parse optional ranges
+ ranges = parseRange(request, response,
+ resource.getResourceMetadata());
+ if (ranges == null) {
+ // there was something wrong, the parseRange has sent a
+ // response and we are done
+ return;
+ }
+
+ // set various response headers, unless the request is included
setHeaders(resource, response);
}
- OutputStream out = response.getOutputStream();
+ ServletOutputStream out = response.getOutputStream();
+
+ if (ranges == FULL) {
+
+ // return full resource
+ byte[] buf = new byte[IO_BUFFER_SIZE];
+ int rd;
+ while ((rd = stream.read(buf)) >= 0) {
+ out.write(buf, 0, rd);
+ }
+
+ } else {
+
+ // return ranges of the resource
+ response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
+
+ if (ranges.size() == 1) {
+
+ Range range = ranges.get(0);
+ response.addHeader("Content-Range", "bytes " + range.start
+ + "-" + range.end + "/" + range.length);
+ setContentLength(response, range.end - range.start + 1);
+
+ copy(stream, out, range);
+
+ } else {
+
+ response.setContentType("multipart/byteranges; boundary="
+ + mimeSeparation);
+
+ copy(resource, out, ranges.iterator());
+ }
- byte[] buf = new byte[1024];
- int rd;
- while ((rd = stream.read(buf)) >= 0) {
- out.write(buf, 0, rd);
}
} finally {
- try {
- stream.close();
- } catch (IOException ignore) {
- // don't care
- }
+ closeSilently(stream);
}
}
@@ -254,9 +311,9 @@ public class StreamRendererServlet exten
* @param request
* @param response
*/
- private void setHeaders(Resource resource,
+ private void setHeaders(Resource resource,
SlingHttpServletResponse response) {
-
+
final ResourceMetadata meta = resource.getResourceMetadata();
final long modifTime = meta.getModificationTime();
if (modifTime > 0) {
@@ -285,11 +342,28 @@ public class StreamRendererServlet exten
response.setCharacterEncoding(encoding);
}
- long length = meta.getContentLength();
- if (length > 0 && length < Integer.MAX_VALUE) {
- response.setContentLength((int) length);
+ setContentLength(response, meta.getContentLength());
+ }
+
+ /**
+ * Set the <code>Content-Length</code> header to the give value. If the
+ * length is larger than <code>Integer.MAX_VALUE</code> it is converted to a
+ * string and the <code>setHeader(String, String)</code> method is called
+ * instead of the <code>setContentLength(int)</code> method.
+ *
+ * @param response The response on which to set the
+ * <code>Content-Length</code> header.
+ * @param length The content length to be set. If this value is equal to or
+ * less than zero, the header is not set.
+ */
+ private void setContentLength(final HttpServletResponse response, final long length) {
+ if (length > 0) {
+ if (length < Integer.MAX_VALUE) {
+ response.setContentLength((int) length);
+ } else {
+ response.setHeader("Content-Length", String.valueOf(length));
+ }
}
-
}
private void renderIndex(Resource resource,
@@ -338,10 +412,7 @@ public class StreamRendererServlet exten
if (ins == null) {
name += "/";
} else {
- try {
- ins.close();
- } catch (IOException ignore) {
- }
+ closeSilently(ins);
}
String displayName = name;
@@ -368,4 +439,297 @@ public class StreamRendererServlet exten
pw.println();
}
+
+ //---------- Range header support
+ // The following code is copy-derived from the Tomcate DefaultServlet
+ // http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/servlets/DefaultServlet.java?view=markup
+
+ /**
+ * Copies a number of ranges from the given resource to the output stream.
+ * Copy the contents of the specified input stream to the specified output
+ * stream, and ensure that both streams are closed before returning (even in
+ * the face of an exception).
+ *
+ * @param resource The resource from which to send ranges
+ * @param ostream The output stream to write to
+ * @param ranges Iterator of the ranges the client wanted to retrieve
+ * @param contentType Content type of the resource
+ * @exception IOException if an input/output error occurs
+ */
+ private void copy(Resource resource, ServletOutputStream ostream,
+ Iterator<Range> ranges) throws IOException {
+
+ String contentType = resource.getResourceMetadata().getContentType();
+ IOException exception = null;
+
+ while ((exception == null) && (ranges.hasNext())) {
+
+ InputStream resourceInputStream = resource.adaptTo(InputStream.class);
+ InputStream istream = new BufferedInputStream(resourceInputStream,
+ IO_BUFFER_SIZE);
+
+ try {
+ Range currentRange = ranges.next();
+
+ // Writing MIME header.
+ ostream.println();
+ ostream.println("--" + mimeSeparation);
+ if (contentType != null) {
+ ostream.println("Content-Type: " + contentType);
+ }
+ ostream.println("Content-Range: bytes " + currentRange.start + "-"
+ + currentRange.end + "/" + currentRange.length);
+ ostream.println();
+
+ // Printing content
+ exception = copyRange(istream, ostream, currentRange.start,
+ currentRange.end);
+ } finally {
+ closeSilently(istream);
+ }
+
+ }
+
+ ostream.println();
+ ostream.print("--" + mimeSeparation + "--");
+
+ // Rethrow any exception that has occurred
+ if (exception != null) throw exception;
+
+ }
+
+ /**
+ * Copy the contents of the specified input stream to the specified
+ * output stream.
+ *
+ * @param cacheEntry The cache entry for the source resource
+ * @param ostream The output stream to write to
+ * @param range Range the client wanted to retrieve
+ * @exception IOException if an input/output error occurs
+ */
+ private void copy(InputStream resourceInputStream, OutputStream ostream,
+ Range range) throws IOException {
+
+ InputStream istream = new BufferedInputStream(resourceInputStream, IO_BUFFER_SIZE);
+ IOException exception = copyRange(istream, ostream, range.start, range.end);
+
+ // Rethrow any exception that has occurred
+ if (exception != null) {
+ throw exception;
+ }
+ }
+
+ /**
+ * Copy the contents of the specified input stream to the specified output
+ * stream.
+ *
+ * @param istream The input stream to read from
+ * @param ostream The output stream to write to
+ * @param start Start of the range which will be copied
+ * @param end End of the range which will be copied
+ * @return Exception which occurred during processing
+ */
+ private IOException copyRange(InputStream istream,
+ OutputStream ostream, long start, long end) {
+
+ log.debug("copyRange: Serving bytes: {}-{}", start, end);
+
+ try {
+ long skipped = istream.skip(start);
+ if (skipped < start) {
+ return new IOException("Failed to skip " + start
+ + " bytes; only skipped " + skipped + " bytes");
+ }
+ } catch (IOException e) {
+ return e;
+ }
+
+ IOException exception = null;
+ long bytesToRead = end - start + 1;
+
+ byte buffer[] = new byte[IO_BUFFER_SIZE];
+ int len = buffer.length;
+ while ((bytesToRead > 0) && (len >= buffer.length)) {
+ try {
+ len = istream.read(buffer);
+ if (bytesToRead >= len) {
+ ostream.write(buffer, 0, len);
+ bytesToRead -= len;
+ } else {
+ ostream.write(buffer, 0, (int) bytesToRead);
+ bytesToRead = 0;
+ }
+ } catch (IOException e) {
+ exception = e;
+ len = -1;
+ }
+ if (len < buffer.length) {
+ break;
+ }
+ }
+
+ return exception;
+ }
+
+ /**
+ * Parse the range header.
+ *
+ * @param request The servlet request we are processing
+ * @param response The servlet response we are creating
+ * @return ArrayList of ranges parsed from the Range header or {@link #FULL}
+ * if the full resource should be returned or <code>null</code> if
+ * an error occurred parsing the header and the request has been
+ * finished sending an error status.
+ */
+ private ArrayList<Range> parseRange(HttpServletRequest request,
+ HttpServletResponse response, ResourceMetadata metadata)
+ throws IOException {
+
+ // Checking If-Range
+ String headerValue = request.getHeader("If-Range");
+ if (headerValue != null) {
+
+ long headerValueTime = (-1L);
+ try {
+ headerValueTime = request.getDateHeader("If-Range");
+ } catch (IllegalArgumentException e) {
+ // Ignore
+ }
+
+ if (headerValueTime == (-1L)) {
+
+ // If the ETag the client gave does not match the entity
+ // etag, then the entire entity is returned.
+ // Sling: no etag support yet, return full range
+ return FULL;
+
+ } else if (metadata.getModificationTime() > (headerValueTime + 1000)) {
+
+ // If the timestamp of the entity the client got is older than
+ // the last modification date of the entity, the entire entity
+ // is returned.
+ return FULL;
+
+ }
+
+ }
+
+ long fileLength = metadata.getContentLength();
+ if (fileLength == 0) {
+ return FULL;
+ }
+
+ // Retrieving the range header (if any is specified)
+ String rangeHeader = request.getHeader("Range");
+ if (rangeHeader == null) {
+ return FULL;
+ }
+
+ // bytes is the only range unit supported (and I don't see the point
+ // of adding new ones).
+ if (!rangeHeader.startsWith("bytes")) {
+ failParseRange(response, fileLength, rangeHeader);
+ return null;
+ }
+
+ rangeHeader = rangeHeader.substring(6);
+
+ // Vector which will contain all the ranges which are successfully
+ // parsed.
+ ArrayList<Range> result = new ArrayList<Range>();
+ StringTokenizer commaTokenizer = new StringTokenizer(rangeHeader, ",");
+
+ // Parsing the range list
+ while (commaTokenizer.hasMoreTokens()) {
+ String rangeDefinition = commaTokenizer.nextToken().trim();
+
+ Range currentRange = new Range();
+ currentRange.length = fileLength;
+
+ int dashPos = rangeDefinition.indexOf('-');
+
+ if (dashPos == -1) {
+ failParseRange(response, fileLength, rangeHeader);
+ return null;
+ }
+
+ if (dashPos == 0) {
+
+ try {
+ long offset = Long.parseLong(rangeDefinition);
+ currentRange.start = fileLength + offset;
+ currentRange.end = fileLength - 1;
+ } catch (NumberFormatException e) {
+ failParseRange(response, fileLength, rangeHeader);
+ return null;
+ }
+
+ } else {
+
+ try {
+ currentRange.start = Long.parseLong(rangeDefinition.substring(
+ 0, dashPos));
+ if (dashPos < rangeDefinition.length() - 1)
+ currentRange.end = Long.parseLong(rangeDefinition.substring(
+ dashPos + 1, rangeDefinition.length()));
+ else
+ currentRange.end = fileLength - 1;
+ } catch (NumberFormatException e) {
+ failParseRange(response, fileLength, rangeHeader);
+ return null;
+ }
+
+ }
+
+ if (!currentRange.validate()) {
+ failParseRange(response, fileLength, rangeHeader);
+ return null;
+ }
+
+ result.add(currentRange);
+ }
+
+ return result;
+ }
+
+ /**
+ * Sends a 416 error response to the client if the Range header is
+ * not acceptable
+ */
+ private void failParseRange(final HttpServletResponse response,
+ final long fileLength, final String rangeHeader) throws IOException {
+ log.error("parseRange: Cannot support range {}; sending 416",
+ rangeHeader);
+ response.addHeader("Content-Range", "bytes */" + fileLength);
+ response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
+ }
+
+ private void closeSilently(final Closeable closeable) {
+ if (closeable != null) {
+ try {
+ closeable.close();
+ } catch (IOException ignore) {
+ }
+ }
+ }
+
+ // --------- Range Inner Class
+
+ protected class Range {
+
+ public long start;
+
+ public long end;
+
+ public long length;
+
+ /**
+ * Validate range.
+ */
+ public boolean validate() {
+ if (end >= length) end = length - 1;
+ return ((start >= 0) && (end >= 0) && (start <= end) && (length > 0));
+ }
+
+ }
}
\ No newline at end of file