HTTPCLIENT-967: support for non-shared (private) caches

Contributed by Jonathan Moore <jonathan_moore at comcast.com>


git-svn-id: https://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk@980759 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Oleg Kalnichevski 2010-07-30 12:34:30 +00:00
parent 8690e96fa6
commit b8c1bb05cc
6 changed files with 216 additions and 103 deletions

View File

@ -112,7 +112,7 @@ public class CachingHttpClient implements HttpClient {
this.backend = client;
this.responseCache = cache;
this.validityPolicy = new CacheValidityPolicy();
this.responseCachingPolicy = new ResponseCachingPolicy(maxObjectSizeBytes);
this.responseCachingPolicy = new ResponseCachingPolicy(maxObjectSizeBytes, sharedCache);
this.responseGenerator = new CachedHttpResponseGenerator(this.validityPolicy);
this.cacheEntryGenerator = new CacheEntryGenerator();
this.uriExtractor = new URIExtractor();

View File

@ -50,6 +50,7 @@ import org.apache.http.protocol.HTTP;
class ResponseCachingPolicy {
private final int maxObjectSizeBytes;
private final boolean sharedCache;
private final Log log = LogFactory.getLog(getClass());
/**
@ -57,9 +58,12 @@ class ResponseCachingPolicy {
* in the cache to a maximum of {@link HttpResponse} bytes in size.
*
* @param maxObjectSizeBytes the size to limit items into the cache
* @param sharedCache whether to behave as a shared cache (true) or a
* non-shared/private cache (false)
*/
public ResponseCachingPolicy(int maxObjectSizeBytes) {
public ResponseCachingPolicy(int maxObjectSizeBytes, boolean sharedCache) {
this.maxObjectSizeBytes = maxObjectSizeBytes;
this.sharedCache = sharedCache;
}
/**
@ -148,7 +152,7 @@ class ResponseCachingPolicy {
for (HeaderElement elem : header.getElements()) {
if (HeaderConstants.CACHE_CONTROL_NO_STORE.equals(elem.getName())
|| HeaderConstants.CACHE_CONTROL_NO_CACHE.equals(elem.getName())
|| "private".equals(elem.getName())) {
|| (sharedCache && "private".equals(elem.getName()))) {
return true;
}
}
@ -202,12 +206,14 @@ class ResponseCachingPolicy {
return false;
}
Header[] authNHeaders = request.getHeaders("Authorization");
if (authNHeaders != null && authNHeaders.length > 0) {
String[] authCacheableParams = {
"s-maxage", "must-revalidate", "public"
};
return hasCacheControlParameterFrom(response, authCacheableParams);
if (sharedCache) {
Header[] authNHeaders = request.getHeaders("Authorization");
if (authNHeaders != null && authNHeaders.length > 0) {
String[] authCacheableParams = {
"s-maxage", "must-revalidate", "public"
};
return hasCacheControlParameterFrom(response, authCacheableParams);
}
}
String method = request.getRequestLine().getMethod();

View File

@ -0,0 +1,115 @@
package org.apache.http.impl.client.cache;
import java.util.Date;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.HttpVersion;
import org.apache.http.client.HttpClient;
import org.apache.http.client.cache.HttpCache;
import org.apache.http.impl.cookie.DateUtils;
import org.apache.http.message.BasicHttpRequest;
import org.apache.http.message.BasicHttpResponse;
import org.apache.http.protocol.HttpContext;
import org.easymock.IExpectationSetters;
import org.easymock.classextension.EasyMock;
import org.junit.Before;
public abstract class AbstractProtocolTest {
protected static final int MAX_BYTES = 1024;
protected static final int MAX_ENTRIES = 100;
protected int entityLength = 128;
protected HttpHost host;
protected HttpEntity body;
protected HttpEntity mockEntity;
protected HttpClient mockBackend;
protected HttpCache mockCache;
protected HttpRequest request;
protected HttpResponse originResponse;
protected CacheConfig params;
protected CachingHttpClient impl;
private HttpCache cache;
public static HttpRequest eqRequest(HttpRequest in) {
EasyMock.reportMatcher(new RequestEquivalent(in));
return null;
}
@Before
public void setUp() {
host = new HttpHost("foo.example.com");
body = HttpTestUtils.makeBody(entityLength);
request = new BasicHttpRequest("GET", "/foo", HttpVersion.HTTP_1_1);
originResponse = make200Response();
cache = new BasicHttpCache(MAX_ENTRIES);
mockBackend = EasyMock.createMock(HttpClient.class);
mockEntity = EasyMock.createMock(HttpEntity.class);
mockCache = EasyMock.createMock(HttpCache.class);
params = new CacheConfig();
params.setMaxObjectSizeBytes(MAX_BYTES);
impl = new CachingHttpClient(mockBackend, cache, params);
}
protected void replayMocks() {
EasyMock.replay(mockBackend);
EasyMock.replay(mockCache);
EasyMock.replay(mockEntity);
}
protected void verifyMocks() {
EasyMock.verify(mockBackend);
EasyMock.verify(mockCache);
EasyMock.verify(mockEntity);
}
protected HttpResponse make200Response() {
HttpResponse out = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
out.setHeader("Date", DateUtils.formatDate(new Date()));
out.setHeader("Server", "MockOrigin/1.0");
out.setHeader("Content-Length", "128");
out.setEntity(makeBody(128));
return out;
}
protected HttpEntity makeBody(int nbytes) {
return HttpTestUtils.makeBody(nbytes);
}
protected IExpectationSetters<HttpResponse> backendExpectsAnyRequest() throws Exception {
HttpResponse resp = mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock
.isA(HttpRequest.class), (HttpContext) EasyMock.isNull());
return EasyMock.expect(resp);
}
protected void emptyMockCacheExpectsNoPuts() throws Exception {
mockBackend = EasyMock.createMock(HttpClient.class);
mockCache = EasyMock.createMock(HttpCache.class);
mockEntity = EasyMock.createMock(HttpEntity.class);
impl = new CachingHttpClient(mockBackend, mockCache, params);
EasyMock.expect(mockCache.getEntry((String) EasyMock.anyObject())).andReturn(null)
.anyTimes();
mockCache.removeEntry(EasyMock.isA(String.class));
EasyMock.expectLastCall().anyTimes();
}
protected void behaveAsNonSharedCache() {
params.setSharedCache(false);
impl = new CachingHttpClient(mockBackend, cache, params);
}
public AbstractProtocolTest() {
super();
}
}

View File

@ -0,0 +1,66 @@
package org.apache.http.impl.client.cache;
import java.net.SocketTimeoutException;
import java.util.Date;
import junit.framework.Assert;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.HttpVersion;
import org.apache.http.impl.cookie.DateUtils;
import org.apache.http.message.BasicHttpRequest;
import org.junit.Test;
/**
* This class tests behavior that is allowed (MAY) by the HTTP/1.1 protocol
* specification and for which we have implemented the behavior in the
* {@link CachingHttpClient}.
*/
public class TestProtocolAllowedBehavior extends AbstractProtocolTest {
@Test
public void testNonSharedCacheReturnsStaleResponseWhenRevalidationFailsForProxyRevalidate()
throws Exception {
HttpRequest req1 = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
Date now = new Date();
Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
originResponse.setHeader("Date", DateUtils.formatDate(tenSecondsAgo));
originResponse.setHeader("Cache-Control","max-age=5,proxy-revalidate");
originResponse.setHeader("Etag","\"etag\"");
backendExpectsAnyRequest().andReturn(originResponse);
HttpRequest req2 = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
backendExpectsAnyRequest().andThrow(new SocketTimeoutException());
replayMocks();
behaveAsNonSharedCache();
impl.execute(host, req1);
HttpResponse result = impl.execute(host, req2);
verifyMocks();
Assert.assertEquals(HttpStatus.SC_OK, result.getStatusLine().getStatusCode());
}
@Test
public void testNonSharedCacheMayCacheResponsesWithCacheControlPrivate()
throws Exception {
HttpRequest req1 = new BasicHttpRequest("GET","/", HttpVersion.HTTP_1_1);
originResponse.setHeader("Cache-Control","private,max-age=3600");
backendExpectsAnyRequest().andReturn(originResponse);
HttpRequest req2 = new BasicHttpRequest("GET","/", HttpVersion.HTTP_1_1);
replayMocks();
behaveAsNonSharedCache();
impl.execute(host, req1);
HttpResponse result = impl.execute(host, req2);
verifyMocks();
Assert.assertEquals(HttpStatus.SC_OK, result.getStatusLine().getStatusCode());
}
}

View File

@ -34,7 +34,6 @@ import java.util.Random;
import org.apache.http.Header;
import org.apache.http.HeaderElement;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
@ -43,8 +42,6 @@ import org.apache.http.HttpStatus;
import org.apache.http.HttpVersion;
import org.apache.http.ProtocolVersion;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.cache.HttpCache;
import org.apache.http.client.cache.HttpCacheEntry;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.impl.client.RequestWrapper;
@ -55,10 +52,8 @@ import org.apache.http.message.BasicHttpRequest;
import org.apache.http.message.BasicHttpResponse;
import org.apache.http.protocol.HttpContext;
import org.easymock.Capture;
import org.easymock.IExpectationSetters;
import org.easymock.classextension.EasyMock;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
@ -71,91 +66,7 @@ import org.junit.Test;
* pass downstream to the backend HttpClient are are conditionally compliant
* with the rules for an HTTP/1.1 client.
*/
public class TestProtocolRequirements {
private static final int MAX_BYTES = 1024;
private static final int MAX_ENTRIES = 100;
private int entityLength = 128;
private HttpHost host;
private HttpEntity body;
private HttpEntity mockEntity;
private HttpClient mockBackend;
private HttpCache mockCache;
private HttpRequest request;
private HttpResponse originResponse;
private CacheConfig params;
private CachingHttpClient impl;
@Before
public void setUp() {
host = new HttpHost("foo.example.com");
body = HttpTestUtils.makeBody(entityLength);
request = new BasicHttpRequest("GET", "/foo", HttpVersion.HTTP_1_1);
originResponse = make200Response();
HttpCache cache = new BasicHttpCache(MAX_ENTRIES);
mockBackend = EasyMock.createMock(HttpClient.class);
mockEntity = EasyMock.createMock(HttpEntity.class);
mockCache = EasyMock.createMock(HttpCache.class);
params = new CacheConfig();
params.setMaxObjectSizeBytes(MAX_BYTES);
impl = new CachingHttpClient(mockBackend, cache, params);
}
private void replayMocks() {
EasyMock.replay(mockBackend);
EasyMock.replay(mockCache);
EasyMock.replay(mockEntity);
}
private void verifyMocks() {
EasyMock.verify(mockBackend);
EasyMock.verify(mockCache);
EasyMock.verify(mockEntity);
}
private HttpResponse make200Response() {
HttpResponse out = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
out.setHeader("Date", DateUtils.formatDate(new Date()));
out.setHeader("Server", "MockOrigin/1.0");
out.setHeader("Content-Length", "128");
out.setEntity(makeBody(128));
return out;
}
private HttpEntity makeBody(int nbytes) {
return HttpTestUtils.makeBody(nbytes);
}
private IExpectationSetters<HttpResponse> backendExpectsAnyRequest() throws Exception {
HttpResponse resp = mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock
.isA(HttpRequest.class), (HttpContext) EasyMock.isNull());
return EasyMock.expect(resp);
}
private void emptyMockCacheExpectsNoPuts() throws Exception {
mockBackend = EasyMock.createMock(HttpClient.class);
mockCache = EasyMock.createMock(HttpCache.class);
mockEntity = EasyMock.createMock(HttpEntity.class);
impl = new CachingHttpClient(mockBackend, mockCache, params);
EasyMock.expect(mockCache.getEntry((String) EasyMock.anyObject())).andReturn(null)
.anyTimes();
mockCache.removeEntry(EasyMock.isA(String.class));
EasyMock.expectLastCall().anyTimes();
}
public static HttpRequest eqRequest(HttpRequest in) {
EasyMock.reportMatcher(new RequestEquivalent(in));
return null;
}
public class TestProtocolRequirements extends AbstractProtocolTest {
@Test
public void testCacheMissOnGETUsesOriginResponse() throws Exception {

View File

@ -53,7 +53,7 @@ public class TestResponseCachingPolicy {
@Before
public void setUp() throws Exception {
policy = new ResponseCachingPolicy(0);
policy = new ResponseCachingPolicy(0, true);
request = new BasicHttpRequest("GET","/",HTTP_1_1);
response = new BasicHttpResponse(
new BasicStatusLine(HTTP_1_1, HttpStatus.SC_OK, ""));
@ -67,12 +67,20 @@ public class TestResponseCachingPolicy {
}
@Test
public void testResponsesToRequestsWithAuthorizationHeadersAreNotCacheable() {
public void testResponsesToRequestsWithAuthorizationHeadersAreNotCacheableBySharedCache() {
request = new BasicHttpRequest("GET","/",HTTP_1_1);
request.setHeader("Authorization","Basic dXNlcjpwYXNzd2Q=");
Assert.assertFalse(policy.isResponseCacheable(request,response));
}
@Test
public void testResponsesToRequestsWithAuthorizationHeadersAreCacheableByNonSharedCache() {
policy = new ResponseCachingPolicy(0, false);
request = new BasicHttpRequest("GET","/",HTTP_1_1);
request.setHeader("Authorization","Basic dXNlcjpwYXNzd2Q=");
Assert.assertTrue(policy.isResponseCacheable(request,response));
}
@Test
public void testAuthorizedResponsesWithSMaxAgeAreCacheable() {
request = new BasicHttpRequest("GET","/",HTTP_1_1);
@ -199,15 +207,22 @@ public class TestResponseCachingPolicy {
Assert.assertTrue(policy.isResponseCacheable("GET", response));
}
// are we truly a non-shared cache? best be safe
@Test
public void testNon206WithPrivateCacheControlIsNotCacheable() {
public void testNon206WithPrivateCacheControlIsNotCacheableBySharedCache() {
int status = getRandomStatus();
response.setStatusCode(status);
response.setHeader("Cache-Control", "private");
Assert.assertFalse(policy.isResponseCacheable("GET", response));
}
@Test
public void test200ResponseWithPrivateCacheControlIsCacheableByNonSharedCache() {
policy = new ResponseCachingPolicy(0, false);
response.setStatusCode(HttpStatus.SC_OK);
response.setHeader("Cache-Control", "private");
Assert.assertTrue(policy.isResponseCacheable("GET", response));
}
@Test
public void testIsGetWithNoCacheCacheable() {
response.addHeader("Cache-Control", "no-cache");