### Eclipse Workspace Patch 1.0 #P httpcomponents-client Index: httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestProtocolRequirements.java =================================================================== --- httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestProtocolRequirements.java (revision 958667) +++ httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestProtocolRequirements.java (working copy) @@ -91,7 +91,7 @@ public void setUp() { host = new HttpHost("foo.example.com"); - body = makeBody(entityLength); + body = HttpTestUtils.makeBody(entityLength); request = new BasicHttpRequest("GET", "/foo", HTTP_1_1); @@ -121,15 +121,13 @@ out.setHeader("Date", DateUtils.formatDate(new Date())); out.setHeader("Server", "MockOrigin/1.0"); out.setHeader("Content-Length", "128"); - out.setEntity(makeBody(128)); + out.setEntity(HttpTestUtils.makeBody(128)); return out; } - private HttpEntity makeBody(int nbytes) { - byte[] bytes = new byte[nbytes]; - (new Random()).nextBytes(bytes); - return new ByteArrayEntity(bytes); - } + public static HttpEntity makeBody(int nbytes) { + return HttpTestUtils.makeBody(nbytes); + } private IExpectationSetters backendExpectsAnyRequest() throws Exception { HttpResponse resp = mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock @@ -1029,7 +1027,7 @@ BasicHttpEntityEnclosingRequest post = new BasicHttpEntityEnclosingRequest("POST", "/", HTTP_1_1); post.setHeader("Content-Length", "128"); - post.setEntity(makeBody(128)); + post.setEntity(HttpTestUtils.makeBody(128)); originResponse.removeHeaders("Cache-Control"); originResponse.removeHeaders("Expires"); @@ -1056,7 +1054,7 @@ BasicHttpEntityEnclosingRequest put = new BasicHttpEntityEnclosingRequest("PUT", "/", HTTP_1_1); - put.setEntity(makeBody(128)); + put.setEntity(HttpTestUtils.makeBody(128)); put.addHeader("Content-Length", "128"); originResponse.setHeader("Cache-Control", "max-age=3600"); @@ -1104,7 +1102,7 @@ public void testForwardedTRACERequestsDoNotIncludeAnEntity() throws Exception { BasicHttpEntityEnclosingRequest trace = new BasicHttpEntityEnclosingRequest("TRACE", "/", HTTP_1_1); - trace.setEntity(makeBody(entityLength)); + trace.setEntity(HttpTestUtils.makeBody(entityLength)); trace.setHeader("Content-Length", Integer.toString(entityLength)); Capture reqCap = new Capture(); @@ -1159,7 +1157,7 @@ @Test public void test204ResponsesDoNotContainMessageBodies() throws Exception { originResponse = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_NO_CONTENT, "No Content"); - originResponse.setEntity(makeBody(entityLength)); + originResponse.setEntity(HttpTestUtils.makeBody(entityLength)); EasyMock.expect( mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class), @@ -1183,7 +1181,7 @@ public void test205ResponsesDoNotContainMessageBodies() throws Exception { originResponse = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_RESET_CONTENT, "Reset Content"); - originResponse.setEntity(makeBody(entityLength)); + originResponse.setEntity(HttpTestUtils.makeBody(entityLength)); EasyMock.expect( mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class), @@ -1310,7 +1308,7 @@ "Partial Content"); originResponse.setHeader("Date", DateUtils.formatDate(new Date())); originResponse.setHeader("Server", "MockOrigin/1.0"); - originResponse.setEntity(makeBody(500)); + originResponse.setEntity(HttpTestUtils.makeBody(500)); originResponse.setHeader("Content-Range", "bytes 0-499/1234"); originResponse.removeHeaders("Date"); @@ -1733,7 +1731,7 @@ originResponse.setHeader("Cache-Control", "max-age=3600"); originResponse.setHeader("Content-Type", "application/x-cachingclient-test"); originResponse.setHeader("Location", "http://foo.example.com/other"); - originResponse.setEntity(makeBody(entityLength)); + originResponse.setEntity(HttpTestUtils.makeBody(entityLength)); EasyMock.expect( mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class), @@ -1758,7 +1756,7 @@ originResponse.setHeader("Date", DateUtils.formatDate(new Date())); originResponse.setHeader("Server", "MockServer/1.0"); originResponse.setHeader("Content-Length", "128"); - originResponse.setEntity(makeBody(entityLength)); + originResponse.setEntity(HttpTestUtils.makeBody(entityLength)); EasyMock.expect( mockBackend.execute(EasyMock.isA(HttpHost.class), EasyMock.isA(HttpRequest.class), @@ -1903,7 +1901,7 @@ resp1.setHeader("Cache-Control", "max-age=7200"); resp1.setHeader("Expires", DateUtils.formatDate(inTwoHours)); resp1.setHeader("Vary", "Accept-Encoding"); - resp1.setEntity(makeBody(entityLength)); + resp1.setEntity(HttpTestUtils.makeBody(entityLength)); HttpRequest req2 = new BasicHttpRequest("GET", "/", HTTP_1_1); req1.setHeader("Accept-Encoding", "gzip"); @@ -1914,7 +1912,7 @@ resp2.setHeader("Cache-Control", "max-age=3600"); resp2.setHeader("Expires", DateUtils.formatDate(inTwoHours)); resp2.setHeader("Vary", "Accept-Encoding"); - resp2.setEntity(makeBody(entityLength)); + resp2.setEntity(HttpTestUtils.makeBody(entityLength)); HttpRequest req3 = new BasicHttpRequest("GET", "/", HTTP_1_1); req3.setHeader("Accept-Encoding", "gzip"); @@ -2227,7 +2225,7 @@ public void testMustNotUseMultipartByteRangeContentTypeOnCacheGenerated416Responses() throws Exception { - originResponse.setEntity(makeBody(entityLength)); + originResponse.setEntity(HttpTestUtils.makeBody(entityLength)); originResponse.setHeader("Content-Length", "128"); originResponse.setHeader("Cache-Control", "max-age=3600"); @@ -2785,7 +2783,7 @@ @Test public void testPUTWithIfMatchWeakETagIsNotAllowed() throws Exception { HttpEntityEnclosingRequest put = new BasicHttpEntityEnclosingRequest("PUT", "/", HTTP_1_1); - put.setEntity(makeBody(128)); + put.setEntity(HttpTestUtils.makeBody(128)); put.setHeader("Content-Length", "128"); put.setHeader("If-Match", "W/\"etag\""); request = put; @@ -2796,7 +2794,7 @@ @Test public void testPUTWithIfNoneMatchWeakETagIsNotAllowed() throws Exception { HttpEntityEnclosingRequest put = new BasicHttpEntityEnclosingRequest("PUT", "/", HTTP_1_1); - put.setEntity(makeBody(128)); + put.setEntity(HttpTestUtils.makeBody(128)); put.setHeader("Content-Length", "128"); put.setHeader("If-None-Match", "W/\"etag\""); request = put; @@ -3178,7 +3176,7 @@ private void testDoesNotModifyHeaderOnRequest(String header, String value) throws Exception { BasicHttpEntityEnclosingRequest req = new BasicHttpEntityEnclosingRequest("POST","/",HTTP_1_1); - req.setEntity(makeBody(128)); + req.setEntity(HttpTestUtils.makeBody(128)); req.setHeader("Content-Length","128"); req.setHeader(header,value); @@ -3223,7 +3221,7 @@ private void testDoesNotAddHeaderToRequestIfNotPresent(String header) throws Exception { BasicHttpEntityEnclosingRequest req = new BasicHttpEntityEnclosingRequest("POST","/",HTTP_1_1); - req.setEntity(makeBody(128)); + req.setEntity(HttpTestUtils.makeBody(128)); req.setHeader("Content-Length","128"); req.removeHeaders(header); @@ -3351,7 +3349,7 @@ request.setHeader("Range","bytes=0-49"); originResponse = new BasicHttpResponse(HTTP_1_1, 206, "Partial Content"); - originResponse.setEntity(makeBody(50)); + originResponse.setEntity(HttpTestUtils.makeBody(50)); testDoesNotModifyHeaderFromOriginResponseWithNoTransform("Content-Range","bytes 0-49/128"); } @@ -3663,6 +3661,244 @@ HttpTestUtils.getCanonicalHeaderValue(result2, h)); } + /* "Some HTTP methods MUST cause a cache to invalidate an + * entity. This is either the entity referred to by the + * Request-URI, or by the Location or Content-Location headers (if + * present). These methods are: + * - PUT + * - DELETE + * - POST + * + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.9 + */ + protected void testUnsafeOperationInvalidatesCacheForThatUri( + HttpRequest unsafeReq) throws Exception, IOException { + HttpRequest req1 = new BasicHttpRequest("GET", "/", HTTP_1_1); + HttpResponse resp1 = make200Response(); + resp1.setHeader("Cache-Control","public, max-age=3600"); + + backendExpectsAnyRequest().andReturn(resp1); + + HttpResponse resp2 = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_NO_CONTENT, "No Content"); + + backendExpectsAnyRequest().andReturn(resp2); + + HttpRequest req3 = new BasicHttpRequest("GET", "/", HTTP_1_1); + HttpResponse resp3 = make200Response(); + resp3.setHeader("Cache-Control","public, max-age=3600"); + + // this origin request MUST happen due to invalidation + backendExpectsAnyRequest().andReturn(resp3); + + replayMocks(); + impl.execute(host, req1); + impl.execute(host, unsafeReq); + impl.execute(host, req3); + verifyMocks(); + } + + @Test + public void testPutToUriInvalidatesCacheForThatUri() throws Exception { + HttpRequest req = makeRequestWithBody("PUT","/"); + testUnsafeOperationInvalidatesCacheForThatUri(req); + } + + @Test + public void testDeleteToUriInvalidatesCacheForThatUri() throws Exception { + HttpRequest req = new BasicHttpRequest("DELETE","/"); + testUnsafeOperationInvalidatesCacheForThatUri(req); + } + + @Test + public void testPostToUriInvalidatesCacheForThatUri() throws Exception { + HttpRequest req = makeRequestWithBody("POST","/"); + testUnsafeOperationInvalidatesCacheForThatUri(req); + } + + protected void testUnsafeMethodInvalidatesCacheForHeaderUri( + HttpRequest unsafeReq) throws Exception, IOException { + HttpRequest req1 = new BasicHttpRequest("GET", "/content", HTTP_1_1); + HttpResponse resp1 = make200Response(); + resp1.setHeader("Cache-Control","public, max-age=3600"); + + backendExpectsAnyRequest().andReturn(resp1); + + HttpResponse resp2 = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_NO_CONTENT, "No Content"); + + backendExpectsAnyRequest().andReturn(resp2); + + HttpRequest req3 = new BasicHttpRequest("GET", "/content", HTTP_1_1); + HttpResponse resp3 = make200Response(); + resp3.setHeader("Cache-Control","public, max-age=3600"); + + // this origin request MUST happen due to invalidation + backendExpectsAnyRequest().andReturn(resp3); + + replayMocks(); + impl.execute(host, req1); + impl.execute(host, unsafeReq); + impl.execute(host, req3); + verifyMocks(); + } + + protected void testUnsafeMethodInvalidatesCacheForUriInContentLocationHeader( + HttpRequest unsafeReq) throws Exception, IOException { + unsafeReq.setHeader("Content-Location","http://foo.example.com/content"); + testUnsafeMethodInvalidatesCacheForHeaderUri(unsafeReq); + } + + protected void testUnsafeMethodInvalidatesCacheForRelativeUriInContentLocationHeader( + HttpRequest unsafeReq) throws Exception, IOException { + unsafeReq.setHeader("Content-Location","/content"); + testUnsafeMethodInvalidatesCacheForHeaderUri(unsafeReq); + } + + protected void testUnsafeMethodInvalidatesCacheForUriInLocationHeader( + HttpRequest unsafeReq) throws Exception, IOException { + unsafeReq.setHeader("Location","http://foo.example.com/content"); + testUnsafeMethodInvalidatesCacheForHeaderUri(unsafeReq); + } + + @Test + public void testPutInvalidatesCacheForThatUriInContentLocationHeader() throws Exception { + HttpRequest req2 = makeRequestWithBody("PUT","/"); + testUnsafeMethodInvalidatesCacheForUriInContentLocationHeader(req2); + } + + @Test + public void testPutInvalidatesCacheForThatUriInLocationHeader() throws Exception { + HttpRequest req = makeRequestWithBody("PUT","/"); + testUnsafeMethodInvalidatesCacheForUriInLocationHeader(req); + } + + @Test + public void testPutInvalidatesCacheForThatUriInRelativeContentLocationHeader() throws Exception { + HttpRequest req = makeRequestWithBody("PUT","/"); + testUnsafeMethodInvalidatesCacheForRelativeUriInContentLocationHeader(req); + } + + @Test + public void testDeleteInvalidatesCacheForThatUriInContentLocationHeader() throws Exception { + HttpRequest req = new BasicHttpRequest("DELETE","/"); + testUnsafeMethodInvalidatesCacheForUriInContentLocationHeader(req); + } + + @Test + public void testDeleteInvalidatesCacheForThatUriInRelativeContentLocationHeader() throws Exception { + HttpRequest req = new BasicHttpRequest("DELETE","/"); + testUnsafeMethodInvalidatesCacheForRelativeUriInContentLocationHeader(req); + } + + @Test + public void testDeleteInvalidatesCacheForThatUriInLocationHeader() throws Exception { + HttpRequest req = new BasicHttpRequest("DELETE","/"); + testUnsafeMethodInvalidatesCacheForUriInLocationHeader(req); + } + + @Test + public void testPostInvalidatesCacheForThatUriInContentLocationHeader() throws Exception { + HttpRequest req = makeRequestWithBody("POST","/"); + testUnsafeMethodInvalidatesCacheForUriInContentLocationHeader(req); + } + + @Test + public void testPostInvalidatesCacheForThatUriInLocationHeader() throws Exception { + HttpRequest req = makeRequestWithBody("POST","/"); + testUnsafeMethodInvalidatesCacheForUriInLocationHeader(req); + } + + @Test + public void testPostInvalidatesCacheForRelativeUriInContentLocationHeader() throws Exception { + HttpRequest req = makeRequestWithBody("POST","/"); + testUnsafeMethodInvalidatesCacheForRelativeUriInContentLocationHeader(req); + } + + /* "In order to prevent denial of service attacks, an invalidation based on the URI + * in a Location or Content-Location header MUST only be performed if the host part + * is the same as in the Request-URI." + * + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.10 + */ + protected void testUnsafeMethodDoesNotInvalidateCacheForHeaderUri( + HttpRequest unsafeReq) throws Exception, IOException { + + HttpHost otherHost = new HttpHost("bar.example.com"); + HttpRequest req1 = new BasicHttpRequest("GET", "/content", HTTP_1_1); + HttpResponse resp1 = make200Response(); + resp1.setHeader("Cache-Control","public, max-age=3600"); + + backendExpectsAnyRequest().andReturn(resp1); + + HttpResponse resp2 = new BasicHttpResponse(HTTP_1_1, HttpStatus.SC_NO_CONTENT, "No Content"); + + backendExpectsAnyRequest().andReturn(resp2); + + HttpRequest req3 = new BasicHttpRequest("GET", "/content", HTTP_1_1); + + replayMocks(); + impl.execute(otherHost, req1); + impl.execute(host, unsafeReq); + impl.execute(otherHost, req3); + verifyMocks(); + } + + protected void testUnsafeMethodDoesNotInvalidateCacheForUriInContentLocationHeadersFromOtherHosts( + HttpRequest unsafeReq) throws Exception, IOException { + unsafeReq.setHeader("Content-Location","http://bar.example.com/content"); + testUnsafeMethodDoesNotInvalidateCacheForHeaderUri(unsafeReq); + } + + protected void testUnsafeMethodDoesNotInvalidateCacheForUriInLocationHeadersFromOtherHosts( + HttpRequest unsafeReq) throws Exception, IOException { + unsafeReq.setHeader("Location","http://bar.example.com/content"); + testUnsafeMethodDoesNotInvalidateCacheForHeaderUri(unsafeReq); + } + + protected HttpRequest makeRequestWithBody(String method, String requestUri) { + HttpEntityEnclosingRequest request = + new BasicHttpEntityEnclosingRequest(method, requestUri, HTTP_1_1); + int nbytes = 128; + request.setEntity(HttpTestUtils.makeBody(nbytes)); + request.setHeader("Content-Length",""+nbytes); + return request; + } + + @Test + public void testPutDoesNotInvalidateCacheForUriInContentLocationHeadersFromOtherHosts() throws Exception { + HttpRequest req = makeRequestWithBody("PUT","/"); + testUnsafeMethodDoesNotInvalidateCacheForUriInContentLocationHeadersFromOtherHosts(req); + } + + @Test + public void testPutDoesNotInvalidateCacheForUriInLocationHeadersFromOtherHosts() throws Exception { + HttpRequest req = makeRequestWithBody("PUT","/"); + testUnsafeMethodDoesNotInvalidateCacheForUriInLocationHeadersFromOtherHosts(req); + } + + @Test + public void testPostDoesNotInvalidateCacheForUriInContentLocationHeadersFromOtherHosts() throws Exception { + HttpRequest req = makeRequestWithBody("POST","/"); + testUnsafeMethodDoesNotInvalidateCacheForUriInContentLocationHeadersFromOtherHosts(req); + } + + @Test + public void testPostDoesNotInvalidateCacheForUriInLocationHeadersFromOtherHosts() throws Exception { + HttpRequest req = makeRequestWithBody("POST","/"); + testUnsafeMethodDoesNotInvalidateCacheForUriInLocationHeadersFromOtherHosts(req); + } + + @Test + public void testDeleteDoesNotInvalidateCacheForUriInContentLocationHeadersFromOtherHosts() throws Exception { + HttpRequest req = new BasicHttpRequest("DELETE","/",HTTP_1_1); + testUnsafeMethodDoesNotInvalidateCacheForUriInContentLocationHeadersFromOtherHosts(req); + } + + @Test + public void testDeleteDoesNotInvalidateCacheForUriInLocationHeadersFromOtherHosts() throws Exception { + HttpRequest req = new BasicHttpRequest("DELETE","/",HTTP_1_1); + testUnsafeMethodDoesNotInvalidateCacheForUriInLocationHeadersFromOtherHosts(req); + } + private class FakeHeaderGroup extends HeaderGroup{ public void addHeader(String name, String value){ Index: httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestCacheInvalidator.java =================================================================== --- httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestCacheInvalidator.java (revision 958667) +++ httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestCacheInvalidator.java (working copy) @@ -29,78 +29,53 @@ import java.util.HashSet; import java.util.Set; -import org.apache.http.Header; -import org.apache.http.HeaderElement; +import org.apache.http.HttpEntityEnclosingRequest; import org.apache.http.HttpHost; import org.apache.http.HttpRequest; -import org.apache.http.RequestLine; +import org.apache.http.ProtocolVersion; import org.apache.http.client.cache.HttpCache; import org.apache.http.client.cache.HttpCacheOperationException; +import org.apache.http.message.BasicHttpEntityEnclosingRequest; +import org.apache.http.message.BasicHttpRequest; import org.easymock.classextension.EasyMock; import org.junit.Before; import org.junit.Test; public class TestCacheInvalidator { + private static final ProtocolVersion HTTP_1_1 = new ProtocolVersion("HTTP", 1, 1); + private CacheInvalidator impl; private HttpCache mockCache; - private Header mockHeader; - private Header[] mockHeaderArray = new Header[1]; private HttpHost host; - private HttpRequest mockRequest; - private RequestLine mockRequestLine; - private URIExtractor mockExtractor; + private URIExtractor extractor; private CacheEntry mockEntry; private boolean mockedImpl; - private HeaderElement mockElement; - private HeaderElement[] mockElementArray = new HeaderElement[1]; @SuppressWarnings("unchecked") @Before public void setUp() { host = new HttpHost("foo.example.com"); mockCache = EasyMock.createMock(HttpCache.class); - mockExtractor = EasyMock.createMock(URIExtractor.class); - mockHeader = EasyMock.createMock(Header.class); - mockElement = EasyMock.createMock(HeaderElement.class); - mockRequest = EasyMock.createMock(HttpRequest.class); - mockRequestLine = EasyMock.createMock(RequestLine.class); + extractor = new URIExtractor(); mockEntry = EasyMock.createMock(CacheEntry.class); - mockHeaderArray[0] = mockHeader; - mockElementArray[0] = mockElement; - - impl = new CacheInvalidator(mockExtractor, mockCache); + + impl = new CacheInvalidator(extractor, mockCache); } - private void mockImplMethods(String... methods) { - mockedImpl = true; - impl = EasyMock.createMockBuilder(CacheInvalidator.class).withConstructor(mockExtractor, - mockCache).addMockedMethods(methods).createMock(); - } - private void replayMocks() { EasyMock.replay(mockCache); - EasyMock.replay(mockExtractor); - EasyMock.replay(mockHeader); - EasyMock.replay(mockRequest); - EasyMock.replay(mockRequestLine); EasyMock.replay(mockEntry); - EasyMock.replay(mockElement); - + if (mockedImpl) EasyMock.replay(impl); } private void verifyMocks() { EasyMock.verify(mockCache); - EasyMock.verify(mockExtractor); - EasyMock.verify(mockHeader); - EasyMock.verify(mockRequest); - EasyMock.verify(mockRequestLine); EasyMock.verify(mockEntry); - EasyMock.verify(mockElement); - + if (mockedImpl) EasyMock.verify(impl); } @@ -108,140 +83,211 @@ // Tests @Test public void testInvalidatesRequestsThatArentGETorHEAD() throws Exception { - final String theUri = "theUri"; + HttpRequest request = new BasicHttpRequest("POST","/path", HTTP_1_1); + + final String theUri = "http://foo.example.com/path"; Set variantURIs = new HashSet(); cacheEntryHasVariantURIs(variantURIs); cacheReturnsEntryForUri(theUri); - requestLineIsRead(); - requestMethodIs("POST"); - extractorReturns(theUri); entryIsRemoved(theUri); replayMocks(); - impl.flushInvalidatedCacheEntries(host, mockRequest); + impl.flushInvalidatedCacheEntries(host, request); verifyMocks(); } @Test - public void testDoesNotInvalidateGETRequest() { + public void testInvalidatesUrisInContentLocationHeadersOnPUTs() throws Exception { + HttpEntityEnclosingRequest request = new BasicHttpEntityEnclosingRequest("PUT","/",HTTP_1_1); + request.setEntity(HttpTestUtils.makeBody(128)); + request.setHeader("Content-Length","128"); + + String contentLocation = "http://foo.example.com/content"; + request.setHeader("Content-Location",contentLocation); + + final String theUri = "http://foo.example.com/"; + Set variantURIs = new HashSet(); + cacheEntryHasVariantURIs(variantURIs); - requestLineIsRead(); - requestMethodIs("GET"); - requestContainsCacheControlHeader(null); - requestContainsPragmaHeader(null); + cacheReturnsEntryForUri(theUri); + entryIsRemoved(theUri); + entryIsRemoved(contentLocation); + replayMocks(); - impl.flushInvalidatedCacheEntries(host, mockRequest); + impl.flushInvalidatedCacheEntries(host, request); verifyMocks(); } + + @Test + public void testInvalidatesUrisInLocationHeadersOnPUTs() throws Exception { + HttpEntityEnclosingRequest request = new BasicHttpEntityEnclosingRequest("PUT","/",HTTP_1_1); + request.setEntity(HttpTestUtils.makeBody(128)); + request.setHeader("Content-Length","128"); + + String contentLocation = "http://foo.example.com/content"; + request.setHeader("Location",contentLocation); + + final String theUri = "http://foo.example.com/"; + Set variantURIs = new HashSet(); + cacheEntryHasVariantURIs(variantURIs); + cacheReturnsEntryForUri(theUri); + entryIsRemoved(theUri); + entryIsRemoved(contentLocation); + + replayMocks(); + + impl.flushInvalidatedCacheEntries(host, request); + + verifyMocks(); + } + @Test - public void testDoesNotInvalidateHEADRequest() { + public void testInvalidatesRelativeUrisInContentLocationHeadersOnPUTs() throws Exception { + HttpEntityEnclosingRequest request = new BasicHttpEntityEnclosingRequest("PUT","/",HTTP_1_1); + request.setEntity(HttpTestUtils.makeBody(128)); + request.setHeader("Content-Length","128"); + + String contentLocation = "http://foo.example.com/content"; + String relativePath = "/content"; + request.setHeader("Content-Location",relativePath); + + final String theUri = "http://foo.example.com/"; + Set variantURIs = new HashSet(); + cacheEntryHasVariantURIs(variantURIs); - requestLineIsRead(); - requestMethodIs("HEAD"); - requestContainsCacheControlHeader(null); - requestContainsPragmaHeader(null); + cacheReturnsEntryForUri(theUri); + entryIsRemoved(theUri); + entryIsRemoved(contentLocation); + replayMocks(); - impl.flushInvalidatedCacheEntries(host, mockRequest); + impl.flushInvalidatedCacheEntries(host, request); verifyMocks(); } @Test + public void testDoesNotInvalidateUrisInContentLocationHeadersOnPUTsToDifferentHosts() throws Exception { + HttpEntityEnclosingRequest request = new BasicHttpEntityEnclosingRequest("PUT","/",HTTP_1_1); + request.setEntity(HttpTestUtils.makeBody(128)); + request.setHeader("Content-Length","128"); + + String contentLocation = "http://bar.example.com/content"; + request.setHeader("Content-Location",contentLocation); + + final String theUri = "http://foo.example.com/"; + Set variantURIs = new HashSet(); + cacheEntryHasVariantURIs(variantURIs); + + cacheReturnsEntryForUri(theUri); + entryIsRemoved(theUri); + + replayMocks(); + + impl.flushInvalidatedCacheEntries(host, request); + + verifyMocks(); + } + + @Test + public void testDoesNotInvalidateGETRequest() { + HttpRequest request = new BasicHttpRequest("GET","/",HTTP_1_1); + + replayMocks(); + + impl.flushInvalidatedCacheEntries(host, request); + + verifyMocks(); + } + + @Test + public void testDoesNotInvalidateHEADRequest() { + HttpRequest request = new BasicHttpRequest("HEAD","/",HTTP_1_1); + + replayMocks(); + + impl.flushInvalidatedCacheEntries(host, request); + + verifyMocks(); + } + + @Test public void testInvalidatesRequestsWithClientCacheControlHeaders() throws Exception { - final String theUri = "theUri"; - extractorReturns(theUri); + HttpRequest request = new BasicHttpRequest("GET","/",HTTP_1_1); + request.setHeader("Cache-Control","no-cache"); + + final String theUri = "http://foo.example.com/"; cacheReturnsEntryForUri(theUri); Set variantURIs = new HashSet(); cacheEntryHasVariantURIs(variantURIs); - requestLineIsRead(); - requestMethodIs("GET"); - requestContainsCacheControlHeader(mockHeaderArray); - - org.easymock.EasyMock.expect(mockHeader.getElements()).andReturn(mockElementArray); - org.easymock.EasyMock.expect(mockElement.getName()).andReturn("no-cache").anyTimes(); - entryIsRemoved(theUri); replayMocks(); - impl.flushInvalidatedCacheEntries(host, mockRequest); + impl.flushInvalidatedCacheEntries(host, request); verifyMocks(); } @Test public void testInvalidatesRequestsWithClientPragmaHeaders() throws Exception { - final String theUri = "theUri"; - extractorReturns(theUri); + HttpRequest request = new BasicHttpRequest("GET","/",HTTP_1_1); + request.setHeader("Pragma","no-cache"); + + final String theUri = "http://foo.example.com/"; cacheReturnsEntryForUri(theUri); Set variantURIs = new HashSet(); cacheEntryHasVariantURIs(variantURIs); - requestLineIsRead(); - requestMethodIs("GET"); - requestContainsCacheControlHeader(null); - requestContainsPragmaHeader(mockHeader); entryIsRemoved(theUri); replayMocks(); - impl.flushInvalidatedCacheEntries(host, mockRequest); + impl.flushInvalidatedCacheEntries(host, request); verifyMocks(); } @Test public void testVariantURIsAreFlushedAlso() throws HttpCacheOperationException { - final String theUri = "theUri"; + HttpRequest request = new BasicHttpRequest("POST","/",HTTP_1_1); + + final String theUri = "http://foo.example.com/"; final String variantUri = "theVariantURI"; Set listOfURIs = new HashSet(); listOfURIs.add(variantUri); - extractorReturns(theUri); cacheReturnsEntryForUri(theUri); cacheEntryHasVariantURIs(listOfURIs); - + entryIsRemoved(variantUri); entryIsRemoved(theUri); - mockImplMethods("requestShouldNotBeCached"); - org.easymock.EasyMock.expect(impl.requestShouldNotBeCached(mockRequest)).andReturn(true); - replayMocks(); - impl.flushInvalidatedCacheEntries(host, mockRequest); + impl.flushInvalidatedCacheEntries(host, request); verifyMocks(); } @Test public void testCacheFlushException() throws Exception { - String theURI = "theURI"; + HttpRequest request = new BasicHttpRequest("POST","/",HTTP_1_1); + String theURI = "http://foo.example.com/"; - mockImplMethods("requestShouldNotBeCached"); - org.easymock.EasyMock.expect(impl.requestShouldNotBeCached(mockRequest)).andReturn(true); - - extractorReturns(theURI); cacheReturnsExceptionForUri(theURI); replayMocks(); - impl.flushInvalidatedCacheEntries(host, mockRequest); + impl.flushInvalidatedCacheEntries(host, request); verifyMocks(); } // Expectations - private void requestContainsPragmaHeader(Header header) { - org.easymock.EasyMock.expect(mockRequest.getFirstHeader("Pragma")).andReturn(header); - } - - private void requestMethodIs(String s) { - org.easymock.EasyMock.expect(mockRequestLine.getMethod()).andReturn(s); - } - + + private void cacheEntryHasVariantURIs(Set variantURIs) { org.easymock.EasyMock.expect(mockEntry.getVariantURIs()).andReturn(variantURIs); } @@ -255,19 +301,8 @@ new HttpCacheOperationException("TOTAL FAIL")); } - private void extractorReturns(String theUri) { - org.easymock.EasyMock.expect(mockExtractor.getURI(host, mockRequest)).andReturn(theUri); - } - private void entryIsRemoved(String theUri) throws HttpCacheOperationException { mockCache.removeEntry(theUri); } - private void requestLineIsRead() { - org.easymock.EasyMock.expect(mockRequest.getRequestLine()).andReturn(mockRequestLine); - } - - private void requestContainsCacheControlHeader(Header[] header) { - org.easymock.EasyMock.expect(mockRequest.getHeaders("Cache-Control")).andReturn(header); - } } \ No newline at end of file Index: httpclient-cache/src/main/java/org/apache/http/impl/client/cache/CacheInvalidator.java =================================================================== --- httpclient-cache/src/main/java/org/apache/http/impl/client/cache/CacheInvalidator.java (revision 958667) +++ httpclient-cache/src/main/java/org/apache/http/impl/client/cache/CacheInvalidator.java (working copy) @@ -26,6 +26,9 @@ */ package org.apache.http.impl.client.cache; +import java.net.MalformedURLException; +import java.net.URL; + import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.http.Header; @@ -86,6 +89,24 @@ } cache.removeEntry(theUri); } + URL reqURL; + try { + reqURL = new URL(theUri); + } catch (MalformedURLException mue) { + log.error("Couldn't transform request into valid URL"); + return; + } + Header clHdr = req.getFirstHeader("Content-Location"); + if (clHdr != null) { + String contentLocation = clHdr.getValue(); + if (!flushAbsoluteUriFromSameHost(reqURL, contentLocation)) { + flushRelativeUriFromSameHost(reqURL, contentLocation); + } + } + Header lHdr = req.getFirstHeader("Location"); + if (lHdr != null) { + flushAbsoluteUriFromSameHost(reqURL, lHdr.getValue()); + } } catch (HttpCacheOperationException ex) { log.debug("Was unable to REMOVE an entry from the cache based on the uri provided", ex); @@ -93,6 +114,37 @@ } } + protected void flushUriIfSameHost(URL requestURL, URL targetURL) + throws HttpCacheOperationException { + if (targetURL.getAuthority().equalsIgnoreCase(requestURL.getAuthority())) { + cache.removeEntry(targetURL.toString()); + } + } + + protected void flushRelativeUriFromSameHost(URL reqURL, String relUri) + throws HttpCacheOperationException { + URL relURL; + try { + relURL = new URL(reqURL,relUri); + } catch (MalformedURLException e) { + log.debug("Invalid relative URI",e); + return; + } + flushUriIfSameHost(reqURL, relURL); + } + + protected boolean flushAbsoluteUriFromSameHost(URL reqURL, String uri) + throws HttpCacheOperationException { + URL absURL; + try { + absURL = new URL(uri); + } catch (MalformedURLException mue) { + return false; + } + flushUriIfSameHost(reqURL,absURL); + return true; + } + protected boolean requestShouldNotBeCached(HttpRequest req) { String method = req.getRequestLine().getMethod(); return notGetOrHeadRequest(method) || containsCacheControlHeader(req) Index: httpclient-cache/src/test/java/org/apache/http/impl/client/cache/HttpTestUtils.java =================================================================== --- httpclient-cache/src/test/java/org/apache/http/impl/client/cache/HttpTestUtils.java (revision 958667) +++ httpclient-cache/src/test/java/org/apache/http/impl/client/cache/HttpTestUtils.java (working copy) @@ -27,6 +27,7 @@ package org.apache.http.impl.client.cache; import java.io.InputStream; +import java.util.Random; import org.apache.http.Header; import org.apache.http.HttpEntity; @@ -35,6 +36,7 @@ import org.apache.http.HttpResponse; import org.apache.http.RequestLine; import org.apache.http.StatusLine; +import org.apache.http.entity.ByteArrayEntity; public class HttpTestUtils { @@ -203,4 +205,14 @@ return (equivalent(r1.getRequestLine(), r2.getRequestLine()) && isEndToEndHeaderSubset(r1, r2)); } + + public static HttpEntity makeBody(int nbytes) { + return new ByteArrayEntity(getRandomBytes(nbytes)); + } + + public static byte[] getRandomBytes(int nbytes) { + byte[] bytes = new byte[nbytes]; + (new Random()).nextBytes(bytes); + return bytes; + } } \ No newline at end of file