From b8c1bb05cc4251a4b4620264203663d0c55aaf4d Mon Sep 17 00:00:00 2001 From: Oleg Kalnichevski Date: Fri, 30 Jul 2010 12:34:30 +0000 Subject: [PATCH] HTTPCLIENT-967: support for non-shared (private) caches Contributed by Jonathan Moore git-svn-id: https://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk@980759 13f79535-47bb-0310-9956-ffa450edef68 --- .../impl/client/cache/CachingHttpClient.java | 2 +- .../client/cache/ResponseCachingPolicy.java | 22 ++-- .../client/cache/AbstractProtocolTest.java | 115 ++++++++++++++++++ .../cache/TestProtocolAllowedBehavior.java | 66 ++++++++++ .../cache/TestProtocolRequirements.java | 91 +------------- .../cache/TestResponseCachingPolicy.java | 23 +++- 6 files changed, 216 insertions(+), 103 deletions(-) create mode 100644 httpclient-cache/src/test/java/org/apache/http/impl/client/cache/AbstractProtocolTest.java create mode 100644 httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestProtocolAllowedBehavior.java diff --git a/httpclient-cache/src/main/java/org/apache/http/impl/client/cache/CachingHttpClient.java b/httpclient-cache/src/main/java/org/apache/http/impl/client/cache/CachingHttpClient.java index d6f298bc2..6b54c3349 100644 --- a/httpclient-cache/src/main/java/org/apache/http/impl/client/cache/CachingHttpClient.java +++ b/httpclient-cache/src/main/java/org/apache/http/impl/client/cache/CachingHttpClient.java @@ -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(); diff --git a/httpclient-cache/src/main/java/org/apache/http/impl/client/cache/ResponseCachingPolicy.java b/httpclient-cache/src/main/java/org/apache/http/impl/client/cache/ResponseCachingPolicy.java index 2d7a53688..7d8e5ed8c 100644 --- a/httpclient-cache/src/main/java/org/apache/http/impl/client/cache/ResponseCachingPolicy.java +++ b/httpclient-cache/src/main/java/org/apache/http/impl/client/cache/ResponseCachingPolicy.java @@ -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(); diff --git a/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/AbstractProtocolTest.java b/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/AbstractProtocolTest.java new file mode 100644 index 000000000..d0d9f14b3 --- /dev/null +++ b/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/AbstractProtocolTest.java @@ -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 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(); + } + +} \ No newline at end of file diff --git a/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestProtocolAllowedBehavior.java b/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestProtocolAllowedBehavior.java new file mode 100644 index 000000000..ed8877935 --- /dev/null +++ b/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestProtocolAllowedBehavior.java @@ -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()); + } +} diff --git a/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestProtocolRequirements.java b/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestProtocolRequirements.java index 492f726b4..f3a45853a 100644 --- a/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestProtocolRequirements.java +++ b/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestProtocolRequirements.java @@ -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 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 { diff --git a/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestResponseCachingPolicy.java b/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestResponseCachingPolicy.java index b07b47830..6ea0dcae1 100644 --- a/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestResponseCachingPolicy.java +++ b/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestResponseCachingPolicy.java @@ -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");