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