package fr.gael.dhus.util.http;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.InterruptibleChannel;
import java.nio.channels.WritableByteChannel;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import org.apache.http.HttpException;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
import org.apache.http.impl.nio.client.HttpAsyncClients;
import org.apache.http.nio.IOControl;
import org.apache.http.nio.client.methods.AsyncByteConsumer;
import org.apache.http.nio.client.methods.HttpAsyncMethods;
import org.apache.http.nio.protocol.HttpAsyncRequestProducer;
import org.apache.http.protocol.HttpContext;
/**
* An interruptible HTTP client using Apache HttpComponents' async HTTP
* client.<br>
* This class have interruptible methods that will return as soon as their
* running
* thread is interrupted.<br>
* This class only use interruptible channels from
* {@link java.nio.channels}.<br>
*
* @see
* <a href="https://hc.apache.org/httpcomponents-asyncclient-4.1.x/">HttpComponents:
* async client</a>.
*/
public class InterruptibleHttpClient
{
/** An HttpClient producer. */
private final HttpAsyncClientProducer clientProducer;
/** An InterruptibleHttpClient usign {@code HttpAsyncClients.createDefault()}
* as HttpAsyncClientProducer. */
public InterruptibleHttpClient ()
{
clientProducer = new HttpAsyncClientProducer ()
{
@Override
public CloseableHttpAsyncClient generateClient ()
{
CloseableHttpAsyncClient res = HttpAsyncClients.createDefault ();
res.start ();
return res;
}
};
}
/**
* An InterruptibleHttpClient using the given HttpAsyncClientProducer.
*
* @param clientProducer a custom HttpAsyncClientProducer.
*/
public InterruptibleHttpClient (HttpAsyncClientProducer clientProducer)
{
this.clientProducer = clientProducer;
}
/**
* Performs the given request, writes the content into the given channel.
*
* @param <IWC> a generic type for any classe that implements
* InterruptibleChannel and WritableByteChannel.
* @param request to perform.
* @param output written with the content of the HTTP response.
*
* @return a response (contains the HTTP Headers, the status code, ...).
*
* @throws IOException IO error.
* @throws InterruptedException interrupted.
* @throws RuntimeException containing the actual exception if it is not an
* instance of IOException.
*/
public <IWC extends InterruptibleChannel & WritableByteChannel>
HttpResponse interruptibleRequest(HttpUriRequest request, final IWC output)
throws IOException, InterruptedException
{
// Creates a new client for each request, because we want to close it to interrupt the request.
try (CloseableHttpAsyncClient httpClient = clientProducer.generateClient())
{
HttpAsyncRequestProducer producer = HttpAsyncMethods.create(request);
// Creates a consumer callback that is called each time bytes are received
AsyncByteConsumer<HttpResponse> consumer = new AsyncByteConsumer<HttpResponse>()
{
HttpResponse response = null;
@Override
protected void onByteReceived(ByteBuffer buf, IOControl ioctrl) throws IOException
{
output.write(buf);
}
@Override
protected void onResponseReceived(HttpResponse response)
throws HttpException, IOException
{
this.response = response;
}
@Override
protected HttpResponse buildResult(HttpContext context) throws Exception
{
return response;
}
};
Future<HttpResponse> future = httpClient.execute(producer, consumer, null);
try
{
// Blocks until the download is done, interruptible,
// if interrupted, will close the HttpClient, the download will be interrupted
return future.get();
}
catch (ExecutionException e)
{
// an error occured while producing the Future<HttpResponse>
Throwable t = e.getCause();
// output.write throws only instances of IOException
if (t instanceof IOException)
{
throw (IOException) t;
}
throw new RuntimeException(t);
}
}
}
/**
* Gets the given URL, writes the content into the given channel.
*
* @param <IWC> a generic type for any classe that implements
* InterruptibleChannel and WritableByteChannel.
* @param url to get.
* @param output written with the content of the HTTP response.
*
* @return a response (contains the HTTP Headers, the status code, ...).
*
* @throws IOException IO error.
* @throws InterruptedException interrupted.
* @throws RuntimeException containing the actual exception if it is not an
* instance of IOException.
*/
public <IWC extends InterruptibleChannel & WritableByteChannel>
HttpResponse interruptibleGet(String url, final IWC output)
throws IOException, InterruptedException
{
return interruptibleRequest(new HttpGet(url), output);
}
/**
* Deletes the given URL, writes the content into the given channel.
*
* @param <IWC> a generic type for any classe that implements
* InterruptibleChannel and WritableByteChannel.
* @param url to delete.
* @param output written with the content of the HTTP response.
*
* @return a response (contains the HTTP Headers, the status code, ...).
*
* @throws IOException IO error.
* @throws InterruptedException interrupted.
* @throws RuntimeException containing the actual exception if it is not an
* instance of IOException.
*/
public <IWC extends InterruptibleChannel & WritableByteChannel>
HttpResponse interruptibleDelete(String url, final IWC output)
throws IOException, InterruptedException
{
return interruptibleRequest(new HttpDelete(url), output);
}
/** A null interruptible and writable sink channel. */
public static class NullIWC implements InterruptibleChannel, WritableByteChannel
{
private boolean open = true;
@Override
public void close() throws IOException
{
this.open = false;
}
@Override
public boolean isOpen()
{
return open;
}
@Override
public int write(ByteBuffer src) throws IOException
{
if (!open)
{
throw new ClosedChannelException();
}
// Pretends `src.remaining()` bytes have been read from src
int res = src.remaining();
src.position(src.limit());
return res;
}
}
/** An interruptible, writable channel that writes to an in-memory byte array. */
public static class MemoryIWC implements InterruptibleChannel, WritableByteChannel
{
private boolean open = true;
private final ByteArrayOutputStream byteArrayOS = new ByteArrayOutputStream();
private final WritableByteChannel byteChannel = Channels.newChannel(byteArrayOS);
@Override
public void close() throws IOException
{
this.open = false;
this.byteChannel.close();
this.byteArrayOS.close();
}
@Override
public boolean isOpen()
{
return open;
}
@Override
public int write(ByteBuffer src) throws IOException
{
if (!open)
{
throw new ClosedChannelException();
}
return byteChannel.write(src);
}
/**
* Returns a copy of the underlying byte array.
* @return byte array of written data.
*/
public byte[] getBytes()
{
return byteArrayOS.toByteArray();
}
}
}