Refactored execution proxy classes; entity enclosing requests are enhanced only when necessary

git-svn-id: https://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk@1416177 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Oleg Kalnichevski 2012-12-02 14:03:52 +00:00
parent a2a5d87980
commit 03769ceecc
12 changed files with 184 additions and 161 deletions

View File

@ -42,8 +42,8 @@
* <p/>
* Important: please note it is required for decorators that implement post execution aspects
* or response post-processing of any sort to release resources associated with the response
* by calling {@link HttpResponseProxy#close()} methods in case of an I/O, protocol or runtime
* exception, or in case the response is not propagated to the caller.
* by calling {@link CloseableHttpResponse#close()} methods in case of an I/O, protocol or
* runtime exception, or in case the response is not propagated to the caller.
*
* @since 4.3
*/

View File

@ -27,6 +27,7 @@
package org.apache.http.impl.client.execchain;
import java.io.Closeable;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
@ -43,7 +44,7 @@
* @since 4.3
*/
@ThreadSafe
class ConnectionReleaseTriggerImpl implements ConnectionReleaseTrigger, Cancellable {
class ConnectionReleaseTriggerImpl implements ConnectionReleaseTrigger, Cancellable, Closeable {
private final Log log;
@ -145,4 +146,8 @@ public boolean isReleased() {
return this.released;
}
public void close() throws IOException {
abortConnection();
}
}

View File

@ -0,0 +1,67 @@
package org.apache.http.impl.client.execchain;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.annotation.NotThreadSafe;
import org.apache.http.client.methods.CloseableHttpResponse;
/**
* Execution proxies for HTTP message objects.
*
* @since 4.3
*/
@NotThreadSafe
class ExecProxies {
static void enhanceEntity(final HttpEntityEnclosingRequest request) {
HttpEntity entity = request.getEntity();
if (entity != null && !entity.isRepeatable() && !isEnhanced(entity)) {
HttpEntity proxy = (HttpEntity) Proxy.newProxyInstance(
HttpEntity.class.getClassLoader(),
new Class<?>[] { HttpEntity.class },
new RequestEntityExecHandler(entity));
request.setEntity(proxy);
}
}
static boolean isEnhanced(final HttpEntity entity) {
if (entity != null && Proxy.isProxyClass(entity.getClass())) {
InvocationHandler handler = Proxy.getInvocationHandler(entity);
return handler instanceof RequestEntityExecHandler;
} else {
return false;
}
}
static boolean isRepeatable(final HttpRequest request) {
if (request instanceof HttpEntityEnclosingRequest) {
HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity();
if (entity != null) {
if (isEnhanced(entity)) {
RequestEntityExecHandler handler = (RequestEntityExecHandler)
Proxy.getInvocationHandler(entity);
if (!handler.isConsumed()) {
return true;
}
}
return entity.isRepeatable();
}
}
return true;
}
public static CloseableHttpResponse enhanceResponse(
final HttpResponse original,
final ConnectionReleaseTriggerImpl connReleaseTrigger) {
return (CloseableHttpResponse) Proxy.newProxyInstance(
ResponseProxyHandler.class.getClassLoader(),
new Class<?>[] { CloseableHttpResponse.class },
new ResponseProxyHandler(original, connReleaseTrigger));
}
}

View File

@ -35,6 +35,7 @@
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpRequest;
import org.apache.http.HttpVersion;
import org.apache.http.ProtocolException;
import org.apache.http.ProtocolVersion;
import org.apache.http.RequestLine;
@ -53,12 +54,15 @@
public class HttpRequestWrapper extends AbstractHttpMessage implements HttpRequest {
private final HttpRequest original;
private final String method;
private ProtocolVersion version;
private URI uri;
private HttpRequestWrapper(final HttpRequest request) {
super();
this.original = request;
this.version = this.original.getRequestLine().getProtocolVersion();
this.method = this.original.getRequestLine().getMethod();
if (request instanceof HttpUriRequest) {
this.uri = ((HttpUriRequest) request).getURI();
} else {
@ -71,6 +75,10 @@ public ProtocolVersion getProtocolVersion() {
return this.original.getProtocolVersion();
}
public void setProtocolVersion(final ProtocolVersion version) {
this.version = version;
}
public URI getURI() {
return this.uri;
}
@ -80,28 +88,23 @@ public void setURI(final URI uri) {
}
public RequestLine getRequestLine() {
ProtocolVersion version = this.original.getRequestLine().getProtocolVersion();
String method = this.original.getRequestLine().getMethod();
String uritext = null;
String requestUri = null;
if (this.uri != null) {
uritext = this.uri.toASCIIString();
requestUri = this.uri.toASCIIString();
} else {
uritext = this.original.getRequestLine().getUri();
requestUri = this.original.getRequestLine().getUri();
}
if (uritext == null || uritext.length() == 0) {
uritext = "/";
if (requestUri == null || requestUri.length() == 0) {
requestUri = "/";
}
return new BasicRequestLine(method, uritext, version);
ProtocolVersion version = this.version != null ? this.version : HttpVersion.HTTP_1_1;
return new BasicRequestLine(this.method, requestUri, version);
}
public HttpRequest getOriginal() {
return this.original;
}
public boolean isRepeatable() {
return true;
}
@Override
public String toString() {
return getRequestLine() + " " + this.headergroup;
@ -115,7 +118,7 @@ static class HttpEntityEnclosingRequestWrapper extends HttpRequestWrapper
public HttpEntityEnclosingRequestWrapper(final HttpEntityEnclosingRequest request)
throws ProtocolException {
super(request);
setEntity(request.getEntity());
this.entity = request.getEntity();
}
public HttpEntity getEntity() {
@ -123,7 +126,7 @@ public HttpEntity getEntity() {
}
public void setEntity(final HttpEntity entity) {
this.entity = entity != null ? new RequestEntityWrapper(entity) : null;
this.entity = entity;
}
public boolean expectContinue() {
@ -131,11 +134,6 @@ public boolean expectContinue() {
return expect != null && HTTP.EXPECT_CONTINUE.equalsIgnoreCase(expect.getValue());
}
@Override
public boolean isRepeatable() {
return this.entity == null || this.entity.isRepeatable();
}
}
public static HttpRequestWrapper wrap(final HttpRequest request) throws ProtocolException {

View File

@ -36,6 +36,7 @@
import org.apache.http.ConnectionReuseStrategy;
import org.apache.http.HttpClientConnection;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpException;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
@ -164,6 +165,10 @@ public CloseableHttpResponse execute(
context.setAttribute(ClientContext.PROXY_AUTH_STATE, proxyAuthState);
}
if (request instanceof HttpEntityEnclosingRequest) {
ExecProxies.enhanceEntity((HttpEntityEnclosingRequest) request);
}
Object userToken = context.getUserToken();
final ConnectionRequest connRequest = connManager.requestConnection(route, userToken);
@ -214,7 +219,7 @@ public CloseableHttpResponse execute(
HttpResponse response = null;
for (int execCount = 1;; execCount++) {
if (execCount > 1 && !request.isRepeatable()) {
if (execCount > 1 && !ExecProxies.isRepeatable(request)) {
throw new NonRepeatableRequestException("Cannot retry request " +
"with a non-repeatable request entity.");
}
@ -327,9 +332,9 @@ public CloseableHttpResponse execute(
if (entity == null || !entity.isStreaming()) {
// connection not needed and (assumed to be) in re-usable state
releaseTrigger.releaseConnection();
return HttpResponseProxy.newProxy(response, null);
return ExecProxies.enhanceResponse(response, null);
} else {
return HttpResponseProxy.newProxy(response, releaseTrigger);
return ExecProxies.enhanceResponse(response, releaseTrigger);
}
} catch (ConnectionShutdownException ex) {
InterruptedIOException ioex = new InterruptedIOException(

View File

@ -32,6 +32,7 @@
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpException;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
@ -116,6 +117,9 @@ public CloseableHttpResponse execute(
HttpRequest original = currentRequest.getOriginal();
currentRequest = HttpRequestWrapper.wrap(redirect);
currentRequest.setHeaders(original.getAllHeaders());
if (original instanceof HttpEntityEnclosingRequest) {
ExecProxies.enhanceEntity((HttpEntityEnclosingRequest) request);
}
URI uri = currentRequest.getURI();
HttpHost newTarget = URIUtils.extractHost(uri);

View File

@ -0,0 +1,62 @@
package org.apache.http.impl.client.execchain;
import java.io.OutputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import org.apache.http.HttpEntity;
import org.apache.http.annotation.NotThreadSafe;
/**
* A wrapper class for {@link HttpEntity} enclosed in a request message.
*
* @since 4.3
*/
@NotThreadSafe
class RequestEntityExecHandler implements InvocationHandler {
private static final Method WRITE_TO_METHOD;
static {
try {
WRITE_TO_METHOD = HttpEntity.class.getMethod("writeTo", OutputStream.class);
} catch (NoSuchMethodException ex) {
throw new Error(ex);
}
}
private final HttpEntity original;
private boolean consumed = false;
RequestEntityExecHandler(final HttpEntity original) {
super();
this.original = original;
}
public HttpEntity getOriginal() {
return original;
}
public boolean isConsumed() {
return consumed;
}
public Object invoke(
final Object proxy, final Method method, final Object[] args) throws Throwable {
try {
if (method.equals(WRITE_TO_METHOD)) {
this.consumed = true;
}
return method.invoke(original, args);
} catch (InvocationTargetException ex) {
Throwable cause = ex.getCause();
if (cause != null) {
throw cause;
} else {
throw ex;
}
}
}
}

View File

@ -1,50 +0,0 @@
package org.apache.http.impl.client.execchain;
import java.io.IOException;
import java.io.OutputStream;
import org.apache.http.HttpEntity;
import org.apache.http.annotation.NotThreadSafe;
import org.apache.http.entity.HttpEntityWrapper;
/**
* A wrapper class for {@link HttpEntity} enclosed in a request message.
*
* @since 4.3
*/
@NotThreadSafe
class RequestEntityWrapper extends HttpEntityWrapper {
private boolean consumed = false;
RequestEntityWrapper(final HttpEntity entity) {
super(entity);
}
@Override
public boolean isRepeatable() {
if (!this.consumed) {
return true;
} else {
return super.isRepeatable();
}
}
@Deprecated
@Override
public void consumeContent() throws IOException {
consumed = true;
super.consumeContent();
}
@Override
public void writeTo(final OutputStream outstream) throws IOException {
consumed = true;
super.writeTo(outstream);
}
public boolean isConsumed() {
return consumed;
}
}

View File

@ -27,16 +27,15 @@
package org.apache.http.impl.client.execchain;
import java.io.Closeable;
import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.annotation.NotThreadSafe;
import org.apache.http.client.methods.CloseableHttpResponse;
/**
* A proxy class for {@link HttpResponse} that can be used to release client connection
@ -45,12 +44,22 @@
* @since 4.3
*/
@NotThreadSafe
class HttpResponseProxy implements InvocationHandler {
class ResponseProxyHandler implements InvocationHandler {
private static final Method CLOSE_METHOD;
static {
try {
CLOSE_METHOD = Closeable.class.getMethod("close");
} catch (NoSuchMethodException ex) {
throw new Error(ex);
}
}
private final HttpResponse original;
private final ConnectionReleaseTriggerImpl connReleaseTrigger;
private HttpResponseProxy(
ResponseProxyHandler(
final HttpResponse original,
final ConnectionReleaseTriggerImpl connReleaseTrigger) {
super();
@ -70,8 +79,7 @@ public void close() throws IOException {
public Object invoke(
final Object proxy, final Method method, final Object[] args) throws Throwable {
String mname = method.getName();
if (mname.equals("close")) {
if (method.equals(CLOSE_METHOD)) {
close();
return null;
} else {
@ -88,13 +96,4 @@ public Object invoke(
}
}
public static CloseableHttpResponse newProxy(
final HttpResponse original,
final ConnectionReleaseTriggerImpl connReleaseTrigger) {
return (CloseableHttpResponse) Proxy.newProxyInstance(
HttpResponseProxy.class.getClassLoader(),
new Class<?>[] { CloseableHttpResponse.class },
new HttpResponseProxy(original, connReleaseTrigger));
}
}

View File

@ -33,13 +33,11 @@
import org.apache.commons.logging.LogFactory;
import org.apache.http.Header;
import org.apache.http.HttpException;
import org.apache.http.HttpRequest;
import org.apache.http.annotation.Immutable;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.NonRepeatableRequestException;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpExecutionAware;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.conn.routing.HttpRoute;
@ -86,8 +84,7 @@ public CloseableHttpResponse execute(
try {
return this.requestExecutor.execute(route, request, context, execAware);
} catch (IOException ex) {
HttpRequest original = request.getOriginal();
if (original instanceof HttpUriRequest && ((HttpUriRequest) original).isAborted()) {
if (execAware != null && execAware.isAborted()) {
this.log.debug("Request has been aborted");
throw ex;
}
@ -100,7 +97,7 @@ public CloseableHttpResponse execute(
if (this.log.isDebugEnabled()) {
this.log.debug(ex.getMessage(), ex);
}
if (!request.isRepeatable()) {
if (!ExecProxies.isRepeatable(request)) {
this.log.debug("Cannot retry non-repeatable request");
throw new NonRepeatableRequestException("Cannot retry request " +
"with a non-repeatable request entity", ex);

View File

@ -1,64 +0,0 @@
/*
* ====================================================================
*
* 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.http.impl.client.execchain;
import java.io.OutputStream;
import junit.framework.Assert;
import org.apache.http.HttpEntity;
import org.apache.http.impl.client.execchain.RequestEntityWrapper;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
public class TestRequestEntityWrapper {
private HttpEntity entity;
private RequestEntityWrapper wrapper;
@Before
public void setup() throws Exception {
entity = Mockito.mock(HttpEntity.class);
wrapper = new RequestEntityWrapper(entity);
}
@Test
public void testNonRepeatableEntityWriteTo() throws Exception {
Mockito.when(entity.isRepeatable()).thenReturn(false);
Assert.assertTrue(wrapper.isRepeatable());
OutputStream outstream = Mockito.mock(OutputStream.class);
wrapper.writeTo(outstream);
Assert.assertTrue(wrapper.isConsumed());
Assert.assertFalse(wrapper.isRepeatable());
Mockito.verify(entity).writeTo(outstream);
}
}

View File

@ -259,7 +259,7 @@ public void testBasicAuthenticationFailureOnNonRepeatablePutDontExpectContinue()
this.localServer.register("*", new AuthHandler());
TestCredentialsProvider credsProvider = new TestCredentialsProvider(
new UsernamePasswordCredentials("test", "test"));
new UsernamePasswordCredentials("test", "boom"));
this.httpclient = HttpClients.custom().setCredentialsProvider(credsProvider).build();