package org.limewire.swarm.http;
import java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.impl.nio.DefaultClientIOEventDispatch;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicHttpRequest;
import org.apache.http.nio.entity.ConsumingNHttpEntity;
import org.apache.http.nio.entity.ConsumingNHttpEntityTemplate;
import org.apache.http.nio.protocol.AsyncNHttpClientHandler;
import org.apache.http.nio.protocol.NHttpRequestExecutionHandler;
import org.apache.http.nio.reactor.IOEventDispatch;
import org.apache.http.nio.reactor.SessionRequest;
import org.apache.http.nio.reactor.SessionRequestCallback;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.HttpContext;
import org.limewire.collection.IntervalSet;
import org.limewire.collection.Range;
import org.limewire.http.reactor.LimeConnectingIOReactor;
import org.limewire.http.reactor.LimeConnectingIOReactorFactory;
import org.limewire.logging.Log;
import org.limewire.logging.LogFactory;
import org.limewire.swarm.SwarmCoordinator;
import org.limewire.swarm.SwarmFile;
import org.limewire.swarm.SwarmSource;
import org.limewire.swarm.SwarmSourceDownloader;
import org.limewire.swarm.http.listener.ResponseContentListener;
import org.limewire.swarm.http.listener.SwarmHttpContentListener;
import org.limewire.util.Objects;
/**
* The SwarmHttpSource handler is responsible for processing http source
* connections.
*
* It will attempt to lease a range of bytes to download from the given
* SwarmCoordinator, then it will attempt to download those bytes by submitting
* a Partial Content http request to the connections server. Upon a successful
* download it will write the bytes in batch asynchronously as they come in on
* the reading connection.
*
* When finished writing the bytes it has read from the connection it will try
* to lease more bytes and begin the process anew.
*
* When there are no more bytes left to lease, the swarm sources state will be
* marked as finished, and the connection to it will be closed.
*
* Additionally the swarmSource can be checked to see if it is finished by use
* of the isFinished() flag. This can be used to override the default behavior,
* which is to download until there are no more bytes to lease.
*
*/
public class SwarmHttpSourceDownloader implements SwarmSourceDownloader, NHttpRequestExecutionHandler {
private static final Log LOG = LogFactory.getLog(SwarmHttpSourceDownloader.class);
private final LimeConnectingIOReactor ioReactor;
private final IOEventDispatch eventDispatch;
private final AtomicBoolean started = new AtomicBoolean(false);
private final SwarmCoordinator swarmCoordinator;
private final SwarmStats stats;
public SwarmHttpSourceDownloader(LimeConnectingIOReactorFactory limeConnectingIOReactorFactory, SwarmCoordinator swarmCoordinator, String userAgent) {
this.swarmCoordinator = Objects.nonNull(swarmCoordinator, "swarmCoordinator");
this.stats = new SwarmStats();
HttpParams params = new BasicHttpParams();
params.setIntParameter(CoreConnectionPNames.SO_TIMEOUT, 5000).setIntParameter(
CoreConnectionPNames.CONNECTION_TIMEOUT, 2000).setIntParameter(
CoreConnectionPNames.SOCKET_BUFFER_SIZE, 8 * 1024).setBooleanParameter(
CoreConnectionPNames.STALE_CONNECTION_CHECK, false).setParameter(
CoreProtocolPNames.USER_AGENT, userAgent);
this.ioReactor = limeConnectingIOReactorFactory.createIOReactor(params);
SwarmAsyncNHttpClientHandlerBuilder builder = new SwarmAsyncNHttpClientHandlerBuilder(
params, this);
AsyncNHttpClientHandler clientHandler = builder.get();
eventDispatch = new DefaultClientIOEventDispatch(clientHandler, params);
}
/*
* (non-Javadoc)
*
* @seeorg.limewire.swarm.SwarmSourceHandler#addSource(org.limewire.swarm.
* SwarmSource)
*/
public void addSource(SwarmSource source) {
LOG.tracef("Adding source: {0}", source);
stats.incrementNumberOfSources();
SessionRequestCallback sessionRequestCallback = new SwarmHttpSessionRequestCallback(source);
LOG.tracef("Connecting source to ioReactor: {0}", source);
ioReactor.connect(source.getAddress(), null, new HttpSourceAttachment(source),
sessionRequestCallback);
}
/*
* (non-Javadoc)
*
* @see org.limewire.swarm.SwarmSourceHandler#isComplete()
*/
public boolean isComplete() {
return swarmCoordinator.isComplete();
}
/*
* (non-Javadoc)
*
* @see org.limewire.swarm.SwarmSourceHandler#shutdown()
*/
public void shutdown() throws IOException {
LOG.tracef("Shutting down: {0}", this);
started.set(false);
LOG.tracef("Shutting ioReactor: {0}", ioReactor);
ioReactor.shutdown();
}
/*
* (non-Javadoc)
*
* @see org.limewire.swarm.SwarmSourceHandler#start()
*/
public void start() throws IOException {
LOG.tracef("Starting: {0}", this);
started.set(true);
ioReactor.execute(eventDispatch);
}
/*
* (non-Javadoc)
*
* @see org.limewire.swarm.SwarmSourceHandler#isActive()
*/
public boolean isActive() {
return started.get();
}
/*
* (non-Javadoc)
*
* @see org.limewire.swarm.SwarmSourceHandler#getMeasuredBandwidth(boolean)
*/
public float getMeasuredBandwidth(boolean downstream) {
return ioReactor.getMeasuredBandwidth(downstream);
}
/*
* (non-Javadoc)
*
* @see
* org.apache.http.nio.protocol.NHttpRequestExecutionHandler#initalizeContext
* (org.apache.http.protocol.HttpContext, java.lang.Object)
*/
public void initalizeContext(HttpContext context, Object attachment) {
LOG.tracef("initalizeContext: {0}", this);
HttpSourceAttachment info = (HttpSourceAttachment) attachment;
context.setAttribute(SwarmHttpExecutionContext.HTTP_SWARM_SOURCE, info.getSource());
}
/*
* (non-Javadoc)
*
* @see
* org.apache.http.nio.protocol.NHttpRequestExecutionHandler#finalizeContext
* (org.apache.http.protocol.HttpContext)
*/
public void finalizeContext(HttpContext context) {
LOG.tracef("finalizeContext: {0}", this);
SwarmSource source = getSwarmSource(context);
source.connectionClosed(SwarmHttpSourceDownloader.this);
closeContentListener(context);
}
/*
* (non-Javadoc)
*
* @see
* org.apache.http.nio.protocol.NHttpRequestExecutionHandler#handleResponse
* (org.apache.http.HttpResponse, org.apache.http.protocol.HttpContext)
*/
public void handleResponse(HttpResponse response, HttpContext context) throws IOException {
LOG.tracef("handleResponse: {0}", this);
stats.incrementNumberOfResponses();
if (isActive()) {
SwarmSource source = getSwarmSource(context);
source.responseProcessed(SwarmHttpSourceDownloader.this, new SwarmHttpSourceStatus(
response.getStatusLine()));
if (LOG.isTraceEnabled()) {
LOG.trace(SwarmHttpUtils.logReponse("response", response));
}
int code = response.getStatusLine().getStatusCode();
if (!(code >= 200 && code < 300)) {
closeConnection(context);
}
}
}
/*
* (non-Javadoc)
*
* @see
* org.apache.http.nio.protocol.NHttpRequestExecutionHandler#responseEntity
* (org.apache.http.HttpResponse, org.apache.http.protocol.HttpContext)
*/
public ConsumingNHttpEntity responseEntity(HttpResponse response, HttpContext context)
throws IOException {
LOG.trace(this);
if (LOG.isTraceEnabled()) {
LOG.trace("Handling response: " + response.getStatusLine() + ", headers: "
+ Arrays.asList(response.getAllHeaders()));
}
ResponseContentListener listener = (ResponseContentListener) context
.getAttribute(SwarmHttpExecutionContext.SWARM_RESPONSE_LISTENER);
int code = response.getStatusLine().getStatusCode();
if (code >= 200 && code < 300) {
listener.initialize(response);
context.setAttribute(SwarmHttpExecutionContext.SWARM_RESPONSE_LISTENER, null);
return new ConsumingNHttpEntityTemplate(response.getEntity(), listener);
} else {
listener.finished();
context.setAttribute(SwarmHttpExecutionContext.SWARM_RESPONSE_LISTENER, null);
return null;
}
}
/*
* (non-Javadoc)
*
* @see
* org.apache.http.nio.protocol.NHttpRequestExecutionHandler#submitRequest
* (org.apache.http.protocol.HttpContext)
*/
public HttpRequest submitRequest(HttpContext context) {
LOG.trace(this);
if (isActive()) {
SwarmSource swarmSource = getSwarmSource(context);
HttpRequest request = null;
if (swarmSource.isFinished() || isComplete()) {
if (LOG.isTraceEnabled()) {
LOG.trace("swarmSource.isFinished(): " + swarmSource.isFinished()
+ " isComplete(): " + isComplete());
}
} else {
request = buildRequest(context);
if (LOG.isTraceEnabled()) {
LOG.trace(SwarmHttpUtils.logRequest("submitRequest", request));
}
}
if (request == null) {
LOG
.debugf(
"No more data to request for this swarm source: {0} finishing all listeners then closing connection from context.",
swarmSource);
closeConnection(context);
} else {
stats.incrementNumberOfRequests();
}
return request;
} else {
LOG.warn("submitRequest called while not active!");
closeConnection(context);
return null;
}
}
/**
* Returns the swarmSource from the HttpContext
*/
private SwarmSource getSwarmSource(HttpContext context) {
SwarmSource swarmSource = (SwarmSource) context
.getAttribute(SwarmHttpExecutionContext.HTTP_SWARM_SOURCE);
return swarmSource;
}
/**
* Builds the next request.
*/
private HttpRequest buildRequest(HttpContext context) {
HttpRequest request = null;
SwarmSource source = getSwarmSource(context);
RequestParameters requestParameters = buildRequestParameters(source);
if (requestParameters != null) {
SwarmFile swarmFile = requestParameters.getSwarmFile();
Range leaseRange = requestParameters.getLeaseRange();
Range downloadRange = requestParameters.getDownloadRange();
stats.incrementNumberOfBytesRequested(downloadRange.getLength());
context.setAttribute(SwarmHttpExecutionContext.SWARM_RESPONSE_LISTENER,
new SwarmHttpContentListener(swarmCoordinator, swarmFile, leaseRange));
request = new BasicHttpRequest("GET", requestParameters.getPath());
request.addHeader(new BasicHeader("Range", "bytes=" + requestParameters.getLow() + "-"
+ (requestParameters.getHigh())));
}
return request;
}
private void closeContentListener(HttpContext context) {
if (LOG.isTraceEnabled()) {
LOG.trace("closing content listener: " + this, new Exception("debugging stack trace"));
}
ResponseContentListener contentListener = (ResponseContentListener) context
.getAttribute(SwarmHttpExecutionContext.SWARM_RESPONSE_LISTENER);
if (contentListener != null) {
contentListener.finished();
context.setAttribute(SwarmHttpExecutionContext.SWARM_RESPONSE_LISTENER, null);
}
}
private void closeConnection(HttpContext context) {
if (LOG.isTraceEnabled()) {
LOG.trace("closing connection: " + this, new Exception("debugging stack trace"));
}
closeContentListener(context);
closeSwarmSource(context);
SwarmHttpUtils.closeConnectionFromContext(context);
}
private void closeSwarmSource(HttpContext context) {
if (LOG.isTraceEnabled()) {
LOG.trace("closing swarmSource: " + this, new Exception("debugging stack trace"));
}
SwarmSource swarmSource = getSwarmSource(context);
if (swarmSource != null) {
swarmSource.finished(SwarmHttpSourceDownloader.this);
}
}
private RequestParameters buildRequestParameters(SwarmSource source) {
IntervalSet availableRanges = source.getAvailableRanges();
Range leaseRange = swarmCoordinator.leasePortion(availableRanges);
if (leaseRange == null) {
LOG.debug("No range available to lease.");
return null;
}
SwarmFile swarmFile = swarmCoordinator.getSwarmFile(leaseRange);
long fileEndByte = swarmFile.getEndBytePosition();
if (leaseRange.getHigh() > fileEndByte) {
Range oldRange = leaseRange;
leaseRange = Range.createRange(leaseRange.getLow(), fileEndByte);
leaseRange = swarmCoordinator.renewLease(oldRange, leaseRange);
}
long downloadRangeStart = leaseRange.getLow() - swarmFile.getStartBytePosition();
long downloadRangeEnd = leaseRange.getHigh() - swarmFile.getStartBytePosition();
Range downloadRange = Range.createRange(downloadRangeStart, downloadRangeEnd);
RequestParameters requestParameters = new RequestParameters(swarmFile, source.getPath(),
leaseRange, downloadRange);
return requestParameters;
}
/**
* Class representing the request parameters for a piece download.
*/
private static class RequestParameters {
private final SwarmFile swarmFile;
private final String path;
private final Range leaseRange;
private final Range downloadRange;
public RequestParameters(SwarmFile swarmFile, String path, Range leaseRange,
Range downloadRange) {
super();
this.swarmFile = Objects.nonNull(swarmFile, "swarmFile");
this.path = Objects.nonNull(path, "path");
this.leaseRange = Objects.nonNull(leaseRange, "leaseRange");
this.downloadRange = Objects.nonNull(downloadRange, "downloadRange");
}
public String getPath() {
String path = this.path;
if (path.length() > 0 && path.charAt(path.length() - 1) == '/') {
path += swarmFile.getPath();
}
return path;
}
public long getLow() {
return downloadRange.getLow();
}
public long getHigh() {
return downloadRange.getHigh();
}
public SwarmFile getSwarmFile() {
return swarmFile;
}
public Range getLeaseRange() {
return leaseRange;
}
public Range getDownloadRange() {
return downloadRange;
}
}
/**
* Callback to delegate connection issues back to the swarmsource listeners.
*/
private class SwarmHttpSessionRequestCallback implements SessionRequestCallback {
private final SwarmSource source;
public SwarmHttpSessionRequestCallback(SwarmSource source) {
this.source = source;
}
public void cancelled(SessionRequest request) {
source.connectFailed(SwarmHttpSourceDownloader.this);
};
public void completed(SessionRequest request) {
source.connected(SwarmHttpSourceDownloader.this);
};
public void failed(SessionRequest request) {
source.connectFailed(SwarmHttpSourceDownloader.this);
};
public void timeout(SessionRequest request) {
source.connectFailed(SwarmHttpSourceDownloader.this);
};
}
}