Author: ssievers
Date: Sat Mar  9 22:44:54 2013
New Revision: 1454767

URL: http://svn.apache.org/r1454767
Log:
SHINDIG-1905 | DefaultRequestPipeline should attempt conditional gets for 
stale, cached responses 

Modified:
    
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/DefaultRequestPipeline.java
    
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/HttpResponse.java
    
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/HttpResponseBuilder.java
    
shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/DefaultRequestPipelineTest.java

Modified: 
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/DefaultRequestPipeline.java
URL: 
http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/DefaultRequestPipeline.java?rev=1454767&r1=1454766&r2=1454767&view=diff
==============================================================================
--- 
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/DefaultRequestPipeline.java
 (original)
+++ 
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/DefaultRequestPipeline.java
 Sat Mar  9 22:44:54 2013
@@ -18,6 +18,8 @@
  */
 package org.apache.shindig.gadgets.http;
 
+import com.google.common.collect.Multimap;
+import com.google.common.net.HttpHeaders;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -61,7 +63,7 @@ public class DefaultRequestPipeline impl
 
   @Inject(optional = true) @Named("shindig.http.date-drift-limit-ms")
   private static long responseDateDriftLimit = DEFAULT_DRIFT_LIMIT_MS;
-  
+
   //class name for logging purpose
   private static final String classname = 
DefaultRequestPipeline.class.getName();
   private static final Logger LOG = 
Logger.getLogger(classname,MessageKeys.MESSAGES);
@@ -100,7 +102,7 @@ public class DefaultRequestPipeline impl
       if (!cachedResponse.isStale()) {
         if (invalidationService.isValid(request, cachedResponse)) {
           if(LOG.isLoggable(Level.FINEST)) {
-            LOG.logp(Level.FINEST, classname, method, 
MessageKeys.CACHED_RESPONSE, 
+            LOG.logp(Level.FINEST, classname, method, 
MessageKeys.CACHED_RESPONSE,
                     new Object[]{request.getUri().toString()});
           }
           return cachedResponse;
@@ -114,6 +116,22 @@ public class DefaultRequestPipeline impl
         }
       }
     }
+
+    // If we have a stale response, perform a conditional GET.
+    // Note: Fixing up the request with these headers will not affect http 
response caching. See
+    // org.apache.shindig.gadgets.http.AbstractHttpCache.createKey(HttpRequest)
+    if (staleResponse != null) {
+      final String lastModified = 
staleResponse.getHeader(HttpHeaders.LAST_MODIFIED);
+      if (lastModified != null) {
+        request.setHeader(HttpHeaders.IF_MODIFIED_SINCE, lastModified);
+      }
+
+      final String etag = staleResponse.getHeader(HttpHeaders.ETAG);
+      if (etag != null) {
+        request.setHeader(HttpHeaders.IF_NONE_MATCH, etag);
+      }
+    }
+
     HttpResponse fetchedResponse = fetchResponse(request);
     fetchedResponse = fixFetchedResponse(request, fetchedResponse, 
invalidatedResponse,
         staleResponse);
@@ -219,6 +237,38 @@ public class DefaultRequestPipeline impl
 
     fetchedResponse = maybeFixDriftTime(fetchedResponse);
 
+    // 304 Not Modified
+    // Return the stale response and update what is in the cache with any new 
headers per
+    // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
+    //
+    // "If a cache uses a received 304 response to update a cache entry,
+    //  the cache MUST update the entry to reflect any new field values
+    //  given in the response."
+    if (fetchedResponse.getHttpStatusCode() == HttpResponse.SC_NOT_MODIFIED) {
+
+      // Update the stale response's headers with the new headers from the 
fetched response.
+      // If the response from the remote server doesn't have an "Expires" or 
"Cache-Control" header,
+      // we should service the current request and remove the stale response 
from the cache. We rely
+      // on those headers to be present so that the response that is in the 
cache will return the
+      // correct value when isStale() is called. Without removing the stale 
response, we would get
+      // stuck in a loop of doing conditional GETs for the same stale resource 
every time it is
+      // requested.
+      final Multimap<String, String> fetchedResponseHeaders = 
fetchedResponse.getHeaders();
+      if (fetchedResponse.getCacheControlMaxAge() == -1
+              && fetchedResponse.getExpiresTime() == -1) {
+        // CONSIDER: We could try to recurse in this case and do a 
non-conditional-get for the resource.
+        return httpCache.removeResponse(request);
+      }
+
+      HttpResponseBuilder httpResponseBuilder = new 
HttpResponseBuilder(staleResponse);
+      for (String headerName : fetchedResponseHeaders.keySet()) {
+        httpResponseBuilder.removeHeader(headerName);
+      }
+
+      httpResponseBuilder.addAllHeaders(fetchedResponse.getHeaders());
+      return cacheFetchedResponse(request, httpResponseBuilder.create());
+    }
+
     if (!fetchedResponse.isError() && !request.getIgnoreCache() && 
request.getCacheTtl() != 0) {
       try {
         fetchedResponse =

Modified: 
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/HttpResponse.java
URL: 
http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/HttpResponse.java?rev=1454767&r1=1454766&r2=1454767&view=diff
==============================================================================
--- 
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/HttpResponse.java
 (original)
+++ 
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/HttpResponse.java
 Sat Mar  9 22:44:54 2013
@@ -445,7 +445,7 @@ public final class HttpResponse implemen
   /**
    * @return the expiration time from the Expires header or -1 if not set
    */
-  private long getExpiresTime() {
+  public long getExpiresTime() {
     String expires = getHeader(HttpHeaders.EXPIRES);
     if (expires != null) {
       Date expiresDate = DateUtil.parseRfc1123Date(expires);
@@ -668,6 +668,8 @@ public final class HttpResponse implemen
       return new LinkedList<String>();  //To change body of implemented 
methods use File | Settings | File Templates.
     }
   }
+
+  // FIXME: Why isn't this a ListMultimap?  Headers should be ordered and we 
want to be able to do type checks on our Multimap.
   public static Multimap<String,String> newHeaderMultimap() {
     TreeMap<String,Collection<String>> map = new 
TreeMap<String,Collection<String>>(String.CASE_INSENSITIVE_ORDER);
     return Multimaps.newMultimap(map, HEADER_COLLECTION_SUPPLIER);

Modified: 
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/HttpResponseBuilder.java
URL: 
http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/HttpResponseBuilder.java?rev=1454767&r1=1454766&r2=1454767&view=diff
==============================================================================
--- 
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/HttpResponseBuilder.java
 (original)
+++ 
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/HttpResponseBuilder.java
 Sat Mar  9 22:44:54 2013
@@ -212,6 +212,20 @@ public class HttpResponseBuilder extends
     return this;
   }
 
+  /**
+   * Adds all the headers in the given multimap to the HttpResponse that is 
under construction.
+   *
+   * @param headers
+   *          A multimap of header keys and values. WARNING: This Multimap 
should be one constructed
+   *          by 
org.apache.shindig.gadgets.http.HttpResponse.newHeaderMultimap()
+   * @return <code>this</code>
+   */
+  public HttpResponseBuilder addAllHeaders(Multimap<String, String> headers) {
+    this.headers.putAll(headers);
+    incrementNumChanges();
+    return this;
+  }
+
   public Collection<String> removeHeader(String name) {
     Collection<String> ret = headers.removeAll(name);
     if (ret != null) {

Modified: 
shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/DefaultRequestPipelineTest.java
URL: 
http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/DefaultRequestPipelineTest.java?rev=1454767&r1=1454766&r2=1454767&view=diff
==============================================================================
--- 
shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/DefaultRequestPipelineTest.java
 (original)
+++ 
shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/DefaultRequestPipelineTest.java
 Sat Mar  9 22:44:54 2013
@@ -22,6 +22,7 @@ import static org.junit.Assert.assertEqu
 import static org.junit.Assert.assertSame;
 
 import com.google.common.collect.Maps;
+import com.google.common.net.HttpHeaders;
 import com.google.inject.Provider;
 
 import org.apache.shindig.common.uri.Uri;
@@ -39,6 +40,7 @@ import java.util.Map;
 
 public class DefaultRequestPipelineTest {
   private static final Uri DEFAULT_URI = 
Uri.parse("http://example.org/gadget.xml";);
+  private static final String RFC1123_EPOCH = DateUtil.formatRfc1123Date(0);
 
   private final FakeHttpFetcher fetcher = new FakeHttpFetcher();
   private final FakeHttpCache cache = new FakeHttpCache();
@@ -217,6 +219,105 @@ public class DefaultRequestPipelineTest 
   }
 
   @Test
+  public void authTypeNoneStaleConditionalGet() throws Exception {
+    // Cached response that is stale.  Test that a conditional GET is used.
+    // Verify that the cached response is updated and returned.
+    // Verify that the 304 doesn't get cached.
+    HttpRequest request = new 
HttpRequest(DEFAULT_URI).setAuthType(AuthType.NONE);
+
+    HttpResponse cached = new HttpResponseBuilder()
+                                .setHeader(HttpHeaders.LAST_MODIFIED, 
RFC1123_EPOCH)
+                                .setHeader(HttpHeaders.ETAG, "ETAG")
+                                .setCacheTtl(-1)
+                                .create();
+    cache.data.put(DEFAULT_URI, cached);
+
+    String expiresDate = DateUtil.formatRfc1123Date(System.currentTimeMillis() 
+ 3600 * 1000);
+    String maxAge = "max-age=3600";
+    HttpResponse notModified = new HttpResponseBuilder()
+                                    
.setHttpStatusCode(HttpResponse.SC_NOT_MODIFIED)
+                                    .setHeader(HttpHeaders.EXPIRES, 
expiresDate)
+                                    .setHeader(HttpHeaders.CACHE_CONTROL, 
maxAge)
+                                    .create();
+    fetcher.response = notModified;
+    HttpResponse response = pipeline.execute(request);
+
+    HttpResponse expectedResponse = new HttpResponseBuilder(cached)
+                                      .setHeader(HttpHeaders.EXPIRES, 
expiresDate)
+                                      .setHeader(HttpHeaders.CACHE_CONTROL, 
maxAge)
+                                      .create();
+
+    assertEquals(RFC1123_EPOCH, 
fetcher.request.getHeader(HttpHeaders.IF_MODIFIED_SINCE));
+    assertEquals("ETAG", fetcher.request.getHeader(HttpHeaders.IF_NONE_MATCH));
+    assertEquals(expectedResponse, response);
+    assertEquals(expectedResponse, cache.data.get(DEFAULT_URI));
+    assertEquals(1, cache.readCount);
+    assertEquals(1, cache.writeCount);
+    assertEquals(1, fetcher.fetchCount);
+  }
+
+  @Test
+  public void authTypeNoneStaleConditionalGetNoExpiresNoMaxAge() throws 
Exception {
+    // Cached response that is stale and a conditional GET is used. Response 
has no Expires
+    // header and no Cache-Control header with max-age. Remove the cached 
entry from the cache and
+    // return it.
+    HttpRequest request = new 
HttpRequest(DEFAULT_URI).setAuthType(AuthType.NONE);
+
+    HttpResponse cached = new HttpResponseBuilder()
+                                .setHeader(HttpHeaders.LAST_MODIFIED, 
RFC1123_EPOCH)
+                                .setCacheTtl(-1)
+                                .create();
+    cache.data.put(DEFAULT_URI, cached);
+
+    HttpResponse notModified = new HttpResponseBuilder()
+                                    
.setHttpStatusCode(HttpResponse.SC_NOT_MODIFIED)
+                                    .create();
+    fetcher.response = notModified;
+    HttpResponse response = pipeline.execute(request);
+
+    assertEquals(RFC1123_EPOCH, 
fetcher.request.getHeader(HttpHeaders.IF_MODIFIED_SINCE));
+    assertEquals(cached, response);
+    assertEquals(null, cache.data.get(DEFAULT_URI));
+    assertEquals(1, cache.readCount);
+    assertEquals(0, cache.writeCount);
+    assertEquals(1, fetcher.fetchCount);
+  }
+
+  @Test
+  public void authTypeNoneStaleConditionalGetNoLastModified() throws Exception 
{
+    // Cached response is stale and has no Last-Modified header on it. Test 
that an
+    // If-Modified-Since header is not issued.
+    HttpRequest request = new 
HttpRequest(DEFAULT_URI).setAuthType(AuthType.NONE);
+
+    HttpResponse cached = new HttpResponseBuilder()
+                                .setCacheTtl(-1)
+                                .create();
+    cache.data.put(DEFAULT_URI, cached);
+
+    fetcher.response = HttpResponse.error(); // Really don't care what this is.
+    pipeline.execute(request);
+
+    assertEquals(null, 
fetcher.request.getHeader(HttpHeaders.IF_MODIFIED_SINCE));
+  }
+
+  @Test
+  public void authTypeNoneStaleConditionalGetNoEtag() throws Exception {
+    // Cached response is stale and has no Etag header on it. Test that an
+    // If-None-Match header is not issued.
+    HttpRequest request = new 
HttpRequest(DEFAULT_URI).setAuthType(AuthType.NONE);
+
+    HttpResponse cached = new HttpResponseBuilder()
+                                .setCacheTtl(-1)
+                                .create();
+    cache.data.put(DEFAULT_URI, cached);
+
+    fetcher.response = HttpResponse.error(); // Really don't care what this is.
+    pipeline.execute(request);
+
+    assertEquals(null, fetcher.request.getHeader(HttpHeaders.IF_NONE_MATCH));
+  }
+
+  @Test
   public void authTypeOAuthNotCached() throws Exception {
     HttpRequest request = new 
HttpRequest(DEFAULT_URI).setAuthType(AuthType.OAUTH);
 
@@ -337,7 +438,7 @@ public class DefaultRequestPipelineTest 
     }
 
     public HttpResponse removeResponse(HttpRequest key) {
-      throw new UnsupportedOperationException();
+      return data.remove(key.getUri());
     }
 
     public String createKey(HttpRequest request) {


Reply via email to