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());