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:
parent
8690e96fa6
commit
b8c1bb05cc
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
115
httpclient-cache/src/test/java/org/apache/http/impl/client/cache/AbstractProtocolTest.java
vendored
Normal file
115
httpclient-cache/src/test/java/org/apache/http/impl/client/cache/AbstractProtocolTest.java
vendored
Normal 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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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");
|
||||
|
|
Loading…
Reference in New Issue