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