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
[email protected]
https://lists.sourceforge.net/lists/listinfo/stripes-development