Revision: 1342
          http://stripes.svn.sourceforge.net/stripes/?rev=1342&view=rev
Author:   bengunter
Date:     2010-11-12 20:37:10 +0000 (Fri, 12 Nov 2010)

Log Message:
-----------
Applied patch provided by Ward van Wanrooij for STS-761: Patch for 
StreamingResolution to allow for byte range streaming

Modified Paths:
--------------
    
branches/1.5.x/stripes/src/net/sourceforge/stripes/action/StreamingResolution.java

Added Paths:
-----------
    branches/1.5.x/stripes/src/net/sourceforge/stripes/util/Range.java

Modified: 
branches/1.5.x/stripes/src/net/sourceforge/stripes/action/StreamingResolution.java
===================================================================
--- 
branches/1.5.x/stripes/src/net/sourceforge/stripes/action/StreamingResolution.java
  2010-11-12 18:46:06 UTC (rev 1341)
+++ 
branches/1.5.x/stripes/src/net/sourceforge/stripes/action/StreamingResolution.java
  2010-11-12 20:37:10 UTC (rev 1342)
@@ -14,20 +14,26 @@
  */
 package net.sourceforge.stripes.action;
 
-import net.sourceforge.stripes.exception.StripesRuntimeException;
-import net.sourceforge.stripes.util.Log;
-
-import javax.servlet.ServletOutputStream;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.PrintWriter;
 import java.io.Reader;
 import java.io.StringReader;
 import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Date;
+import java.util.Iterator;
+import java.util.List;
 
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import net.sourceforge.stripes.exception.StripesRuntimeException;
+import net.sourceforge.stripes.util.Log;
+import net.sourceforge.stripes.util.Range;
+
 /**
  * <p>Resolution for streaming data back to the client (in place of forwarding 
the user to
  * another page). Designed to be used for streaming non-page data such as 
generated images/charts
@@ -58,6 +64,8 @@
 public class StreamingResolution implements Resolution {
     /** Date format string for RFC 822 dates. */
     private static final String RFC_822_DATE_FORMAT = "EEE, d MMM yyyy 
HH:mm:ss Z";
+    /** Boundary for use in multipart responses. */
+    private static final String MULTIPART_BOUNDARY = 
"BOUNDARY_F7C98B76AEF711DF86D1B4FCDFD72085";
     private static final Log log = Log.getInstance(StreamingResolution.class);
     private InputStream inputStream;
     private Reader reader;
@@ -67,6 +75,8 @@
     private long lastModified = -1;
     private long length = -1;
     private boolean attachment;
+    private boolean rangeSupport = false;
+    private List<Range<Long>> byteRanges;
 
     /**
      * Constructor only to be used when subclassing the StreamingResolution 
(usually using
@@ -182,6 +192,30 @@
     }
 
     /**
+     * Indicates whether byte range serving is supported by stream method. 
(Defaults to false).
+     * Besides setting this flag, the ActionBean also needs to set the length 
of the response and
+     * provide an {...@link InputStream}-based input. Reasons for disabling 
byte range serving:
+     * <ul>
+     * <li>The stream method is overridden and does not support byte range 
serving</li>
+     * <li>The input to this {...@link StreamingResolution} was created 
on-demand, and retrieving in
+     * byte ranges would redo this process for every byte range.</li>
+     * </ul>
+     * Reasons for enabling byte range serving:
+     * <ul>
+     * <li>Streaming static multimedia files</li>
+     * <li>Supporting resuming download managers</li>
+     * </ul>
+     * 
+     * @param rangeSupport Whether byte range serving is supported by stream 
method.
+     * @return StreamingResolution so that this method call can be chained to 
the constructor and
+     *         returned.
+     */
+    public StreamingResolution setRangeSupport(boolean rangeSupport) {
+        this.rangeSupport = rangeSupport;
+        return this;
+    }
+
+    /**
      * Streams data from the InputStream or Reader to the response's 
OutputStream or PrinterWriter,
      * using a moderately sized buffer to ensure that the operation is 
reasonable efficient.
      * Once the InputStream or Reader signaled the end of the stream, close() 
is called on it.
@@ -193,6 +227,15 @@
      */
     final public void execute(HttpServletRequest request, HttpServletResponse 
response)
             throws Exception {
+        /*-
+         * Process byte ranges only when the following three conditions are 
met:
+         *     - Length has been defined (without length it is impossible to 
efficiently stream)
+         *     - rangeSupport has not been set to false
+         *     - Output is binary and not character based
+        -*/
+        if (rangeSupport && (length >= 0) && (inputStream != null))
+            byteRanges = parseRangeHeader(request.getHeader("Range"));
+
         applyHeaders(response);
         stream(response);
     }
@@ -203,18 +246,39 @@
      * @param response the current HttpServletResponse
      */
     protected void applyHeaders(HttpServletResponse response) {
-        response.setContentType(this.contentType);
+        if (byteRanges != null) {
+            response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
+        }
+
+        if ((byteRanges == null) || (byteRanges.size() == 1)) {
+            response.setContentType(this.contentType);
+        }
+        else {
+            response.setContentType("multipart/byteranges; boundary=" + 
MULTIPART_BOUNDARY);
+        }
+
         if (this.characterEncoding != null) {
             response.setCharacterEncoding(characterEncoding);
         }
 
         // Set Content-Length header
         if (length >= 0) {
-            // Odd that ServletResponse.setContentLength is limited to int.
-            // requires downcast from long to int e.g.
-            // response.setContentLength((int)length);
-            // Workaround to allow large files:
-            response.addHeader("Content-Length", Long.toString(length));
+            if (byteRanges == null) {
+                // Odd that ServletResponse.setContentLength is limited to int.
+                // requires downcast from long to int e.g.
+                // response.setContentLength((int)length);
+                // Workaround to allow large files:
+                response.addHeader("Content-Length", Long.toString(length));
+            }
+            else if (byteRanges.size() == 1) {
+                Range<Long> byteRange;
+
+                byteRange = byteRanges.get(0);
+                response.setHeader("Content-Length",
+                        Long.toString(byteRange.getEnd() - 
byteRange.getStart() + 1));
+                response.setHeader("Content-Range", "bytes " + 
byteRange.getStart() + "-"
+                        + byteRange.getEnd() + "/" + length);
+            }
         }
 
         // Set Last-Modified header
@@ -242,10 +306,101 @@
     }
 
     /**
+     * Parse the Range header according to RFC 2616 section 14.35.1. Example 
ranges from this
+     * section:
+     * <ul>
+     * <li>The first 500 bytes (byte offsets 0-499, inclusive): 
bytes=0-499</li>
+     * <li>The second 500 bytes (byte offsets 500-999, inclusive): 
bytes=500-999</li>
+     * <li>The final 500 bytes (byte offsets 9500-9999, inclusive): bytes=-500 
- Or bytes=9500-</li>
+     * <li>The first and last bytes only (bytes 0 and 9999): bytes=0-0,-1</li>
+     * <li>Several legal but not canonical specifications of the second 500 
bytes (byte offsets
+     * 500-999, inclusive): bytes=500-600,601-999 bytes=500-700,601-999</li>
+     * </ul>
+     * 
+     * @param value the value of the Range header
+     * @return List of sorted, non-overlapping ranges
+     */
+    protected List<Range<Long>> parseRangeHeader(String value) {
+        Iterator<Range<Long>> i;
+        String byteRangesSpecifier[], bytesUnit, byteRangeSet[];
+        List<Range<Long>> res;
+        long lastEnd = -1;
+
+        if (value == null)
+            return null;
+        res = new ArrayList<Range<Long>>();
+        // Parse prelude
+        byteRangesSpecifier = value.split("=");
+        if (byteRangesSpecifier.length != 2)
+            return null;
+        bytesUnit = byteRangesSpecifier[0];
+        byteRangeSet = byteRangesSpecifier[1].split(",");
+        if (!bytesUnit.equals("bytes"))
+            return null;
+        // Parse individual byte ranges
+        for (String byteRangeSpec : byteRangeSet) {
+            String[] bytePos;
+            Long firstBytePos = null, lastBytePos = null;
+
+            bytePos = byteRangeSpec.split("-", -1);
+            try {
+                if (bytePos[0].trim().length() > 0)
+                    firstBytePos = Long.valueOf(bytePos[0].trim());
+                if (bytePos[1].trim().length() > 0)
+                    lastBytePos = Long.valueOf(bytePos[1].trim());
+            }
+            catch (NumberFormatException e) {
+                log.warn("Unable to parse Range header", e);
+            }
+            if ((firstBytePos == null) && (lastBytePos == null)) {
+                return null;
+            }
+            else if (firstBytePos == null) {
+                firstBytePos = length - lastBytePos;
+                lastBytePos = length - 1;
+            }
+            else if (lastBytePos == null) {
+                lastBytePos = length - 1;
+            }
+            if (firstBytePos > lastBytePos)
+                return null;
+            if (firstBytePos < 0)
+                return null;
+            if (lastBytePos >= length)
+                return null;
+            res.add(new Range<Long>(firstBytePos, lastBytePos));
+        }
+        // Sort byte ranges
+        Collections.sort(res);
+        // Remove overlapping ranges
+        i = res.listIterator();
+        while (i.hasNext()) {
+            Range<Long> range;
+
+            range = i.next();
+            if (lastEnd >= range.getStart()) {
+                range.setStart(lastEnd + 1);
+                if ((range.getStart() >= length) || (range.getStart() > 
range.getEnd()))
+                    i.remove();
+                else
+                    lastEnd = range.getEnd();
+            }
+            else {
+                lastEnd = range.getEnd();
+            }
+        }
+        if (res.isEmpty())
+            return null;
+        else
+            return res;
+    }
+
+    /**
      * <p>
      * Does the actual streaming of data through the response. If subclassed, 
this method should be
      * overridden to stream back data other than data supplied by an 
InputStream or a Reader
-     * supplied to a constructor.
+     * supplied to a constructor. If not implementing byte range serving, be 
sure not to set
+     * rangeSupport to true.
      * </p>
      * 
      * <p>
@@ -282,12 +437,47 @@
         }
         else if (this.inputStream != null) {
             byte[] buffer = new byte[512];
+            long count = 0;
+
             try {
                 ServletOutputStream out = response.getOutputStream();
 
-                while ( (length = this.inputStream.read(buffer)) != -1) {
-                    out.write(buffer, 0, length);
+                if (byteRanges == null) {
+                    while ((length = this.inputStream.read(buffer)) != -1) {
+                        out.write(buffer, 0, length);
+                    }
                 }
+                else {
+                    for (Range<Long> byteRange : byteRanges) {
+                        // See RFC 2616 section 14.16
+                        if (byteRanges.size() > 1) {
+                            out.print("--" + MULTIPART_BOUNDARY + "\r\n");
+                            out.print("Content-Type: " + contentType + "\r\n");
+                            out.print("Content-Range: bytes " + 
byteRange.getStart() + "-"
+                                    + byteRange.getEnd() + "/" + this.length + 
"\r\n");
+                            out.print("\r\n");
+                        }
+                        if (count < byteRange.getStart()) {
+                            long skip;
+
+                            skip = byteRange.getStart() - count;
+                            this.inputStream.skip(skip);
+                            count += skip;
+                        }
+                        while ((length = this.inputStream.read(buffer, 0, 
(int) Math.min(
+                                (long) buffer.length, byteRange.getEnd() + 1 - 
count))) != -1) {
+                            out.write(buffer, 0, length);
+                            count += length;
+                            if (byteRange.getEnd() + 1 == count)
+                                break;
+                        }
+                        if (byteRanges.size() > 1) {
+                            out.print("\r\n");
+                        }
+                    }
+                    if (byteRanges.size() > 1)
+                        out.print("--" + MULTIPART_BOUNDARY + "--\r\n");
+                }
             }
             finally {
                 try {

Added: branches/1.5.x/stripes/src/net/sourceforge/stripes/util/Range.java
===================================================================
--- branches/1.5.x/stripes/src/net/sourceforge/stripes/util/Range.java          
                (rev 0)
+++ branches/1.5.x/stripes/src/net/sourceforge/stripes/util/Range.java  
2010-11-12 20:37:10 UTC (rev 1342)
@@ -0,0 +1,111 @@
+/* Copyright 2010 Ward van Wanrooij
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sourceforge.stripes.util;
+
+/**
+ * Utility class for working with ranges, ranging from start to end (both 
inclusive).
+ * 
+ * @author Ward van Wanrooij
+ * @since Stripes 1.6
+ */
+public class Range<T extends Comparable<T>> implements Comparable<Range<T>> {
+    private T start, end;
+
+    /**
+     * Constructor for range from start to end (both inclusive). Start and end 
may not be null.
+     * 
+     * @param start Start of the range
+     * @param end End of the range
+     */
+    public Range(T start, T end) {
+        setStart(start);
+        setEnd(end);
+    }
+
+    /**
+     * Retrieves start of the range.
+     * 
+     * @return Start of the range
+     */
+    public T getStart() {
+        return start;
+    }
+
+    /**
+     * Sets start of the range. Start may not be null.
+     * 
+     * @param start Start of the range
+     */
+    public void setStart(T start) {
+        if (start == null)
+            throw new NullPointerException();
+        this.start = start;
+    }
+
+    /**
+     * Retrieves end of the range.
+     * 
+     * @return End of the range
+     */
+    public T getEnd() {
+        return end;
+    }
+
+    /**
+     * Sets end of the range. End may not be null.
+     * 
+     * @param end End of the range
+     */
+    public void setEnd(T end) {
+        if (end == null)
+            throw new NullPointerException();
+        this.end = end;
+    }
+
+    /**
+     * Checks whether an item is contained in this range.
+     * 
+     * @param item Item to check
+     * @return True if item is in range
+     */
+    public boolean contains(T item) {
+        return (start.compareTo(item) <= 0) && (end.compareTo(item) >= 0);
+    }
+
+    public int compareTo(Range<T> o) {
+        int res;
+
+        if ((res = start.compareTo(o.getStart())) == 0)
+            res = end.compareTo(o.getEnd());
+        return res;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public boolean equals(Object o) {
+        return (o instanceof Range) && ((this == o) || (compareTo((Range<T>) 
o) == 0));
+    }
+
+    @Override
+    public int hashCode() {
+        return start.hashCode() ^ end.hashCode();
+    }
+
+    @Override
+    public String toString() {
+        return getClass().getName() + " { type: " + start.getClass().getName() 
+ ", start: "
+                + start.toString() + ", end: " + end.toString() + " }";
+    }
+}


This was sent by the SourceForge.net collaborative development platform, the 
world's largest Open Source development site.

------------------------------------------------------------------------------
Centralized Desktop Delivery: Dell and VMware Reference Architecture
Simplifying enterprise desktop deployment and management using
Dell EqualLogic storage and VMware View: A highly scalable, end-to-end
client virtualization framework. Read more!
http://p.sf.net/sfu/dell-eql-dev2dev
_______________________________________________
Stripes-development mailing list
Stripes-development@lists.sourceforge.net
https://lists.sourceforge.net/lists/listinfo/stripes-development

Reply via email to