/*
* ====================================================================
*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*/
package org.apache.ogt.http.impl.client;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.ConnectException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.ogt.http.Header;
import org.apache.ogt.http.HttpClientConnection;
import org.apache.ogt.http.HttpException;
import org.apache.ogt.http.HttpHost;
import org.apache.ogt.http.HttpRequest;
import org.apache.ogt.http.HttpRequestInterceptor;
import org.apache.ogt.http.HttpResponse;
import org.apache.ogt.http.HttpStatus;
import org.apache.ogt.http.ProtocolVersion;
import org.apache.ogt.http.client.ClientProtocolException;
import org.apache.ogt.http.client.HttpRequestRetryHandler;
import org.apache.ogt.http.client.NonRepeatableRequestException;
import org.apache.ogt.http.client.methods.AbortableHttpRequest;
import org.apache.ogt.http.client.methods.HttpGet;
import org.apache.ogt.http.client.methods.HttpPost;
import org.apache.ogt.http.client.params.ClientPNames;
import org.apache.ogt.http.conn.ClientConnectionManager;
import org.apache.ogt.http.conn.ClientConnectionRequest;
import org.apache.ogt.http.conn.ConnectionPoolTimeoutException;
import org.apache.ogt.http.conn.ConnectionReleaseTrigger;
import org.apache.ogt.http.conn.ManagedClientConnection;
import org.apache.ogt.http.conn.routing.HttpRoute;
import org.apache.ogt.http.conn.scheme.PlainSocketFactory;
import org.apache.ogt.http.conn.scheme.Scheme;
import org.apache.ogt.http.conn.scheme.SchemeRegistry;
import org.apache.ogt.http.entity.InputStreamEntity;
import org.apache.ogt.http.entity.StringEntity;
import org.apache.ogt.http.impl.client.DefaultHttpClient;
import org.apache.ogt.http.impl.client.DefaultRequestDirector;
import org.apache.ogt.http.impl.client.RequestWrapper;
import org.apache.ogt.http.impl.conn.ClientConnAdapterMockup;
import org.apache.ogt.http.impl.conn.SingleClientConnManager;
import org.apache.ogt.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.ogt.http.localserver.BasicServerTestBase;
import org.apache.ogt.http.localserver.LocalTestServer;
import org.apache.ogt.http.message.BasicHeader;
import org.apache.ogt.http.mockup.SocketFactoryMockup;
import org.apache.ogt.http.params.BasicHttpParams;
import org.apache.ogt.http.params.HttpParams;
import org.apache.ogt.http.protocol.BasicHttpContext;
import org.apache.ogt.http.protocol.ExecutionContext;
import org.apache.ogt.http.protocol.HttpContext;
import org.apache.ogt.http.protocol.HttpRequestExecutor;
import org.apache.ogt.http.protocol.HttpRequestHandler;
import org.apache.ogt.http.util.EntityUtils;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
/**
* Unit tests for {@link DefaultRequestDirector}
*/
public class TestDefaultClientRequestDirector extends BasicServerTestBase {
@Before
public void setUp() throws Exception {
localServer = new LocalTestServer(null, null);
localServer.registerDefaultHandlers();
localServer.start();
}
/**
* Tests that if abort is called on an {@link AbortableHttpRequest} while
* {@link DefaultRequestDirector} is allocating a connection, that the
* connection is properly aborted.
*/
@Test
public void testAbortInAllocate() throws Exception {
CountDownLatch connLatch = new CountDownLatch(1);
CountDownLatch awaitLatch = new CountDownLatch(1);
final ConMan conMan = new ConMan(connLatch, awaitLatch);
final AtomicReference<Throwable> throwableRef = new AtomicReference<Throwable>();
final CountDownLatch getLatch = new CountDownLatch(1);
final DefaultHttpClient client = new DefaultHttpClient(conMan, new BasicHttpParams());
final HttpContext context = new BasicHttpContext();
final HttpGet httpget = new HttpGet("http://www.example.com/a");
new Thread(new Runnable() {
public void run() {
try {
client.execute(httpget, context);
} catch(Throwable t) {
throwableRef.set(t);
} finally {
getLatch.countDown();
}
}
}).start();
Assert.assertTrue("should have tried to get a connection", connLatch.await(1, TimeUnit.SECONDS));
httpget.abort();
Assert.assertTrue("should have finished get request", getLatch.await(1, TimeUnit.SECONDS));
Assert.assertTrue("should be instanceof IOException, was: " + throwableRef.get(),
throwableRef.get() instanceof IOException);
Assert.assertTrue("cause should be InterruptedException, was: " + throwableRef.get().getCause(),
throwableRef.get().getCause() instanceof InterruptedException);
}
/**
* Tests that an abort called after the connection has been retrieved
* but before a release trigger is set does still abort the request.
*/
@Test
public void testAbortAfterAllocateBeforeRequest() throws Exception {
this.localServer.register("*", new BasicService());
CountDownLatch releaseLatch = new CountDownLatch(1);
SchemeRegistry registry = new SchemeRegistry();
registry.register(new Scheme("http", 80, PlainSocketFactory.getSocketFactory()));
SingleClientConnManager conMan = new SingleClientConnManager(registry);
final AtomicReference<Throwable> throwableRef = new AtomicReference<Throwable>();
final CountDownLatch getLatch = new CountDownLatch(1);
final DefaultHttpClient client = new DefaultHttpClient(conMan, new BasicHttpParams());
final HttpContext context = new BasicHttpContext();
final HttpGet httpget = new CustomGet("a", releaseLatch);
new Thread(new Runnable() {
public void run() {
try {
client.execute(getServerHttp(), httpget, context);
} catch(Throwable t) {
throwableRef.set(t);
} finally {
getLatch.countDown();
}
}
}).start();
Thread.sleep(100); // Give it a little time to proceed to release...
httpget.abort();
releaseLatch.countDown();
Assert.assertTrue("should have finished get request", getLatch.await(1, TimeUnit.SECONDS));
Assert.assertTrue("should be instanceof IOException, was: " + throwableRef.get(),
throwableRef.get() instanceof IOException);
}
/**
* Tests that an abort called completely before execute
* still aborts the request.
*/
@Test
public void testAbortBeforeExecute() throws Exception {
this.localServer.register("*", new BasicService());
SchemeRegistry registry = new SchemeRegistry();
registry.register(new Scheme("http", 80, PlainSocketFactory.getSocketFactory()));
SingleClientConnManager conMan = new SingleClientConnManager(registry);
final AtomicReference<Throwable> throwableRef = new AtomicReference<Throwable>();
final CountDownLatch getLatch = new CountDownLatch(1);
final CountDownLatch startLatch = new CountDownLatch(1);
final DefaultHttpClient client = new DefaultHttpClient(conMan, new BasicHttpParams());
final HttpContext context = new BasicHttpContext();
final HttpGet httpget = new HttpGet("a");
new Thread(new Runnable() {
public void run() {
try {
try {
if(!startLatch.await(1, TimeUnit.SECONDS))
throw new RuntimeException("Took too long to start!");
} catch(InterruptedException interrupted) {
throw new RuntimeException("Never started!", interrupted);
}
client.execute(getServerHttp(), httpget, context);
} catch(Throwable t) {
throwableRef.set(t);
} finally {
getLatch.countDown();
}
}
}).start();
httpget.abort();
startLatch.countDown();
Assert.assertTrue("should have finished get request", getLatch.await(1, TimeUnit.SECONDS));
Assert.assertTrue("should be instanceof IOException, was: " + throwableRef.get(),
throwableRef.get() instanceof IOException);
}
/**
* Tests that an abort called after a redirect has found a new host
* still aborts in the correct place (while trying to get the new
* host's route, not while doing the subsequent request).
*/
@Test
public void testAbortAfterRedirectedRoute() throws Exception {
final int port = this.localServer.getServiceAddress().getPort();
this.localServer.register("*", new BasicRedirectService(port));
SchemeRegistry registry = new SchemeRegistry();
registry.register(new Scheme("http", 80, PlainSocketFactory.getSocketFactory()));
CountDownLatch connLatch = new CountDownLatch(1);
CountDownLatch awaitLatch = new CountDownLatch(1);
ConnMan4 conMan = new ConnMan4(registry, connLatch, awaitLatch);
final AtomicReference<Throwable> throwableRef = new AtomicReference<Throwable>();
final CountDownLatch getLatch = new CountDownLatch(1);
final DefaultHttpClient client = new DefaultHttpClient(conMan, new BasicHttpParams());
final HttpContext context = new BasicHttpContext();
final HttpGet httpget = new HttpGet("a");
new Thread(new Runnable() {
public void run() {
try {
HttpHost host = new HttpHost("127.0.0.1", port);
client.execute(host, httpget, context);
} catch(Throwable t) {
throwableRef.set(t);
} finally {
getLatch.countDown();
}
}
}).start();
Assert.assertTrue("should have tried to get a connection", connLatch.await(1, TimeUnit.SECONDS));
httpget.abort();
Assert.assertTrue("should have finished get request", getLatch.await(1, TimeUnit.SECONDS));
Assert.assertTrue("should be instanceof IOException, was: " + throwableRef.get(),
throwableRef.get() instanceof IOException);
Assert.assertTrue("cause should be InterruptedException, was: " + throwableRef.get().getCause(),
throwableRef.get().getCause() instanceof InterruptedException);
}
/**
* Tests that if a socket fails to connect, the allocated connection is
* properly released back to the connection manager.
*/
@Test
public void testSocketConnectFailureReleasesConnection() throws Exception {
final ConnMan2 conMan = new ConnMan2();
final DefaultHttpClient client = new DefaultHttpClient(conMan, new BasicHttpParams());
final HttpContext context = new BasicHttpContext();
final HttpGet httpget = new HttpGet("http://www.example.com/a");
try {
client.execute(httpget, context);
Assert.fail("expected IOException");
} catch(IOException expected) {}
Assert.assertNotNull(conMan.allocatedConnection);
Assert.assertSame(conMan.allocatedConnection, conMan.releasedConnection);
}
@Test
public void testRequestFailureReleasesConnection() throws Exception {
this.localServer.register("*", new ThrowingService());
SchemeRegistry registry = new SchemeRegistry();
registry.register(new Scheme("http", 80, PlainSocketFactory.getSocketFactory()));
ConnMan3 conMan = new ConnMan3(registry);
DefaultHttpClient client = new DefaultHttpClient(conMan, new BasicHttpParams());
HttpGet httpget = new HttpGet("/a");
try {
client.execute(getServerHttp(), httpget);
Assert.fail("expected IOException");
} catch (IOException expected) {}
Assert.assertNotNull(conMan.allocatedConnection);
Assert.assertSame(conMan.allocatedConnection, conMan.releasedConnection);
}
private static class ThrowingService implements HttpRequestHandler {
public void handle(
final HttpRequest request,
final HttpResponse response,
final HttpContext context) throws HttpException, IOException {
throw new IOException();
}
}
private static class BasicService implements HttpRequestHandler {
public void handle(final HttpRequest request,
final HttpResponse response,
final HttpContext context) throws HttpException, IOException {
response.setStatusCode(200);
response.setEntity(new StringEntity("Hello World"));
}
}
private static class BasicRedirectService implements HttpRequestHandler {
private int statuscode = HttpStatus.SC_SEE_OTHER;
private int port;
public BasicRedirectService(int port) {
this.port = port;
}
public void handle(final HttpRequest request,
final HttpResponse response, final HttpContext context)
throws HttpException, IOException {
ProtocolVersion ver = request.getRequestLine().getProtocolVersion();
response.setStatusLine(ver, this.statuscode);
response.addHeader(new BasicHeader("Location", "http://localhost:"
+ this.port + "/newlocation/"));
response.addHeader(new BasicHeader("Connection", "close"));
}
}
private static class ConnMan4 extends ThreadSafeClientConnManager {
private final CountDownLatch connLatch;
private final CountDownLatch awaitLatch;
public ConnMan4(SchemeRegistry schreg,
CountDownLatch connLatch, CountDownLatch awaitLatch) {
super(schreg);
this.connLatch = connLatch;
this.awaitLatch = awaitLatch;
}
@Override
public ClientConnectionRequest requestConnection(HttpRoute route, Object state) {
// If this is the redirect route, stub the return value
// so-as to pretend the host is waiting on a slot...
if(route.getTargetHost().getHostName().equals("localhost")) {
final Thread currentThread = Thread.currentThread();
return new ClientConnectionRequest() {
public void abortRequest() {
currentThread.interrupt();
}
public ManagedClientConnection getConnection(
long timeout, TimeUnit tunit)
throws InterruptedException,
ConnectionPoolTimeoutException {
connLatch.countDown(); // notify waiter that we're getting a connection
// zero usually means sleep forever, but CountDownLatch doesn't interpret it that way.
if(timeout == 0)
timeout = Integer.MAX_VALUE;
if(!awaitLatch.await(timeout, tunit))
throw new ConnectionPoolTimeoutException();
return new ClientConnAdapterMockup(ConnMan4.this);
}
};
} else {
return super.requestConnection(route, state);
}
}
}
private static class ConnMan3 extends SingleClientConnManager {
private ManagedClientConnection allocatedConnection;
private ManagedClientConnection releasedConnection;
public ConnMan3(SchemeRegistry schreg) {
super(schreg);
}
@Override
public ManagedClientConnection getConnection(HttpRoute route, Object state) {
allocatedConnection = super.getConnection(route, state);
return allocatedConnection;
}
@Override
public void releaseConnection(ManagedClientConnection conn, long validDuration, TimeUnit timeUnit) {
releasedConnection = conn;
super.releaseConnection(conn, validDuration, timeUnit);
}
}
static class ConnMan2 implements ClientConnectionManager {
private ManagedClientConnection allocatedConnection;
private ManagedClientConnection releasedConnection;
public ConnMan2() {
}
public void closeIdleConnections(long idletime, TimeUnit tunit) {
throw new UnsupportedOperationException("just a mockup");
}
public void closeExpiredConnections() {
throw new UnsupportedOperationException("just a mockup");
}
public ManagedClientConnection getConnection(HttpRoute route) {
throw new UnsupportedOperationException("just a mockup");
}
public ManagedClientConnection getConnection(HttpRoute route,
long timeout, TimeUnit tunit) {
throw new UnsupportedOperationException("just a mockup");
}
public ClientConnectionRequest requestConnection(
final HttpRoute route,
final Object state) {
return new ClientConnectionRequest() {
public void abortRequest() {
throw new UnsupportedOperationException("just a mockup");
}
public ManagedClientConnection getConnection(
long timeout, TimeUnit unit)
throws InterruptedException,
ConnectionPoolTimeoutException {
allocatedConnection = new ClientConnAdapterMockup(ConnMan2.this) {
@Override
public void open(HttpRoute route, HttpContext context,
HttpParams params) throws IOException {
throw new ConnectException();
}
};
return allocatedConnection;
}
};
}
public HttpParams getParams() {
throw new UnsupportedOperationException("just a mockup");
}
public SchemeRegistry getSchemeRegistry() {
SchemeRegistry registry = new SchemeRegistry();
registry.register(new Scheme("http", 80, new SocketFactoryMockup(null)));
return registry;
}
public void releaseConnection(ManagedClientConnection conn, long validDuration, TimeUnit timeUnit) {
this.releasedConnection = conn;
}
public void shutdown() {
throw new UnsupportedOperationException("just a mockup");
}
}
static class ConMan implements ClientConnectionManager {
private final CountDownLatch connLatch;
private final CountDownLatch awaitLatch;
public ConMan(CountDownLatch connLatch, CountDownLatch awaitLatch) {
this.connLatch = connLatch;
this.awaitLatch = awaitLatch;
}
public void closeIdleConnections(long idletime, TimeUnit tunit) {
throw new UnsupportedOperationException("just a mockup");
}
public void closeExpiredConnections() {
throw new UnsupportedOperationException("just a mockup");
}
public ManagedClientConnection getConnection(HttpRoute route) {
throw new UnsupportedOperationException("just a mockup");
}
public ManagedClientConnection getConnection(HttpRoute route,
long timeout, TimeUnit tunit) {
throw new UnsupportedOperationException("just a mockup");
}
public ClientConnectionRequest requestConnection(
final HttpRoute route,
final Object state) {
final Thread currentThread = Thread.currentThread();
return new ClientConnectionRequest() {
public void abortRequest() {
currentThread.interrupt();
}
public ManagedClientConnection getConnection(
long timeout, TimeUnit tunit)
throws InterruptedException,
ConnectionPoolTimeoutException {
connLatch.countDown(); // notify waiter that we're getting a connection
// zero usually means sleep forever, but CountDownLatch doesn't interpret it that way.
if(timeout == 0)
timeout = Integer.MAX_VALUE;
if(!awaitLatch.await(timeout, tunit))
throw new ConnectionPoolTimeoutException();
return new ClientConnAdapterMockup(ConMan.this);
}
};
}
public HttpParams getParams() {
throw new UnsupportedOperationException("just a mockup");
}
public SchemeRegistry getSchemeRegistry() {
SchemeRegistry registry = new SchemeRegistry();
registry.register(new Scheme("http", 80, new SocketFactoryMockup(null)));
return registry;
}
public void releaseConnection(ManagedClientConnection conn, long validDuration, TimeUnit timeUnit) {
throw new UnsupportedOperationException("just a mockup");
}
public void shutdown() {
throw new UnsupportedOperationException("just a mockup");
}
}
private static class CustomGet extends HttpGet {
private final CountDownLatch releaseTriggerLatch;
public CustomGet(String uri, CountDownLatch releaseTriggerLatch) {
super(uri);
this.releaseTriggerLatch = releaseTriggerLatch;
}
@Override
public void setReleaseTrigger(ConnectionReleaseTrigger releaseTrigger) throws IOException {
try {
if(!releaseTriggerLatch.await(1, TimeUnit.SECONDS))
throw new RuntimeException("Waited too long...");
} catch(InterruptedException ie) {
throw new RuntimeException(ie);
}
super.setReleaseTrigger(releaseTrigger);
}
}
private static class SimpleService implements HttpRequestHandler {
public SimpleService() {
super();
}
public void handle(
final HttpRequest request,
final HttpResponse response,
final HttpContext context) throws HttpException, IOException {
response.setStatusCode(HttpStatus.SC_OK);
StringEntity entity = new StringEntity("Whatever");
response.setEntity(entity);
}
}
@Test
public void testDefaultHostAtClientLevel() throws Exception {
int port = this.localServer.getServiceAddress().getPort();
this.localServer.register("*", new SimpleService());
HttpHost target = new HttpHost("localhost", port);
DefaultHttpClient client = new DefaultHttpClient();
client.getParams().setParameter(ClientPNames.DEFAULT_HOST, target);
String s = "/path";
HttpGet httpget = new HttpGet(s);
HttpResponse response = client.execute(httpget);
EntityUtils.consume(response.getEntity());
Assert.assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode());
}
@Test
public void testDefaultHostAtRequestLevel() throws Exception {
int port = this.localServer.getServiceAddress().getPort();
this.localServer.register("*", new SimpleService());
HttpHost target1 = new HttpHost("whatever", 80);
HttpHost target2 = new HttpHost("localhost", port);
DefaultHttpClient client = new DefaultHttpClient();
client.getParams().setParameter(ClientPNames.DEFAULT_HOST, target1);
String s = "/path";
HttpGet httpget = new HttpGet(s);
httpget.getParams().setParameter(ClientPNames.DEFAULT_HOST, target2);
HttpResponse response = client.execute(httpget);
EntityUtils.consume(response.getEntity());
Assert.assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode());
}
private static class FaultyHttpRequestExecutor extends HttpRequestExecutor {
private static final String MARKER = "marker";
private final String failureMsg;
public FaultyHttpRequestExecutor(String failureMsg) {
this.failureMsg = failureMsg;
}
@Override
public HttpResponse execute(
final HttpRequest request,
final HttpClientConnection conn,
final HttpContext context) throws IOException, HttpException {
HttpResponse response = super.execute(request, conn, context);
Object marker = context.getAttribute(MARKER);
if (marker == null) {
context.setAttribute(MARKER, Boolean.TRUE);
throw new IOException(failureMsg);
}
return response;
}
}
private static class FaultyHttpClient extends DefaultHttpClient {
private final String failureMsg;
public FaultyHttpClient() {
this("Oppsie");
}
public FaultyHttpClient(String failureMsg) {
this.failureMsg = failureMsg;
}
@Override
protected HttpRequestExecutor createRequestExecutor() {
return new FaultyHttpRequestExecutor(failureMsg);
}
}
@Test
public void testAutoGeneratedHeaders() throws Exception {
int port = this.localServer.getServiceAddress().getPort();
this.localServer.register("*", new SimpleService());
FaultyHttpClient client = new FaultyHttpClient();
client.addRequestInterceptor(new HttpRequestInterceptor() {
public void process(
final HttpRequest request,
final HttpContext context) throws HttpException, IOException {
request.addHeader("my-header", "stuff");
}
}) ;
client.setHttpRequestRetryHandler(new HttpRequestRetryHandler() {
public boolean retryRequest(
final IOException exception,
int executionCount,
final HttpContext context) {
return true;
}
});
HttpContext context = new BasicHttpContext();
String s = "http://localhost:" + port;
HttpGet httpget = new HttpGet(s);
HttpResponse response = client.execute(getServerHttp(), httpget, context);
EntityUtils.consume(response.getEntity());
HttpRequest reqWrapper = (HttpRequest) context.getAttribute(
ExecutionContext.HTTP_REQUEST);
Assert.assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode());
Assert.assertTrue(reqWrapper instanceof RequestWrapper);
Header[] myheaders = reqWrapper.getHeaders("my-header");
Assert.assertNotNull(myheaders);
Assert.assertEquals(1, myheaders.length);
}
@Test(expected=ClientProtocolException.class)
public void testNonRepeatableEntity() throws Exception {
int port = this.localServer.getServiceAddress().getPort();
this.localServer.register("*", new SimpleService());
String failureMsg = "a message showing that this failed";
FaultyHttpClient client = new FaultyHttpClient(failureMsg);
client.setHttpRequestRetryHandler(new HttpRequestRetryHandler() {
public boolean retryRequest(
final IOException exception,
int executionCount,
final HttpContext context) {
return true;
}
});
HttpContext context = new BasicHttpContext();
String s = "http://localhost:" + port;
HttpPost httppost = new HttpPost(s);
httppost.setEntity(new InputStreamEntity(
new ByteArrayInputStream(
new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 } ),
-1));
try {
client.execute(getServerHttp(), httppost, context);
} catch (ClientProtocolException ex) {
Assert.assertTrue(ex.getCause() instanceof NonRepeatableRequestException);
NonRepeatableRequestException nonRepeat = (NonRepeatableRequestException)ex.getCause();
Assert.assertTrue(nonRepeat.getCause() instanceof IOException);
Assert.assertEquals(failureMsg, nonRepeat.getCause().getMessage());
throw ex;
}
}
}