/* The contents of this file are subject to the license and copyright terms
* detailed in the license directory at the root of the source tree (also
* available online at http://fedora-commons.org/license/).
*/
package org.fcrepo.utilities.io;
import java.io.IOException;
import java.io.InputStream;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Wrapping an InputStream to limit it to a sub-range of bytes,
* but allow it to be used as a response entity
* @author armintor@gmail.com
*
*/
public class ByteRangeInputStream extends InputStream {
private static final Pattern RANGE_HEADER = Pattern.compile("^bytes\\s*=\\s*(\\d*)\\s*(-\\s*(\\d*))?\\s*$");
public final long offset;
public final long length;
public final String contentRange;
private final InputStream src;
private long read = 0;
/**
*
* @param src the source input stream
* @param limit the maximum size of the stream
* @param rangeHeader the value of a rfc2616 HTTP Range request header, given in the form "bytes=[num][-num]"
* @throws IndexOutOfBoundsException when there is no satisfiable range in the header value (HTTP 416)
* @throws IOException
*/
public ByteRangeInputStream(InputStream src, long limit, String rangeHeader)
throws IOException, IndexOutOfBoundsException {
Matcher m = RANGE_HEADER.matcher(rangeHeader);
if (!m.find()) {
throw new IOException("Bad range spec: " + rangeHeader);
}
String g1 = m.group(1), g3 = m.group(3);
if (g1 == null && g3 == null) {
throw new IOException("Bad range spec values: " + rangeHeader);
}
long endByte;
if (g3 == null || g3.isEmpty()) {
endByte = limit;
} else {
endByte = Long.parseLong(g3);
}
long offset = (g1 != null && !g1.isEmpty()) ? Long.parseLong(g1) : (0L - endByte);
long length = 0;
if (offset < 0) {
length = Math.min(limit, Math.abs(offset));
offset = limit - length;
} else {
length = Math.min(limit - offset, endByte + 1 - offset);
}
if (offset >= limit || offset < 0) {
throw new IndexOutOfBoundsException("Bad range spec start position: " + rangeHeader);
}
this.length = Math.min(length, limit);
if (length < 0) {
throw new IndexOutOfBoundsException("Bad range spec end position: " + rangeHeader);
}
this.offset = offset;
this.src = src;
long skipped = 0;
while ((skipped += src.skip(offset - skipped)) < offset) {
src.skip(offset - skipped);
}
// describe the inclusive range of byte positions of this segment
contentRange = "bytes " + Long.toString(this.offset) + "-" + Long.toString(this.offset + this.length - 1)
+ "/" + Long.toString(limit);
}
@Override
public int read() throws IOException {
java.io.FileInputStream.class.getCanonicalName();
java.io.ByteArrayInputStream.class.getCanonicalName();
if ((this.read+1) < this.length) {
this.read++;
return src.read();
} else {
src.close();
return -1;
}
}
@Override
public int read(byte[] buf) throws IOException {
return read(buf, 0 , buf.length);
}
@Override
public int read(byte[] buf, int offset, int length) throws IOException {
long rem = this.length - this.read;
if (rem < 1) return -1;
int toRead = (int)Math.min(rem, length);
int read = src.read(buf, offset, toRead);
this.read += read;
if (this.read >= this.length) {
src.close();
}
return read;
}
@Override
public int available() throws IOException {
long rem = this.length - this.read;
if (rem > Integer.MAX_VALUE) {
return src.available();
}
return Math.min(src.available(), (int)(rem));
}
@Override
public void close() throws IOException {
this.src.close();
}
@Override
public long skip(long skip) throws IOException {
long skipped = this.src.skip(Math.min(skip, this.length - this.read));
this.read += skipped;
return skipped;
}
}