diff --git a/httpclient-cache/src/main/java/org/apache/http/client/cache/CacheResponseStatus.java b/httpclient-cache/src/main/java/org/apache/http/client/cache/CacheResponseStatus.java new file mode 100644 index 000000000..0f92e449f --- /dev/null +++ b/httpclient-cache/src/main/java/org/apache/http/client/cache/CacheResponseStatus.java @@ -0,0 +1,54 @@ +/* + * ==================================================================== + * 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 + * . + * + */ +package org.apache.http.client.cache; + +/** + * This enumeration represents the various ways a response can be generated + * by the {@link CachingHttpClient}; if a request is executed with an + * {@link org.apache.http.protocol.HttpContext} + * then a parameter with one of these values will be registered in the + * context. + */ +public enum CacheResponseStatus { + + /** The response was generated directly by the caching module. */ + CACHE_MODULE_RESPONSE, + + /** A response was generated from the cache with no requests sent + * upstream. + */ + CACHE_HIT, + + /** The response came from an upstream server. */ + CACHE_MISS, + + /** The response was generated from the cache after validating the + * entry with the origin server. + */ + VALIDATED; + +} 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 5afb99e58..4d96ce924 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 @@ -45,6 +45,7 @@ import org.apache.http.annotation.ThreadSafe; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.HttpClient; import org.apache.http.client.ResponseHandler; +import org.apache.http.client.cache.CacheResponseStatus; import org.apache.http.client.cache.HeaderConstants; import org.apache.http.client.cache.HttpCache; import org.apache.http.client.cache.HttpCacheEntry; @@ -61,6 +62,8 @@ import org.apache.http.protocol.HttpContext; @ThreadSafe // So long as the responseCache implementation is threadsafe public class CachingHttpClient implements HttpClient { + public static final String CACHE_RESPONSE_STATUS = "http.cache.response.status"; + private final static boolean SUPPORTS_RANGE_AND_CONTENT_RANGE_HEADERS = false; private final AtomicLong cacheHits = new AtomicLong(); @@ -352,13 +355,18 @@ public class CachingHttpClient implements HttpClient { public HttpResponse execute(HttpHost target, HttpRequest request, HttpContext context) throws IOException { + // default response context + setResponseStatus(context, CacheResponseStatus.CACHE_MISS); + if (clientRequestsOurOptions(request)) { + setResponseStatus(context, CacheResponseStatus.CACHE_MODULE_RESPONSE); return new OptionsHttp11Response(); } List fatalError = requestCompliance.requestIsFatallyNonCompliant(request); for (RequestProtocolError error : fatalError) { + setResponseStatus(context, CacheResponseStatus.CACHE_MODULE_RESPONSE); return requestCompliance.getErrorForRequest(error); } @@ -393,6 +401,7 @@ public class CachingHttpClient implements HttpClient { cacheHits.getAndIncrement(); if (suitabilityChecker.canCachedResponseBeUsed(target, request, entry)) { + setResponseStatus(context, CacheResponseStatus.CACHE_HIT); return responseGenerator.generateResponse(entry); } @@ -404,8 +413,10 @@ public class CachingHttpClient implements HttpClient { } catch (IOException ioex) { if (validityPolicy.mustRevalidate(entry) || (isSharedCache() && validityPolicy.proxyRevalidate(entry))) { + setResponseStatus(context, CacheResponseStatus.CACHE_MODULE_RESPONSE); return new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_GATEWAY_TIMEOUT, "Gateway Timeout"); } else { + setResponseStatus(context, CacheResponseStatus.CACHE_HIT); HttpResponse response = responseGenerator.generateResponse(entry); response.addHeader(HeaderConstants.WARNING, "111 Revalidation Failed - " + ioex.getMessage()); log.debug("111 revalidation failed due to exception: " + ioex); @@ -418,6 +429,12 @@ public class CachingHttpClient implements HttpClient { return callBackend(target, request, context); } + private void setResponseStatus(final HttpContext context, final CacheResponseStatus value) { + if (context != null) { + context.setAttribute(CACHE_RESPONSE_STATUS, value); + } + } + public boolean supportsRangeAndContentRangeHeaders() { return SUPPORTS_RANGE_AND_CONTENT_RANGE_HEADERS; } @@ -472,6 +489,7 @@ public class CachingHttpClient implements HttpClient { int statusCode = backendResponse.getStatusLine().getStatusCode(); if (statusCode == HttpStatus.SC_NOT_MODIFIED || statusCode == HttpStatus.SC_OK) { cacheUpdates.getAndIncrement(); + setResponseStatus(context, CacheResponseStatus.VALIDATED); return responseCache.updateCacheEntry(target, request, cacheEntry, backendResponse, requestDate, responseDate); } diff --git a/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestCachingHttpClient.java b/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestCachingHttpClient.java index ba412ede5..e4b5ba293 100644 --- a/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestCachingHttpClient.java +++ b/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestCachingHttpClient.java @@ -42,11 +42,15 @@ import org.apache.http.StatusLine; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.HttpClient; import org.apache.http.client.ResponseHandler; +import org.apache.http.client.cache.CacheResponseStatus; import org.apache.http.client.cache.HttpCache; import org.apache.http.client.cache.HttpCacheEntry; +import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.conn.ClientConnectionManager; +import org.apache.http.impl.cookie.DateUtils; import org.apache.http.message.BasicHttpRequest; +import org.apache.http.message.BasicHttpResponse; import org.apache.http.params.BasicHttpParams; import org.apache.http.params.HttpParams; import org.apache.http.protocol.BasicHttpContext; @@ -726,6 +730,178 @@ public class TestCachingHttpClient { Assert.assertTrue(gotException); } + @Test + public void testSetsModuleGeneratedResponseContextForCacheOptionsResponse() + throws Exception { + impl = new CachingHttpClient(mockBackend); + HttpRequest req = new BasicHttpRequest("OPTIONS","*",HttpVersion.HTTP_1_1); + req.setHeader("Max-Forwards","0"); + + impl.execute(host, req, context); + Assert.assertEquals(CacheResponseStatus.CACHE_MODULE_RESPONSE, + context.getAttribute("http.cache.response.context")); + } + + @Test + public void testSetsModuleGeneratedResponseContextForFatallyNoncompliantRequest() + throws Exception { + impl = new CachingHttpClient(mockBackend); + HttpRequest req = new HttpGet("http://foo.example.com/"); + req.setHeader("Range","bytes=0-50"); + req.setHeader("If-Range","W/\"weak-etag\""); + + impl.execute(host, req, context); + Assert.assertEquals(CacheResponseStatus.CACHE_MODULE_RESPONSE, + context.getAttribute("http.cache.response.context")); + } + + @Test + public void testSetsCacheMissContextIfRequestNotServableFromCache() + throws Exception { + impl = new CachingHttpClient(mockBackend); + HttpRequest req = new HttpGet("http://foo.example.com/"); + req.setHeader("Cache-Control","no-cache"); + HttpResponse resp = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NO_CONTENT, "No Content"); + + EasyMock.expect(mockBackend.execute(EasyMock.isA(HttpHost.class), + EasyMock.isA(HttpRequest.class), EasyMock.isA(HttpContext.class))) + .andReturn(resp); + + replayMocks(); + impl.execute(host, req, context); + verifyMocks(); + Assert.assertEquals(CacheResponseStatus.CACHE_MISS, + context.getAttribute("http.cache.response.context")); + } + + @Test + public void testSetsCacheHitContextIfRequestServedFromCache() + throws Exception { + impl = new CachingHttpClient(mockBackend); + HttpRequest req1 = new HttpGet("http://foo.example.com/"); + HttpRequest req2 = new HttpGet("http://foo.example.com/"); + HttpResponse resp1 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK"); + resp1.setEntity(HttpTestUtils.makeBody(128)); + resp1.setHeader("Content-Length","128"); + resp1.setHeader("ETag","\"etag\""); + resp1.setHeader("Date", DateUtils.formatDate(new Date())); + resp1.setHeader("Cache-Control","public, max-age=3600"); + + EasyMock.expect(mockBackend.execute(EasyMock.isA(HttpHost.class), + EasyMock.isA(HttpRequest.class), EasyMock.isA(HttpContext.class))) + .andReturn(resp1); + + replayMocks(); + impl.execute(host, req1, new BasicHttpContext()); + impl.execute(host, req2, context); + verifyMocks(); + Assert.assertEquals(CacheResponseStatus.CACHE_HIT, + context.getAttribute("http.cache.response.context")); + } + + @Test + public void testSetsValidatedContextIfRequestWasSuccessfullyValidated() + throws Exception { + Date now = new Date(); + Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L); + + impl = new CachingHttpClient(mockBackend); + HttpRequest req1 = new HttpGet("http://foo.example.com/"); + HttpRequest req2 = new HttpGet("http://foo.example.com/"); + + HttpResponse resp1 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK"); + resp1.setEntity(HttpTestUtils.makeBody(128)); + resp1.setHeader("Content-Length","128"); + resp1.setHeader("ETag","\"etag\""); + resp1.setHeader("Date", DateUtils.formatDate(tenSecondsAgo)); + resp1.setHeader("Cache-Control","public, max-age=5"); + + HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK"); + resp2.setEntity(HttpTestUtils.makeBody(128)); + resp2.setHeader("Content-Length","128"); + resp2.setHeader("ETag","\"etag\""); + resp2.setHeader("Date", DateUtils.formatDate(tenSecondsAgo)); + resp2.setHeader("Cache-Control","public, max-age=5"); + + EasyMock.expect(mockBackend.execute(EasyMock.isA(HttpHost.class), + EasyMock.isA(HttpRequest.class), EasyMock.isA(HttpContext.class))) + .andReturn(resp1); + EasyMock.expect(mockBackend.execute(EasyMock.isA(HttpHost.class), + EasyMock.isA(HttpRequest.class), EasyMock.isA(HttpContext.class))) + .andReturn(resp2); + + replayMocks(); + impl.execute(host, req1, new BasicHttpContext()); + impl.execute(host, req2, context); + verifyMocks(); + Assert.assertEquals(CacheResponseStatus.VALIDATED, + context.getAttribute("http.cache.response.context")); + } + + @Test + public void testSetsModuleResponseContextIfValidationRequiredButFailed() + throws Exception { + Date now = new Date(); + Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L); + + impl = new CachingHttpClient(mockBackend); + HttpRequest req1 = new HttpGet("http://foo.example.com/"); + HttpRequest req2 = new HttpGet("http://foo.example.com/"); + + HttpResponse resp1 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK"); + resp1.setEntity(HttpTestUtils.makeBody(128)); + resp1.setHeader("Content-Length","128"); + resp1.setHeader("ETag","\"etag\""); + resp1.setHeader("Date", DateUtils.formatDate(tenSecondsAgo)); + resp1.setHeader("Cache-Control","public, max-age=5, must-revalidate"); + + EasyMock.expect(mockBackend.execute(EasyMock.isA(HttpHost.class), + EasyMock.isA(HttpRequest.class), EasyMock.isA(HttpContext.class))) + .andReturn(resp1); + EasyMock.expect(mockBackend.execute(EasyMock.isA(HttpHost.class), + EasyMock.isA(HttpRequest.class), EasyMock.isA(HttpContext.class))) + .andThrow(new IOException()); + + replayMocks(); + impl.execute(host, req1, new BasicHttpContext()); + impl.execute(host, req2, context); + verifyMocks(); + Assert.assertEquals(CacheResponseStatus.CACHE_MODULE_RESPONSE, + context.getAttribute("http.cache.response.context")); + } + + @Test + public void testSetsModuleResponseContextIfValidationFailsButNotRequired() + throws Exception { + Date now = new Date(); + Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L); + + impl = new CachingHttpClient(mockBackend); + HttpRequest req1 = new HttpGet("http://foo.example.com/"); + HttpRequest req2 = new HttpGet("http://foo.example.com/"); + + HttpResponse resp1 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK"); + resp1.setEntity(HttpTestUtils.makeBody(128)); + resp1.setHeader("Content-Length","128"); + resp1.setHeader("ETag","\"etag\""); + resp1.setHeader("Date", DateUtils.formatDate(tenSecondsAgo)); + resp1.setHeader("Cache-Control","public, max-age=5"); + + EasyMock.expect(mockBackend.execute(EasyMock.isA(HttpHost.class), + EasyMock.isA(HttpRequest.class), EasyMock.isA(HttpContext.class))) + .andReturn(resp1); + EasyMock.expect(mockBackend.execute(EasyMock.isA(HttpHost.class), + EasyMock.isA(HttpRequest.class), EasyMock.isA(HttpContext.class))) + .andThrow(new IOException()); + + replayMocks(); + impl.execute(host, req1, new BasicHttpContext()); + impl.execute(host, req2, context); + verifyMocks(); + Assert.assertEquals(CacheResponseStatus.CACHE_HIT, + context.getAttribute("http.cache.response.context")); + } + @Test public void testIsSharedCache() { Assert.assertTrue(impl.isSharedCache());