/*
* The MIT License (MIT)
*
* Copyright (c) 2007-2015 Broad Institute
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.broad.igv.util.stream;
import htsjdk.samtools.seekablestream.SeekableStream;
import org.apache.log4j.Logger;
import org.broad.igv.ui.util.Packable;
import org.broad.igv.util.HttpUtils;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
/**
*/
public class IGVSeekableHTTPStream extends SeekableStream {
static Logger log = Logger.getLogger(IGVSeekableHTTPStream.class);
private long position = 0;
private URL url;
long contentLength = -1; // Not set
public IGVSeekableHTTPStream(final URL url) {
this.url = url;
}
public long position() {
return position;
}
public long length() {
return contentLength;
}
@Override
public long skip(long n) throws IOException {
long bytesToSkip = contentLength < 0 ? n : Math.min(n, contentLength - position);
position += bytesToSkip;
return bytesToSkip;
}
public boolean eof() throws IOException {
return contentLength > 0 && position >= contentLength;
}
public void seek(final long position) {
this.position = position;
}
public int read(byte[] buffer, int offset, int len) throws IOException {
int attempts = 0;
while(attempts < 3) {
try {
return _read(buffer, offset, len);
} catch (java.net.SocketException e) {
if(attempts < 3) {
attempts++;
log.error("Socket exception. Trying again.", e);
}
else {
throw e;
}
}
}
throw new RuntimeException("Reading " + url + " failed with unknown error."); // Should be impossible to get here
}
public int _read(byte[] buffer, int offset, int len) throws IOException {
String stats = "Offset=" + offset + ",len=" + len + ",buflen=" + buffer.length;
if (offset < 0 || len < 0 || (offset + len) > buffer.length) {
throw new IndexOutOfBoundsException(stats);
}
if (len == 0) {
return 0;
}
InputStream is = null;
int n = 0;
try {
if (contentLength > 0 && position >= contentLength) {
return -1; // EOF
}
long endRange = position + len - 1;
// IF we know the total content length, limit the end range to that.
if (contentLength > 0) {
endRange = Math.min(endRange, contentLength);
}
if (log.isTraceEnabled()) {
log.trace("Trying to read range " + position + " to " + endRange);
}
is = openInputStreamForRange(position, endRange);
while (n < len) {
int count = is.read(buffer, offset + n, len - n);
if (count < 0) {
if (n == 0) {
return -1;
} else {
break;
}
}
n += count;
}
position += n;
return n;
} catch (HttpUtils.UnsatisfiableRangeException e) {
return handleUnsatisfiableRange(n);
}
catch (IOException e) {
if (e.getMessage().contains("416") || (e instanceof EOFException)) {
return handleUnsatisfiableRange(n);
} else {
throw e;
}
} finally {
if (is != null) {
is.close();
}
}
}
private int handleUnsatisfiableRange(int n) {
if (n == 0) {
contentLength = position;
return -1;
} else {
position += n;
// As we are at EOF, the contentLength and position are by definition =
contentLength = position;
return n;
}
}
public void close() throws IOException {
// Nothing to do
}
public int read() throws IOException {
byte[] tmp = new byte[1];
read(tmp, 0, 1);
return (int) tmp[0] & 0xFF;
}
public InputStream openInputStreamForRange(long start, long end) throws IOException {
String byteRange = "bytes=" + start + "-" + end;
Map<String, String> params = new HashMap();
params.put("Range", byteRange);
//URL url = addStartEndQueryString(this.url, start, end);
HttpURLConnection conn = HttpUtils.getInstance().openConnection(url, params);
try {
InputStream input = conn.getInputStream();
return input;
} catch (IOException e) {
HttpUtils.getInstance().readErrorStream(conn); // Consume content
throw e;
}
}
/**
* Add query parameters which should more properly be in Range header field
* to query string
*
* @param start start byte
* @param end end byte
* @throws java.net.MalformedURLException
*/
static URL addStartEndQueryString(URL url, long start, long end) throws MalformedURLException {
String queryString = url.getQuery();
if (queryString == null) {
return new URL(url.toExternalForm() + "?start=" + start + "&end=" + end);
} else {
String newQueryString = queryString + "&start=" + start + "&end" + end;
return new URL(url.toExternalForm().replace(queryString, newQueryString));
}
}
@Override
public String getSource() {
return url.toExternalForm();
}
public static void main(String[] args) throws IOException {
IGVSeekableHTTPStream stream = new IGVSeekableHTTPStream(new URL("http://localhost/igv-web/test/data/misc/BufferedReaderTest.bin"));
byte[] buffer = new byte[1000];
stream.read(buffer, 0, 1000);
System.out.println("Done");
}
}