hc-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ol...@apache.org
Subject svn commit: r992117 - in /httpcomponents/httpclient/trunk/httpclient-cache/src: main/java/org/apache/http/impl/client/cache/ test/java/org/apache/http/impl/client/cache/
Date Thu, 02 Sep 2010 21:08:08 GMT
Author: olegk
Date: Thu Sep  2 21:08:08 2010
New Revision: 992117

URL: http://svn.apache.org/viewvc?rev=992117&view=rev
Log:
HTTPCLIENT-985: cache module should populate Via header to capture upstream and downstream
protocols
Contributed by Jonathan Moore <jonathan_moore at comcast.com>

Modified:
    httpcomponents/httpclient/trunk/httpclient-cache/src/main/java/org/apache/http/impl/client/cache/CachingHttpClient.java
    httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestCachingHttpClient.java
    httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestProtocolRequirements.java

Modified: httpcomponents/httpclient/trunk/httpclient-cache/src/main/java/org/apache/http/impl/client/cache/CachingHttpClient.java
URL: http://svn.apache.org/viewvc/httpcomponents/httpclient/trunk/httpclient-cache/src/main/java/org/apache/http/impl/client/cache/CachingHttpClient.java?rev=992117&r1=992116&r2=992117&view=diff
==============================================================================
--- httpcomponents/httpclient/trunk/httpclient-cache/src/main/java/org/apache/http/impl/client/cache/CachingHttpClient.java
(original)
+++ httpcomponents/httpclient/trunk/httpclient-cache/src/main/java/org/apache/http/impl/client/cache/CachingHttpClient.java
Thu Sep  2 21:08:08 2010
@@ -35,11 +35,13 @@ import java.util.concurrent.atomic.Atomi
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 import org.apache.http.HttpHost;
+import org.apache.http.HttpMessage;
 import org.apache.http.HttpRequest;
 import org.apache.http.HttpResponse;
 import org.apache.http.HttpStatus;
 import org.apache.http.HttpVersion;
 import org.apache.http.ProtocolException;
+import org.apache.http.ProtocolVersion;
 import org.apache.http.RequestLine;
 import org.apache.http.annotation.ThreadSafe;
 import org.apache.http.client.ClientProtocolException;
@@ -55,6 +57,7 @@ import org.apache.http.impl.client.Defau
 import org.apache.http.message.BasicHttpResponse;
 import org.apache.http.params.HttpParams;
 import org.apache.http.protocol.HttpContext;
+import org.apache.http.util.VersionInfo;
 
 /**
  * @since 4.1
@@ -358,6 +361,8 @@ public class CachingHttpClient implement
         // default response context
         setResponseStatus(context, CacheResponseStatus.CACHE_MISS);
 
+        String via = generateViaHeader(request);
+
         if (clientRequestsOurOptions(request)) {
             setResponseStatus(context, CacheResponseStatus.CACHE_MODULE_RESPONSE);
             return new OptionsHttp11Response();
@@ -375,6 +380,7 @@ public class CachingHttpClient implement
         } catch (ProtocolException e) {
             throw new ClientProtocolException(e);
         }
+        request.addHeader("Via",via);
 
         responseCache.flushInvalidatedCacheEntriesFor(target, request);
 
@@ -429,6 +435,19 @@ public class CachingHttpClient implement
         return callBackend(target, request, context);
     }
 
+    private String generateViaHeader(HttpMessage msg) {
+        final VersionInfo vi = VersionInfo.loadVersionInfo("org.apache.http.client", getClass().getClassLoader());
+        final String release = (vi != null) ? vi.getRelease() : VersionInfo.UNAVAILABLE;
+        final ProtocolVersion pv = msg.getProtocolVersion();
+        if ("http".equalsIgnoreCase(pv.getProtocol())) {
+            return String.format("%d.%d localhost (Apache-HttpClient/%s (cache))",
+                pv.getMajor(), pv.getMinor(), release);
+        } else {
+            return String.format("%s/%d.%d localhost (Apache-HttpClient/%s (cache))",
+                    pv.getProtocol(), pv.getMajor(), pv.getMinor(), release);
+        }
+    }
+
     private void setResponseStatus(final HttpContext context, final CacheResponseStatus value)
{
         if (context != null) {
             context.setAttribute(CACHE_RESPONSE_STATUS, value);
@@ -469,6 +488,7 @@ public class CachingHttpClient implement
 
         log.debug("Calling the backend");
         HttpResponse backendResponse = backend.execute(target, request, context);
+        backendResponse.addHeader("Via", generateViaHeader(backendResponse));
         return handleBackendResponse(target, request, requestDate, getCurrentDate(),
                 backendResponse);
 

Modified: httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestCachingHttpClient.java
URL: http://svn.apache.org/viewvc/httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestCachingHttpClient.java?rev=992117&r1=992116&r2=992117&view=diff
==============================================================================
--- httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestCachingHttpClient.java
(original)
+++ httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestCachingHttpClient.java
Thu Sep  2 21:08:08 2010
@@ -55,6 +55,7 @@ import org.apache.http.params.BasicHttpP
 import org.apache.http.params.HttpParams;
 import org.apache.http.protocol.BasicHttpContext;
 import org.apache.http.protocol.HttpContext;
+import org.easymock.Capture;
 import org.easymock.classextension.EasyMock;
 import org.junit.Assert;
 import org.junit.Before;
@@ -371,10 +372,11 @@ public class TestCachingHttpClient {
     @Test
     public void testCallBackendMakesBackEndRequestAndHandlesResponse() throws Exception {
         mockImplMethods(GET_CURRENT_DATE, HANDLE_BACKEND_RESPONSE);
+        HttpResponse resp = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK,
"OK");
         getCurrentDateReturns(requestDate);
-        backendCallWasMadeWithRequest(request);
+        backendCallWasMade(request, resp);
         getCurrentDateReturns(responseDate);
-        handleBackendResponseReturnsResponse(request, mockBackendResponse);
+        handleBackendResponseReturnsResponse(request, resp);
 
         replayMocks();
 
@@ -756,6 +758,30 @@ public class TestCachingHttpClient {
     }
 
     @Test
+    public void testRecordsClientProtocolInViaHeaderIfRequestNotServableFromCache()
+        throws Exception {
+        impl = new CachingHttpClient(mockBackend);
+        HttpRequest req = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_0);
+        req.setHeader("Cache-Control","no-cache");
+        HttpResponse resp = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NO_CONTENT,
"No Content");
+        Capture<HttpRequest> cap = new Capture<HttpRequest>();
+
+        EasyMock.expect(mockBackend.execute(EasyMock.isA(HttpHost.class),
+                EasyMock.capture(cap), EasyMock.isA(HttpContext.class)))
+            .andReturn(resp);
+
+        replayMocks();
+        impl.execute(host, req, context);
+        verifyMocks();
+
+        HttpRequest captured = cap.getValue();
+        String via = captured.getFirstHeader("Via").getValue();
+        String proto = via.split("\\s+")[0];
+        Assert.assertTrue("http/1.0".equalsIgnoreCase(proto) ||
+                "1.0".equalsIgnoreCase(proto));
+    }
+
+    @Test
     public void testSetsCacheMissContextIfRequestNotServableFromCache()
         throws Exception {
         impl = new CachingHttpClient(mockBackend);
@@ -775,6 +801,46 @@ public class TestCachingHttpClient {
     }
 
     @Test
+    public void testSetsViaHeaderOnResponseIfRequestNotServableFromCache()
+            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), (HttpContext)EasyMock.isNull()))
+                .andReturn(resp);
+
+        replayMocks();
+        HttpResponse result = impl.execute(host, req);
+        verifyMocks();
+        Assert.assertNotNull(result.getFirstHeader("Via"));
+    }
+
+    @Test
+    public void testSetsViaHeaderOnResponseForCacheMiss()
+        throws Exception {
+        impl = new CachingHttpClient(mockBackend);
+        HttpRequest req1 = 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();
+        HttpResponse result = impl.execute(host, req1, new BasicHttpContext());
+        verifyMocks();
+        Assert.assertNotNull(result.getFirstHeader("Via"));
+    }
+
+    @Test
     public void testSetsCacheHitContextIfRequestServedFromCache()
         throws Exception {
         impl = new CachingHttpClient(mockBackend);
@@ -800,6 +866,30 @@ public class TestCachingHttpClient {
     }
 
     @Test
+    public void testSetsViaHeaderOnResponseIfRequestServedFromCache()
+        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), (HttpContext)EasyMock.isNull()))
+            .andReturn(resp1);
+
+        replayMocks();
+        impl.execute(host, req1);
+        HttpResponse result = impl.execute(host, req2);
+        verifyMocks();
+        Assert.assertNotNull(result.getFirstHeader("Via"));
+    }
+
+    @Test
     public void testSetsValidatedContextIfRequestWasSuccessfullyValidated()
         throws Exception {
         Date now = new Date();
@@ -839,6 +929,45 @@ public class TestCachingHttpClient {
     }
 
     @Test
+    public void testSetsViaHeaderIfRequestWasSuccessfullyValidated()
+        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());
+        HttpResponse result = impl.execute(host, req2, context);
+        verifyMocks();
+        Assert.assertNotNull(result.getFirstHeader("Via"));
+    }
+
+
+    @Test
     public void testSetsModuleResponseContextIfValidationRequiredButFailed()
         throws Exception {
         Date now = new Date();
@@ -903,6 +1032,38 @@ public class TestCachingHttpClient {
     }
 
     @Test
+    public void testSetViaHeaderIfValidationFailsButNotRequired()
+        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());
+        HttpResponse result = impl.execute(host, req2, context);
+        verifyMocks();
+        Assert.assertNotNull(result.getFirstHeader("Via"));
+    }
+
+
+    @Test
     public void testIsSharedCache() {
         Assert.assertTrue(impl.isSharedCache());
     }
@@ -967,6 +1128,13 @@ public class TestCachingHttpClient {
                 EasyMock.<HttpContext>anyObject())).andReturn(mockBackendResponse);
     }
 
+    private void backendCallWasMade(HttpRequest request, HttpResponse response) throws IOException
{
+        EasyMock.expect(mockBackend.execute(
+                EasyMock.<HttpHost>anyObject(),
+                EasyMock.same(request),
+                EasyMock.<HttpContext>anyObject())).andReturn(response);
+    }
+
     private void responsePolicyAllowsCaching(boolean allow) {
         EasyMock.expect(
                 mockResponsePolicy.isResponseCacheable(

Modified: httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestProtocolRequirements.java
URL: http://svn.apache.org/viewvc/httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestProtocolRequirements.java?rev=992117&r1=992116&r2=992117&view=diff
==============================================================================
--- httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestProtocolRequirements.java
(original)
+++ httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestProtocolRequirements.java
Thu Sep  2 21:08:08 2010
@@ -31,6 +31,8 @@ import java.io.InputStream;
 import java.net.SocketTimeoutException;
 import java.util.Date;
 import java.util.Random;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 import org.apache.http.Header;
 import org.apache.http.HeaderElement;
@@ -5463,5 +5465,147 @@ public class TestProtocolRequirements ex
         }
     }
 
+    /* "The Via general-header field MUST be used by gateways and proxies
+     * to indicate the intermediate protocols and recipients between the
+     * user agent and the server on requests, and between the origin server
+     * and the client on responses."
+     *
+     * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.45
+     */
+    @Test
+    public void testProperlyFormattedViaHeaderIsAddedToRequests() throws Exception {
+        Capture<HttpRequest> cap = new Capture<HttpRequest>();
+        request.removeHeaders("Via");
+        EasyMock.expect(mockBackend.execute(EasyMock.isA(HttpHost.class),
+                EasyMock.capture(cap), (HttpContext)EasyMock.isNull()))
+            .andReturn(originResponse);
+
+        replayMocks();
+        impl.execute(host, request);
+        verifyMocks();
+
+        HttpRequest captured = cap.getValue();
+        String via = captured.getFirstHeader("Via").getValue();
+        assertValidViaHeader(via);
+    }
+
+    @Test
+    public void testProperlyFormattedViaHeaderIsAddedToResponses() throws Exception {
+        originResponse.removeHeaders("Via");
+        backendExpectsAnyRequest().andReturn(originResponse);
+        replayMocks();
+        HttpResponse result = impl.execute(host, request);
+        verifyMocks();
+        assertValidViaHeader(result.getFirstHeader("Via").getValue());
+    }
+
+
+    private void assertValidViaHeader(String via) {
+        //    	Via =  "Via" ":" 1#( received-protocol received-by [ comment ] )
+        //        received-protocol = [ protocol-name "/" ] protocol-version
+        //        protocol-name     = token
+        //        protocol-version  = token
+        //        received-by       = ( host [ ":" port ] ) | pseudonym
+        //        pseudonym         = token
+
+        String[] parts = via.split("\\s+");
+        Assert.assertTrue(parts.length >= 2);
+
+        // received protocol
+        String receivedProtocol = parts[0];
+        String[] protocolParts = receivedProtocol.split("/");
+        Assert.assertTrue(protocolParts.length >= 1);
+        Assert.assertTrue(protocolParts.length <= 2);
+
+        final String tokenRegexp = "[^\\p{Cntrl}()<>@,;:\\\\\"/\\[\\]?={} \\t]+";
+        for(String protocolPart : protocolParts) {
+            Assert.assertTrue(Pattern.matches(tokenRegexp, protocolPart));
+        }
+
+        // received-by
+        if (!Pattern.matches(tokenRegexp, parts[1])) {
+            // host : port
+            new HttpHost(parts[1]);
+        }
+
+        // comment
+        if (parts.length > 2) {
+            StringBuilder buf = new StringBuilder(parts[2]);
+            for(int i=3; i<parts.length; i++) {
+                buf.append(" "); buf.append(parts[i]);
+            }
+            Assert.assertTrue(isValidComment(buf.toString()));
+        }
+    }
 
+    private boolean isValidComment(String s) {
+        final String leafComment = "^\\(([^\\p{Cntrl}()]|\\\\\\p{ASCII})*\\)$";
+        final String nestedPrefix = "^\\(([^\\p{Cntrl}()]|\\\\\\p{ASCII})*\\(";
+        final String nestedSuffix = "\\)([^\\p{Cntrl}()]|\\\\\\p{ASCII})*\\)$";
+
+        if (Pattern.matches(leafComment,s)) return true;
+        Matcher pref = Pattern.compile(nestedPrefix).matcher(s);
+        Matcher suff = Pattern.compile(nestedSuffix).matcher(s);
+        if (!pref.find()) return false;
+        if (!suff.find()) return false;
+        return isValidComment(s.substring(pref.end() - 1, suff.start() + 1));
+    }
+
+
+    /*
+     * "The received-protocol indicates the protocol version of the message
+     * received by the server or client along each segment of the request/
+     * response chain. The received-protocol version is appended to the Via
+     * field value when the message is forwarded so that information about
+     * the protocol capabilities of upstream applications remains visible
+     * to all recipients."
+     *
+     * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.45
+     */
+    @Test
+    public void testViaHeaderOnRequestProperlyRecordsClientProtocol()
+            throws Exception {
+        request = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_0);
+        request.removeHeaders("Via");
+        Capture<HttpRequest> cap = new Capture<HttpRequest>();
+        EasyMock.expect(mockBackend.execute(EasyMock.isA(HttpHost.class),
+                EasyMock.capture(cap), (HttpContext)EasyMock.isNull()))
+                .andReturn(originResponse);
+
+        replayMocks();
+        impl.execute(host, request);
+        verifyMocks();
+
+        HttpRequest captured = cap.getValue();
+        String via = captured.getFirstHeader("Via").getValue();
+        String protocol = via.split("\\s+")[0];
+        String[] protoParts = protocol.split("/");
+        if (protoParts.length > 1) {
+            Assert.assertTrue("http".equalsIgnoreCase(protoParts[0]));
+        }
+        Assert.assertEquals("1.0",protoParts[protoParts.length-1]);
+    }
+
+    @Test
+    public void testViaHeaderOnResponseProperlyRecordsOriginProtocol()
+        throws Exception {
+
+        originResponse = new BasicHttpResponse(HttpVersion.HTTP_1_0, HttpStatus.SC_NO_CONTENT,
"No Content");
+
+        backendExpectsAnyRequest().andReturn(originResponse);
+
+        replayMocks();
+        HttpResponse result = impl.execute(host, request);
+        verifyMocks();
+
+        String via = result.getFirstHeader("Via").getValue();
+        String protocol = via.split("\\s+")[0];
+        String[] protoParts = protocol.split("/");
+        Assert.assertTrue(protoParts.length >= 1);
+        Assert.assertTrue(protoParts.length <= 2);
+        if (protoParts.length > 1) {
+            Assert.assertTrue("http".equalsIgnoreCase(protoParts[0]));
+        }
+        Assert.assertEquals("1.0", protoParts[protoParts.length - 1]);
+    }
 }
\ No newline at end of file



Mime
View raw message