You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hc.apache.org by ol...@apache.org on 2017/12/27 10:55:46 UTC

[2/4] httpcomponents-client git commit: HTTPCLIENT-1824: asynchronous HTTP cache invalidator

HTTPCLIENT-1824: asynchronous HTTP cache invalidator


Project: http://git-wip-us.apache.org/repos/asf/httpcomponents-client/repo
Commit: http://git-wip-us.apache.org/repos/asf/httpcomponents-client/commit/3f52d0bf
Tree: http://git-wip-us.apache.org/repos/asf/httpcomponents-client/tree/3f52d0bf
Diff: http://git-wip-us.apache.org/repos/asf/httpcomponents-client/diff/3f52d0bf

Branch: refs/heads/master
Commit: 3f52d0bf90c4d9ec09cf8e8a3d583cea05f94b0e
Parents: 6200a17
Author: Oleg Kalnichevski <ol...@apache.org>
Authored: Sun Dec 17 14:34:20 2017 +0100
Committer: Oleg Kalnichevski <ol...@apache.org>
Committed: Tue Dec 26 18:12:18 2017 +0100

----------------------------------------------------------------------
 .../http/cache/HttpAsyncCacheInvalidator.java   |  84 +++
 .../http/impl/cache/CacheInvalidatorBase.java   | 105 +++
 .../http/impl/cache/CachingExecBase.java        |   2 +-
 .../cache/DefaultAsyncCacheInvalidator.java     | 266 +++++++
 .../impl/cache/DefaultCacheInvalidator.java     | 125 +---
 .../cache/TestDefaultAsyncCacheInvalidator.java | 696 +++++++++++++++++++
 .../impl/cache/TestDefaultCacheInvalidator.java |  12 +-
 7 files changed, 1170 insertions(+), 120 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/3f52d0bf/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/HttpAsyncCacheInvalidator.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/HttpAsyncCacheInvalidator.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/HttpAsyncCacheInvalidator.java
new file mode 100644
index 0000000..fa9684b
--- /dev/null
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/HttpAsyncCacheInvalidator.java
@@ -0,0 +1,84 @@
+/*
+ * ====================================================================
+ * 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
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.client5.http.cache;
+
+import java.net.URI;
+
+import org.apache.hc.core5.annotation.Internal;
+import org.apache.hc.core5.concurrent.Cancellable;
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.function.Resolver;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.HttpRequest;
+import org.apache.hc.core5.http.HttpResponse;
+
+/**
+ * Given a particular HTTP request / response pair, flush any cache entries
+ * that this exchange would invalidate.
+ *
+ * @since 5.0
+ */
+@Internal
+public interface HttpAsyncCacheInvalidator {
+
+    /**
+     * Remove cache entries from the cache that are no longer fresh or have been
+     * invalidated in some way.
+     *
+     * @param host backend host
+     * @param request request message
+     * @param cacheKeyResolver cache key resolver used by cache storage
+     * @param cacheStorage internal cache storage
+     * @param callback result callback
+     */
+    Cancellable flushInvalidatedCacheEntries(
+            HttpHost host,
+            HttpRequest request,
+            Resolver<URI, String> cacheKeyResolver,
+            HttpAsyncCacheStorage cacheStorage,
+            FutureCallback<Boolean> callback);
+
+    /**
+     * Flushes entries that were invalidated by the given response received for
+     * the given host/request pair.
+     *
+     * @param host backend host
+     * @param request request message
+     * @param response response message
+     * @param cacheKeyResolver cache key resolver used by cache storage
+     * @param cacheStorage internal cache storage
+     * @param callback result callback
+     */
+    Cancellable flushInvalidatedCacheEntries(
+            HttpHost host,
+            HttpRequest request,
+            HttpResponse response,
+            Resolver<URI, String> cacheKeyResolver,
+            HttpAsyncCacheStorage cacheStorage,
+            FutureCallback<Boolean> callback);
+
+}

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/3f52d0bf/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheInvalidatorBase.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheInvalidatorBase.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheInvalidatorBase.java
new file mode 100644
index 0000000..d229011
--- /dev/null
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheInvalidatorBase.java
@@ -0,0 +1,105 @@
+/*
+ * ====================================================================
+ * 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
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.client5.http.impl.cache;
+
+import java.net.URI;
+
+import org.apache.hc.client5.http.cache.HeaderConstants;
+import org.apache.hc.client5.http.cache.HttpCacheEntry;
+import org.apache.hc.client5.http.utils.DateUtils;
+import org.apache.hc.client5.http.utils.URIUtils;
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.HttpHeaders;
+import org.apache.hc.core5.http.HttpRequest;
+import org.apache.hc.core5.http.HttpResponse;
+
+class CacheInvalidatorBase {
+
+    static boolean shouldInvalidateHeadCacheEntry(final HttpRequest req, final HttpCacheEntry parentCacheEntry) {
+        return requestIsGet(req) && isAHeadCacheEntry(parentCacheEntry);
+    }
+
+    static boolean requestIsGet(final HttpRequest req) {
+        return req.getMethod().equals((HeaderConstants.GET_METHOD));
+    }
+
+    static boolean isAHeadCacheEntry(final HttpCacheEntry parentCacheEntry) {
+        return parentCacheEntry != null && parentCacheEntry.getRequestMethod().equals(HeaderConstants.HEAD_METHOD);
+    }
+
+    static boolean isSameHost(final URI requestURI, final URI targetURI) {
+        return targetURI.isAbsolute() && targetURI.getAuthority().equalsIgnoreCase(requestURI.getAuthority());
+    }
+
+    static boolean requestShouldNotBeCached(final HttpRequest req) {
+        final String method = req.getMethod();
+        return notGetOrHeadRequest(method);
+    }
+
+    static boolean notGetOrHeadRequest(final String method) {
+        return !(HeaderConstants.GET_METHOD.equals(method) || HeaderConstants.HEAD_METHOD
+                .equals(method));
+    }
+    private static URI getLocationURI(final URI requestUri, final HttpResponse response, final String headerName) {
+        final Header h = response.getFirstHeader(headerName);
+        if (h == null) {
+            return null;
+        }
+        final URI locationUri = HttpCacheSupport.normalizeQuetly(h.getValue());
+        if (locationUri == null) {
+            return requestUri;
+        }
+        if (locationUri.isAbsolute()) {
+            return locationUri;
+        } else {
+            return URIUtils.resolve(requestUri, locationUri);
+        }
+    }
+
+    static URI getContentLocationURI(final URI requestUri, final HttpResponse response) {
+        return getLocationURI(requestUri, response, HttpHeaders.CONTENT_LOCATION);
+    }
+
+    static URI getLocationURI(final URI requestUri, final HttpResponse response) {
+        return getLocationURI(requestUri, response, HttpHeaders.LOCATION);
+    }
+
+    static boolean responseAndEntryEtagsDiffer(final HttpResponse response,
+            final HttpCacheEntry entry) {
+        final Header entryEtag = entry.getFirstHeader(HeaderConstants.ETAG);
+        final Header responseEtag = response.getFirstHeader(HeaderConstants.ETAG);
+        if (entryEtag == null || responseEtag == null) {
+            return false;
+        }
+        return (!entryEtag.getValue().equals(responseEtag.getValue()));
+    }
+
+    static boolean responseDateOlderThanEntryDate(final HttpResponse response, final HttpCacheEntry entry) {
+        return DateUtils.isBefore(response, entry, HttpHeaders.DATE);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/3f52d0bf/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExecBase.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExecBase.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExecBase.java
index 39300f3..068432b 100644
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExecBase.java
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExecBase.java
@@ -100,7 +100,7 @@ public class CachingExecBase {
         this.cacheConfig = config != null ? config : CacheConfig.DEFAULT;
     }
 
-    public CachingExecBase(final HttpCache cache, final CacheConfig config) {
+    CachingExecBase(final HttpCache cache, final CacheConfig config) {
         super();
         this.responseCache = Args.notNull(cache, "Response cache");
         this.cacheConfig = config != null ? config : CacheConfig.DEFAULT;

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/3f52d0bf/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/DefaultAsyncCacheInvalidator.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/DefaultAsyncCacheInvalidator.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/DefaultAsyncCacheInvalidator.java
new file mode 100644
index 0000000..c815ce6
--- /dev/null
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/DefaultAsyncCacheInvalidator.java
@@ -0,0 +1,266 @@
+/*
+ * ====================================================================
+ * 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
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.client5.http.impl.cache;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.hc.client5.http.cache.HttpAsyncCacheInvalidator;
+import org.apache.hc.client5.http.cache.HttpAsyncCacheStorage;
+import org.apache.hc.client5.http.cache.HttpCacheEntry;
+import org.apache.hc.client5.http.impl.Operations;
+import org.apache.hc.client5.http.utils.URIUtils;
+import org.apache.hc.core5.annotation.Contract;
+import org.apache.hc.core5.annotation.Internal;
+import org.apache.hc.core5.annotation.ThreadingBehavior;
+import org.apache.hc.core5.concurrent.Cancellable;
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.function.Resolver;
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.HttpRequest;
+import org.apache.hc.core5.http.HttpResponse;
+import org.apache.hc.core5.http.HttpStatus;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+/**
+ * Given a particular HTTP request / response pair, flush any cache entries
+ * that this exchange would invalidate.
+ *
+ * @since 5.0
+ */
+@Contract(threading = ThreadingBehavior.IMMUTABLE)
+@Internal
+public class DefaultAsyncCacheInvalidator extends CacheInvalidatorBase implements HttpAsyncCacheInvalidator {
+
+    public static final DefaultAsyncCacheInvalidator INSTANCE = new DefaultAsyncCacheInvalidator();
+
+    private final Logger log = LogManager.getLogger(getClass());
+
+    private void removeEntry(final HttpAsyncCacheStorage storage, final String cacheKey) {
+        storage.removeEntry(cacheKey, new FutureCallback<Boolean>() {
+
+            @Override
+            public void completed(final Boolean result) {
+                if (log.isDebugEnabled()) {
+                    if (result) {
+                        log.debug("Cache entry with key " + cacheKey + " successfully flushed");
+                    } else {
+                        log.debug("Cache entry with key " + cacheKey + " could not be flushed");
+                    }
+                }
+            }
+
+            @Override
+            public void failed(final Exception ex) {
+                if (log.isWarnEnabled()) {
+                    log.warn("Unable to flush cache entry with key " + cacheKey, ex);
+                }
+            }
+
+            @Override
+            public void cancelled() {
+            }
+
+        });
+    }
+
+    @Override
+    public Cancellable flushInvalidatedCacheEntries(
+            final HttpHost host,
+            final HttpRequest request,
+            final Resolver<URI, String> cacheKeyResolver,
+            final HttpAsyncCacheStorage storage,
+            final FutureCallback<Boolean> callback) {
+        final String s = HttpCacheSupport.getRequestUri(request, host);
+        final URI uri = HttpCacheSupport.normalizeQuetly(s);
+        final String cacheKey = uri != null ? cacheKeyResolver.resolve(uri) : s;
+        return storage.getEntry(cacheKey, new FutureCallback<HttpCacheEntry>() {
+
+            @Override
+            public void completed(final HttpCacheEntry parentEntry) {
+                if (requestShouldNotBeCached(request) || shouldInvalidateHeadCacheEntry(request, parentEntry)) {
+                    if (parentEntry != null) {
+                        if (log.isDebugEnabled()) {
+                            log.debug("Invalidating parentEntry cache entry with key " + cacheKey);
+                        }
+                        for (final String variantURI : parentEntry.getVariantMap().values()) {
+                            removeEntry(storage, variantURI);
+                        }
+                        removeEntry(storage, cacheKey);
+                    }
+                    if (uri != null) {
+                        if (log.isWarnEnabled()) {
+                            log.warn(s + " is not a valid URI");
+                        }
+                        final Header clHdr = request.getFirstHeader("Content-Location");
+                        if (clHdr != null) {
+                            final URI contentLocation = HttpCacheSupport.normalizeQuetly(clHdr.getValue());
+                            if (contentLocation != null) {
+                                if (!flushAbsoluteUriFromSameHost(uri, contentLocation, cacheKeyResolver, storage)) {
+                                    flushRelativeUriFromSameHost(uri, contentLocation, cacheKeyResolver, storage);
+                                }
+                            }
+                        }
+                        final Header lHdr = request.getFirstHeader("Location");
+                        if (lHdr != null) {
+                            final URI location = HttpCacheSupport.normalizeQuetly(lHdr.getValue());
+                            if (location != null) {
+                                flushAbsoluteUriFromSameHost(uri, location, cacheKeyResolver, storage);
+                            }
+                        }
+                    }
+                }
+                callback.completed(Boolean.TRUE);
+            }
+
+            @Override
+            public void failed(final Exception ex) {
+                callback.failed(ex);
+            }
+
+            @Override
+            public void cancelled() {
+                callback.cancelled();
+            }
+
+        });
+
+    }
+
+    private void flushRelativeUriFromSameHost(
+            final URI requestUri,
+            final URI uri,
+            final Resolver<URI, String> cacheKeyResolver,
+            final HttpAsyncCacheStorage storage) {
+        final URI resolvedUri = uri != null ? URIUtils.resolve(requestUri, uri) : null;
+        if (resolvedUri != null && isSameHost(requestUri, resolvedUri)) {
+            removeEntry(storage, cacheKeyResolver.resolve(resolvedUri));
+        }
+    }
+
+    private boolean flushAbsoluteUriFromSameHost(
+            final URI requestUri,
+            final URI uri,
+            final Resolver<URI, String> cacheKeyResolver,
+            final HttpAsyncCacheStorage storage) {
+        if (uri != null && isSameHost(requestUri, uri)) {
+            removeEntry(storage, cacheKeyResolver.resolve(uri));
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public Cancellable flushInvalidatedCacheEntries(
+            final HttpHost host,
+            final HttpRequest request,
+            final HttpResponse response,
+            final Resolver<URI, String> cacheKeyResolver,
+            final HttpAsyncCacheStorage storage,
+            final FutureCallback<Boolean> callback) {
+        final int status = response.getCode();
+        if (status >= HttpStatus.SC_SUCCESS && status < HttpStatus.SC_REDIRECTION) {
+            final String s = HttpCacheSupport.getRequestUri(request, host);
+            final URI requestUri = HttpCacheSupport.normalizeQuetly(s);
+            if (requestUri != null) {
+                final List<String> cacheKeys = new ArrayList<>(2);
+                final URI contentLocation = getContentLocationURI(requestUri, response);
+                if (contentLocation != null && isSameHost(requestUri, contentLocation)) {
+                    cacheKeys.add(cacheKeyResolver.resolve(contentLocation));
+                }
+                final URI location = getLocationURI(requestUri, response);
+                if (location != null && isSameHost(requestUri, location)) {
+                    cacheKeys.add(cacheKeyResolver.resolve(location));
+                }
+                if (cacheKeys.size() == 1) {
+                    final String key = cacheKeys.get(0);
+                    storage.getEntry(key, new FutureCallback<HttpCacheEntry>() {
+
+                        @Override
+                        public void completed(final HttpCacheEntry entry) {
+                            if (entry != null) {
+                                // do not invalidate if response is strictly older than entry
+                                // or if the etags match
+                                if (!responseDateOlderThanEntryDate(response, entry) && responseAndEntryEtagsDiffer(response, entry)) {
+                                    removeEntry(storage, key);
+                                }
+                            }
+                            callback.completed(Boolean.TRUE);
+                        }
+
+                        @Override
+                        public void failed(final Exception ex) {
+                            callback.failed(ex);
+                        }
+
+                        @Override
+                        public void cancelled() {
+                            callback.cancelled();
+                        }
+
+                    });
+                } else if (cacheKeys.size() > 1) {
+                    storage.getEntries(cacheKeys, new FutureCallback<Map<String, HttpCacheEntry>>() {
+
+                        @Override
+                        public void completed(final Map<String, HttpCacheEntry> resultMap) {
+                            for (final Map.Entry<String, HttpCacheEntry> resultEntry: resultMap.entrySet()) {
+                                // do not invalidate if response is strictly older than entry
+                                // or if the etags match
+                                final String key = resultEntry.getKey();
+                                final HttpCacheEntry entry = resultEntry.getValue();
+                                if (!responseDateOlderThanEntryDate(response, entry) && responseAndEntryEtagsDiffer(response, entry)) {
+                                    removeEntry(storage, key);
+                                }
+                            }
+                            callback.completed(Boolean.TRUE);
+                        }
+
+                        @Override
+                        public void failed(final Exception ex) {
+                            callback.failed(ex);
+                        }
+
+                        @Override
+                        public void cancelled() {
+                            callback.cancelled();
+                        }
+
+                    });
+                }
+            }
+        }
+        callback.completed(Boolean.TRUE);
+        return Operations.nonCancellable();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/3f52d0bf/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/DefaultCacheInvalidator.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/DefaultCacheInvalidator.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/DefaultCacheInvalidator.java
index 029928b..76ac99e 100644
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/DefaultCacheInvalidator.java
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/DefaultCacheInvalidator.java
@@ -28,19 +28,16 @@ package org.apache.hc.client5.http.impl.cache;
 
 import java.net.URI;
 
-import org.apache.hc.client5.http.cache.HeaderConstants;
 import org.apache.hc.client5.http.cache.HttpCacheEntry;
 import org.apache.hc.client5.http.cache.HttpCacheInvalidator;
 import org.apache.hc.client5.http.cache.HttpCacheStorage;
 import org.apache.hc.client5.http.cache.ResourceIOException;
-import org.apache.hc.client5.http.utils.DateUtils;
 import org.apache.hc.client5.http.utils.URIUtils;
 import org.apache.hc.core5.annotation.Contract;
 import org.apache.hc.core5.annotation.Internal;
 import org.apache.hc.core5.annotation.ThreadingBehavior;
 import org.apache.hc.core5.function.Resolver;
 import org.apache.hc.core5.http.Header;
-import org.apache.hc.core5.http.HttpHeaders;
 import org.apache.hc.core5.http.HttpHost;
 import org.apache.hc.core5.http.HttpRequest;
 import org.apache.hc.core5.http.HttpResponse;
@@ -55,7 +52,7 @@ import org.apache.logging.log4j.Logger;
  */
 @Contract(threading = ThreadingBehavior.IMMUTABLE)
 @Internal
-public class DefaultCacheInvalidator implements HttpCacheInvalidator {
+public class DefaultCacheInvalidator extends CacheInvalidatorBase implements HttpCacheInvalidator {
 
     public static final DefaultCacheInvalidator INSTANCE = new DefaultCacheInvalidator();
 
@@ -82,13 +79,6 @@ public class DefaultCacheInvalidator implements HttpCacheInvalidator {
         }
     }
 
-    /**
-     * Remove cache entries from the cache that are no longer fresh or
-     * have been invalidated in some way.
-     *
-     * @param host The backend host we are talking to
-     * @param request The HttpRequest to that host
-     */
     @Override
     public void flushInvalidatedCacheEntries(
             final HttpHost host,
@@ -134,66 +124,30 @@ public class DefaultCacheInvalidator implements HttpCacheInvalidator {
         }
     }
 
-    private boolean shouldInvalidateHeadCacheEntry(final HttpRequest req, final HttpCacheEntry parentCacheEntry) {
-        return requestIsGet(req) && isAHeadCacheEntry(parentCacheEntry);
-    }
-
-    private boolean requestIsGet(final HttpRequest req) {
-        return req.getMethod().equals((HeaderConstants.GET_METHOD));
-    }
-
-    private boolean isAHeadCacheEntry(final HttpCacheEntry parentCacheEntry) {
-        return parentCacheEntry != null && parentCacheEntry.getRequestMethod().equals(HeaderConstants.HEAD_METHOD);
-    }
-
-    private void flushUriIfSameHost(
-            final URI requestURI,
-            final URI targetURI,
-            final Resolver<URI, String> cacheKeyResolver,
-            final HttpCacheStorage storage) {
-        if (targetURI.isAbsolute() && targetURI.getAuthority().equalsIgnoreCase(requestURI.getAuthority())) {
-            removeEntry(storage, cacheKeyResolver.resolve(targetURI));
-        }
-    }
-
     private void flushRelativeUriFromSameHost(
             final URI requestUri,
             final URI uri,
             final Resolver<URI, String> cacheKeyResolver,
             final HttpCacheStorage storage) {
         final URI resolvedUri = uri != null ? URIUtils.resolve(requestUri, uri) : null;
-        if (resolvedUri != null) {
-            flushUriIfSameHost(requestUri, resolvedUri, cacheKeyResolver, storage);
+        if (resolvedUri != null && isSameHost(requestUri, resolvedUri)) {
+            removeEntry(storage, cacheKeyResolver.resolve(resolvedUri));
         }
     }
 
-
     private boolean flushAbsoluteUriFromSameHost(
             final URI requestUri,
             final URI uri,
             final Resolver<URI, String> cacheKeyResolver,
             final HttpCacheStorage storage) {
-        if (uri != null && uri.isAbsolute()) {
-            flushUriIfSameHost(requestUri, uri, cacheKeyResolver, storage);
+        if (uri != null && isSameHost(requestUri, uri)) {
+            removeEntry(storage, cacheKeyResolver.resolve(uri));
             return true;
         } else {
             return false;
         }
     }
 
-    private boolean requestShouldNotBeCached(final HttpRequest req) {
-        final String method = req.getMethod();
-        return notGetOrHeadRequest(method);
-    }
-
-    private boolean notGetOrHeadRequest(final String method) {
-        return !(HeaderConstants.GET_METHOD.equals(method) || HeaderConstants.HEAD_METHOD
-                .equals(method));
-    }
-
-    /** Flushes entries that were invalidated by the given response
-     * received for the given host/request pair.
-     */
     @Override
     public void flushInvalidatedCacheEntries(
             final HttpHost host,
@@ -211,75 +165,30 @@ public class DefaultCacheInvalidator implements HttpCacheInvalidator {
             return;
         }
         final URI contentLocation = getContentLocationURI(uri, response);
-        if (contentLocation != null) {
-            flushLocationCacheEntry(uri, response, contentLocation, cacheKeyResolver, storage);
+        if (contentLocation != null && isSameHost(uri, contentLocation)) {
+            flushLocationCacheEntry(response, contentLocation, storage, cacheKeyResolver);
         }
         final URI location = getLocationURI(uri, response);
-        if (location != null) {
-            flushLocationCacheEntry(uri, response, location, cacheKeyResolver, storage);
+        if (location != null && isSameHost(uri, location)) {
+            flushLocationCacheEntry(response, location, storage, cacheKeyResolver);
         }
     }
 
     private void flushLocationCacheEntry(
-            final URI requestUri,
             final HttpResponse response,
             final URI location,
-            final Resolver<URI, String> cacheKeyResolver,
-            final HttpCacheStorage storage) {
+            final HttpCacheStorage storage,
+            final Resolver<URI, String> cacheKeyResolver) {
         final String cacheKey = cacheKeyResolver.resolve(location);
         final HttpCacheEntry entry = getEntry(storage, cacheKey);
-        if (entry == null) {
-            return;
-        }
-
-        // do not invalidate if response is strictly older than entry
-        // or if the etags match
+        if (entry != null) {
+            // do not invalidate if response is strictly older than entry
+            // or if the etags match
 
-        if (responseDateOlderThanEntryDate(response, entry)) {
-            return;
-        }
-        if (!responseAndEntryEtagsDiffer(response, entry)) {
-            return;
-        }
-
-        flushUriIfSameHost(requestUri, location, cacheKeyResolver, storage);
-    }
-
-    private static URI getLocationURI(final URI requestUri, final HttpResponse response, final String headerName) {
-        final Header h = response.getFirstHeader(headerName);
-        if (h == null) {
-            return null;
-        }
-        final URI locationUri = HttpCacheSupport.normalizeQuetly(h.getValue());
-        if (locationUri == null) {
-            return requestUri;
-        }
-        if (locationUri.isAbsolute()) {
-            return locationUri;
-        } else {
-            return URIUtils.resolve(requestUri, locationUri);
-        }
-    }
-
-    private URI getContentLocationURI(final URI requestUri, final HttpResponse response) {
-        return getLocationURI(requestUri, response, HttpHeaders.CONTENT_LOCATION);
-    }
-
-    private URI getLocationURI(final URI requestUri, final HttpResponse response) {
-        return getLocationURI(requestUri, response, HttpHeaders.LOCATION);
-    }
-
-    private boolean responseAndEntryEtagsDiffer(final HttpResponse response,
-            final HttpCacheEntry entry) {
-        final Header entryEtag = entry.getFirstHeader(HeaderConstants.ETAG);
-        final Header responseEtag = response.getFirstHeader(HeaderConstants.ETAG);
-        if (entryEtag == null || responseEtag == null) {
-            return false;
+            if (!responseDateOlderThanEntryDate(response, entry) && responseAndEntryEtagsDiffer(response, entry)) {
+                removeEntry(storage, cacheKey);
+            }
         }
-        return (!entryEtag.getValue().equals(responseEtag.getValue()));
     }
 
-    private boolean responseDateOlderThanEntryDate(final HttpResponse response, final HttpCacheEntry entry) {
-        return DateUtils.isBefore(response, entry, HttpHeaders.DATE);
-    }
 }

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/3f52d0bf/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestDefaultAsyncCacheInvalidator.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestDefaultAsyncCacheInvalidator.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestDefaultAsyncCacheInvalidator.java
new file mode 100644
index 0000000..d64018d
--- /dev/null
+++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestDefaultAsyncCacheInvalidator.java
@@ -0,0 +1,696 @@
+/*
+ * ====================================================================
+ * 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
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.client5.http.impl.cache;
+
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import java.net.URI;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.hc.client5.http.cache.HttpAsyncCacheStorage;
+import org.apache.hc.client5.http.cache.HttpCacheEntry;
+import org.apache.hc.client5.http.utils.DateUtils;
+import org.apache.hc.core5.concurrent.Cancellable;
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.function.Resolver;
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.HttpRequest;
+import org.apache.hc.core5.http.HttpResponse;
+import org.apache.hc.core5.http.HttpStatus;
+import org.apache.hc.core5.http.message.BasicHeader;
+import org.apache.hc.core5.http.message.BasicHttpRequest;
+import org.apache.hc.core5.http.message.BasicHttpResponse;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
+
+@RunWith(MockitoJUnitRunner.class)
+public class TestDefaultAsyncCacheInvalidator {
+
+    private DefaultAsyncCacheInvalidator impl;
+    private HttpHost host;
+    @Mock
+    private HttpCacheEntry mockEntry;
+    @Mock
+    private Resolver<URI, String> cacheKeyResolver;
+    @Mock
+    private HttpAsyncCacheStorage mockStorage;
+    @Mock
+    private FutureCallback<Boolean> operationCallback;
+    @Mock
+    private Cancellable cancellable;
+
+    private Date now;
+    private Date tenSecondsAgo;
+
+    @Before
+    public void setUp() {
+        now = new Date();
+        tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
+
+        when(cacheKeyResolver.resolve(Mockito.<URI>any())).thenAnswer(new Answer<String>() {
+
+            @Override
+            public String answer(final InvocationOnMock invocation) throws Throwable {
+                final URI uri = invocation.getArgument(0);
+                return HttpCacheSupport.normalize(uri).toASCIIString();
+            }
+
+        });
+
+        host = new HttpHost("foo.example.com");
+        impl = new DefaultAsyncCacheInvalidator();
+    }
+
+    // Tests
+    @Test
+    public void testInvalidatesRequestsThatArentGETorHEAD() throws Exception {
+        final HttpRequest request = new BasicHttpRequest("POST","/path");
+        final String key = "http://foo.example.com:80/path";
+
+        final Map<String,String> variantMap = new HashMap<>();
+        cacheEntryHasVariantMap(variantMap);
+        cacheReturnsEntryForUri(key, mockEntry);
+
+        impl.flushInvalidatedCacheEntries(host, request, cacheKeyResolver, mockStorage, operationCallback);
+
+        verify(mockEntry).getVariantMap();
+        verify(mockStorage).getEntry(Mockito.eq(key), Mockito.<FutureCallback<HttpCacheEntry>>any());
+        verify(mockStorage).removeEntry(Mockito.eq(key), Mockito.<FutureCallback<Boolean>>any());
+    }
+
+    @Test
+    public void testInvalidatesUrisInContentLocationHeadersOnPUTs() throws Exception {
+        final HttpRequest request = new BasicHttpRequest("PUT","/");
+        request.setHeader("Content-Length","128");
+
+        final String contentLocation = "http://foo.example.com/content";
+        request.setHeader("Content-Location", contentLocation);
+
+        final URI uri = new URI("http://foo.example.com:80/");
+        final String key = uri.toASCIIString();
+        cacheEntryHasVariantMap(new HashMap<String,String>());
+
+        cacheReturnsEntryForUri(key, mockEntry);
+
+        impl.flushInvalidatedCacheEntries(host, request, cacheKeyResolver, mockStorage, operationCallback);
+
+        verify(mockEntry).getVariantMap();
+        verify(mockStorage).getEntry(Mockito.eq(key), Mockito.<FutureCallback<HttpCacheEntry>>any());
+        verify(mockStorage).removeEntry(Mockito.eq(key), Mockito.<FutureCallback<Boolean>>any());
+        verify(mockStorage).removeEntry(Mockito.eq("http://foo.example.com:80/content"), Mockito.<FutureCallback<Boolean>>any());
+    }
+
+    @Test
+    public void testInvalidatesUrisInLocationHeadersOnPUTs() throws Exception {
+        final HttpRequest request = new BasicHttpRequest("PUT","/");
+        request.setHeader("Content-Length","128");
+
+        final String contentLocation = "http://foo.example.com/content";
+        request.setHeader("Location",contentLocation);
+
+        final URI uri = new URI("http://foo.example.com:80/");
+        final String key = uri.toASCIIString();
+        cacheEntryHasVariantMap(new HashMap<String,String>());
+
+        cacheReturnsEntryForUri(key, mockEntry);
+
+        impl.flushInvalidatedCacheEntries(host, request, cacheKeyResolver, mockStorage, operationCallback);
+
+        verify(mockEntry).getVariantMap();
+        verify(mockStorage).getEntry(Mockito.eq(key), Mockito.<FutureCallback<HttpCacheEntry>>any());
+        verify(mockStorage).removeEntry(Mockito.eq(key), Mockito.<FutureCallback<Boolean>>any());
+        verify(mockStorage).removeEntry(Mockito.eq("http://foo.example.com:80/content"), Mockito.<FutureCallback<Boolean>>any());
+    }
+
+    @Test
+    public void testInvalidatesRelativeUrisInContentLocationHeadersOnPUTs() throws Exception {
+        final HttpRequest request = new BasicHttpRequest("PUT","/");
+        request.setHeader("Content-Length","128");
+
+        final String relativePath = "/content";
+        request.setHeader("Content-Location",relativePath);
+
+        final URI uri = new URI("http://foo.example.com:80/");
+        final String key = uri.toASCIIString();
+        cacheEntryHasVariantMap(new HashMap<String,String>());
+
+        cacheReturnsEntryForUri(key, mockEntry);
+
+        impl.flushInvalidatedCacheEntries(host, request, cacheKeyResolver, mockStorage, operationCallback);
+
+        verify(mockEntry).getVariantMap();
+        verify(mockStorage).getEntry(Mockito.eq(key), Mockito.<FutureCallback<HttpCacheEntry>>any());
+        verify(mockStorage).removeEntry(Mockito.eq(key), Mockito.<FutureCallback<Boolean>>any());
+        verify(mockStorage).removeEntry(Mockito.eq("http://foo.example.com:80/content"), Mockito.<FutureCallback<Boolean>>any());
+    }
+
+    @Test
+    public void testDoesNotInvalidateUrisInContentLocationHeadersOnPUTsToDifferentHosts() throws Exception {
+        final HttpRequest request = new BasicHttpRequest("PUT","/");
+        request.setHeader("Content-Length","128");
+
+        final String contentLocation = "http://bar.example.com/content";
+        request.setHeader("Content-Location",contentLocation);
+
+        final URI uri = new URI("http://foo.example.com:80/");
+        final String key = uri.toASCIIString();
+        cacheEntryHasVariantMap(new HashMap<String,String>());
+
+        cacheReturnsEntryForUri(key, mockEntry);
+
+        impl.flushInvalidatedCacheEntries(host, request, cacheKeyResolver, mockStorage, operationCallback);
+
+        verify(mockEntry).getVariantMap();
+        verify(mockStorage).getEntry(Mockito.eq(key), Mockito.<FutureCallback<HttpCacheEntry>>any());
+        verify(mockStorage).removeEntry(Mockito.eq(key), Mockito.<FutureCallback<Boolean>>any());
+    }
+
+    @Test
+    public void testDoesNotInvalidateGETRequest() throws Exception {
+        final HttpRequest request = new BasicHttpRequest("GET","/");
+        impl.flushInvalidatedCacheEntries(host, request, cacheKeyResolver, mockStorage, operationCallback);
+
+        verify(mockStorage).getEntry(Mockito.eq("http://foo.example.com:80/"), Mockito.<FutureCallback<HttpCacheEntry>>any());
+        verifyNoMoreInteractions(mockStorage);
+    }
+
+    @Test
+    public void testDoesNotInvalidateHEADRequest() throws Exception {
+        final HttpRequest request = new BasicHttpRequest("HEAD","/");
+        impl.flushInvalidatedCacheEntries(host, request, cacheKeyResolver, mockStorage, operationCallback);
+
+        verify(mockStorage).getEntry(Mockito.eq("http://foo.example.com:80/"), Mockito.<FutureCallback<HttpCacheEntry>>any());
+        verifyNoMoreInteractions(mockStorage);
+    }
+
+    @Test
+    public void testInvalidatesHEADCacheEntryIfSubsequentGETRequestsAreMadeToTheSameURI() throws Exception {
+        final URI uri = new URI("http://foo.example.com:80/");
+        final String key = uri.toASCIIString();
+        final HttpRequest request = new BasicHttpRequest("GET", uri);
+
+        cacheEntryisForMethod("HEAD");
+        cacheEntryHasVariantMap(new HashMap<String, String>());
+        cacheReturnsEntryForUri(key, mockEntry);
+
+        impl.flushInvalidatedCacheEntries(host, request, cacheKeyResolver, mockStorage, operationCallback);
+
+        verify(mockEntry).getRequestMethod();
+        verify(mockEntry).getVariantMap();
+        verify(mockStorage).getEntry(Mockito.eq(key), Mockito.<FutureCallback<HttpCacheEntry>>any());
+        verify(mockStorage).removeEntry(Mockito.eq(key), Mockito.<FutureCallback<Boolean>>any());
+    }
+
+    @Test
+    public void testInvalidatesVariantHEADCacheEntriesIfSubsequentGETRequestsAreMadeToTheSameURI() throws Exception {
+        final URI uri = new URI("http://foo.example.com:80/");
+        final String key = uri.toASCIIString();
+        final HttpRequest request = new BasicHttpRequest("GET", uri);
+        final String theVariantKey = "{Accept-Encoding=gzip%2Cdeflate&User-Agent=Apache-HttpClient}";
+        final String theVariantURI = "{Accept-Encoding=gzip%2Cdeflate&User-Agent=Apache-HttpClient}http://foo.example.com:80/";
+        final Map<String, String> variants = HttpTestUtils.makeDefaultVariantMap(theVariantKey, theVariantURI);
+
+        cacheEntryisForMethod("HEAD");
+        cacheEntryHasVariantMap(variants);
+        cacheReturnsEntryForUri(key, mockEntry);
+
+        impl.flushInvalidatedCacheEntries(host, request, cacheKeyResolver, mockStorage, operationCallback);
+
+        verify(mockEntry).getRequestMethod();
+        verify(mockEntry).getVariantMap();
+        verify(mockStorage).getEntry(Mockito.eq(key), Mockito.<FutureCallback<HttpCacheEntry>>any());
+        verify(mockStorage).removeEntry(Mockito.eq(key), Mockito.<FutureCallback<Boolean>>any());
+        verify(mockStorage).removeEntry(Mockito.eq(theVariantURI), Mockito.<FutureCallback<Boolean>>any());
+    }
+
+    @Test
+    public void testDoesNotInvalidateHEADCacheEntry() throws Exception {
+        final URI uri = new URI("http://foo.example.com:80/");
+        final String key = uri.toASCIIString();
+        final HttpRequest request = new BasicHttpRequest("HEAD", uri);
+
+        cacheReturnsEntryForUri(key, mockEntry);
+
+        impl.flushInvalidatedCacheEntries(host, request, cacheKeyResolver, mockStorage, operationCallback);
+
+        verify(mockStorage).getEntry(Mockito.eq(key), Mockito.<FutureCallback<HttpCacheEntry>>any());
+        verifyNoMoreInteractions(mockStorage);
+    }
+
+    @Test
+    public void testDoesNotInvalidateHEADCacheEntryIfSubsequentHEADRequestsAreMadeToTheSameURI() throws Exception {
+        final URI uri = new URI("http://foo.example.com:80/");
+        final String key = uri.toASCIIString();
+        final HttpRequest request = new BasicHttpRequest("HEAD", uri);
+
+        cacheReturnsEntryForUri(key, mockEntry);
+
+        impl.flushInvalidatedCacheEntries(host, request, cacheKeyResolver, mockStorage, operationCallback);
+
+        verify(mockStorage).getEntry(Mockito.eq(key), Mockito.<FutureCallback<HttpCacheEntry>>any());
+        verifyNoMoreInteractions(mockStorage);
+    }
+
+    @Test
+    public void testDoesNotInvalidateGETCacheEntryIfSubsequentGETRequestsAreMadeToTheSameURI() throws Exception {
+        final URI uri = new URI("http://foo.example.com:80/");
+        final String key = uri.toASCIIString();
+        final HttpRequest request = new BasicHttpRequest("GET", uri);
+
+        cacheEntryisForMethod("GET");
+        cacheReturnsEntryForUri(key, mockEntry);
+
+        impl.flushInvalidatedCacheEntries(host, request, cacheKeyResolver, mockStorage, operationCallback);
+
+        verify(mockEntry).getRequestMethod();
+        verify(mockStorage).getEntry(Mockito.eq(key), Mockito.<FutureCallback<HttpCacheEntry>>any());
+        verifyNoMoreInteractions(mockStorage);
+    }
+
+    @Test
+    public void testDoesNotInvalidateRequestsWithClientCacheControlHeaders() throws Exception {
+        final HttpRequest request = new BasicHttpRequest("GET","/");
+        request.setHeader("Cache-Control","no-cache");
+
+        impl.flushInvalidatedCacheEntries(host, request, cacheKeyResolver, mockStorage, operationCallback);
+
+        verify(mockStorage).getEntry(Mockito.eq("http://foo.example.com:80/"), Mockito.<FutureCallback<HttpCacheEntry>>any());
+        verifyNoMoreInteractions(mockStorage);
+    }
+
+    @Test
+    public void testDoesNotInvalidateRequestsWithClientPragmaHeaders() throws Exception {
+        final HttpRequest request = new BasicHttpRequest("GET","/");
+        request.setHeader("Pragma","no-cache");
+
+        impl.flushInvalidatedCacheEntries(host, request, cacheKeyResolver, mockStorage, operationCallback);
+
+        verify(mockStorage).getEntry(Mockito.eq("http://foo.example.com:80/"), Mockito.<FutureCallback<HttpCacheEntry>>any());
+        verifyNoMoreInteractions(mockStorage);
+    }
+
+    @Test
+    public void testVariantURIsAreFlushedAlso() throws Exception {
+        final HttpRequest request = new BasicHttpRequest("POST","/");
+        final URI uri = new URI("http://foo.example.com:80/");
+        final String key = uri.toASCIIString();
+        final String variantUri = "theVariantURI";
+        final Map<String,String> mapOfURIs = HttpTestUtils.makeDefaultVariantMap(variantUri, variantUri);
+
+        cacheReturnsEntryForUri(key, mockEntry);
+        cacheEntryHasVariantMap(mapOfURIs);
+
+        impl.flushInvalidatedCacheEntries(host, request, cacheKeyResolver, mockStorage, operationCallback);
+
+        verify(mockStorage).getEntry(Mockito.eq(key), Mockito.<FutureCallback<HttpCacheEntry>>any());
+        verify(mockEntry).getVariantMap();
+        verify(mockStorage).removeEntry(Mockito.eq(key), Mockito.<FutureCallback<Boolean>>any());
+        verify(mockStorage).removeEntry(Mockito.eq(variantUri), Mockito.<FutureCallback<Boolean>>any());
+    }
+
+    @Test
+    public void doesNotFlushForResponsesWithoutContentLocation() throws Exception {
+        final HttpRequest request = new BasicHttpRequest("POST","/");
+        final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_OK);
+        impl.flushInvalidatedCacheEntries(host, request, response, cacheKeyResolver, mockStorage, operationCallback);
+
+        verifyNoMoreInteractions(mockStorage);
+    }
+
+    @Test
+    public void flushesEntryIfFresherAndSpecifiedByContentLocation() throws Exception {
+        final HttpRequest request = new BasicHttpRequest("GET", "/");
+        final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_OK);
+        response.setHeader("ETag","\"new-etag\"");
+        response.setHeader("Date", DateUtils.formatDate(now));
+        final String key = "http://foo.example.com:80/bar";
+        response.setHeader("Content-Location", key);
+
+        final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(new Header[] {
+           new BasicHeader("Date", DateUtils.formatDate(tenSecondsAgo)),
+           new BasicHeader("ETag", "\"old-etag\"")
+        });
+
+        cacheReturnsEntryForUri(key, entry);
+
+        impl.flushInvalidatedCacheEntries(host, request, response, cacheKeyResolver, mockStorage, operationCallback);
+
+        verify(mockStorage).getEntry(Mockito.eq(key), Mockito.<FutureCallback<HttpCacheEntry>>any());
+        verify(mockStorage).removeEntry(Mockito.eq(key), Mockito.<FutureCallback<Boolean>>any());
+    }
+
+    @Test
+    public void flushesEntryIfFresherAndSpecifiedByLocation() throws Exception {
+        final HttpRequest request = new BasicHttpRequest("GET", "/");
+        final HttpResponse response = new BasicHttpResponse(201);
+        response.setHeader("ETag","\"new-etag\"");
+        response.setHeader("Date", DateUtils.formatDate(now));
+        final String key = "http://foo.example.com:80/bar";
+        response.setHeader("Location", key);
+
+        final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(new Header[] {
+           new BasicHeader("Date", DateUtils.formatDate(tenSecondsAgo)),
+           new BasicHeader("ETag", "\"old-etag\"")
+        });
+
+        cacheReturnsEntryForUri(key, entry);
+
+        impl.flushInvalidatedCacheEntries(host, request, response, cacheKeyResolver, mockStorage, operationCallback);
+
+        verify(mockStorage).getEntry(Mockito.eq(key), Mockito.<FutureCallback<HttpCacheEntry>>any());
+        verify(mockStorage).removeEntry(Mockito.eq(key), Mockito.<FutureCallback<Boolean>>any());
+    }
+
+    @Test
+    public void doesNotFlushEntryForUnsuccessfulResponse() throws Exception {
+        final HttpRequest request = new BasicHttpRequest("GET", "/");
+        final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_BAD_REQUEST, "Bad Request");
+        response.setHeader("ETag","\"new-etag\"");
+        response.setHeader("Date", DateUtils.formatDate(now));
+        final String key = "http://foo.example.com:80/bar";
+        response.setHeader("Content-Location", key);
+
+        impl.flushInvalidatedCacheEntries(host, request, response, cacheKeyResolver, mockStorage, operationCallback);
+
+        verifyNoMoreInteractions(mockStorage);
+    }
+
+    @Test
+    public void flushesEntryIfFresherAndSpecifiedByNonCanonicalContentLocation() throws Exception {
+        final HttpRequest request = new BasicHttpRequest("GET", "/");
+        final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_OK);
+        response.setHeader("ETag","\"new-etag\"");
+        response.setHeader("Date", DateUtils.formatDate(now));
+        final String key = "http://foo.example.com:80/bar";
+        response.setHeader("Content-Location", "http://foo.example.com/bar");
+
+        final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(new Header[] {
+           new BasicHeader("Date", DateUtils.formatDate(tenSecondsAgo)),
+           new BasicHeader("ETag", "\"old-etag\"")
+        });
+
+        cacheReturnsEntryForUri(key, entry);
+
+        impl.flushInvalidatedCacheEntries(host, request, response, cacheKeyResolver, mockStorage, operationCallback);
+
+        verify(mockStorage).getEntry(Mockito.eq(key), Mockito.<FutureCallback<HttpCacheEntry>>any());
+        verify(mockStorage).removeEntry(Mockito.eq(key), Mockito.<FutureCallback<Boolean>>any());
+    }
+
+    @Test
+    public void flushesEntryIfFresherAndSpecifiedByRelativeContentLocation() throws Exception {
+        final HttpRequest request = new BasicHttpRequest("GET", "/");
+        final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_OK);
+        response.setHeader("ETag","\"new-etag\"");
+        response.setHeader("Date", DateUtils.formatDate(now));
+        final String key = "http://foo.example.com:80/bar";
+        response.setHeader("Content-Location", "/bar");
+
+        final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(new Header[] {
+           new BasicHeader("Date", DateUtils.formatDate(tenSecondsAgo)),
+           new BasicHeader("ETag", "\"old-etag\"")
+        });
+
+        cacheReturnsEntryForUri(key, entry);
+
+        impl.flushInvalidatedCacheEntries(host, request, response, cacheKeyResolver, mockStorage, operationCallback);
+
+        verify(mockStorage).getEntry(Mockito.eq(key), Mockito.<FutureCallback<HttpCacheEntry>>any());
+        verify(mockStorage).removeEntry(Mockito.eq(key), Mockito.<FutureCallback<Boolean>>any());
+    }
+
+    @Test
+    public void doesNotFlushEntryIfContentLocationFromDifferentHost() throws Exception {
+        final HttpRequest request = new BasicHttpRequest("GET", "/");
+        final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_OK);
+        response.setHeader("ETag","\"new-etag\"");
+        response.setHeader("Date", DateUtils.formatDate(now));
+        final String key = "http://baz.example.com:80/bar";
+        response.setHeader("Content-Location", key);
+
+        final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(new Header[] {
+           new BasicHeader("Date", DateUtils.formatDate(tenSecondsAgo)),
+           new BasicHeader("ETag", "\"old-etag\"")
+        });
+
+        cacheReturnsEntryForUri(key, entry);
+
+        impl.flushInvalidatedCacheEntries(host, request, response, cacheKeyResolver, mockStorage, operationCallback);
+
+        verifyNoMoreInteractions(mockStorage);
+    }
+
+    @Test
+    public void doesNotFlushEntrySpecifiedByContentLocationIfEtagsMatch() throws Exception {
+        final HttpRequest request = new BasicHttpRequest("GET", "/");
+        final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_OK);
+        response.setHeader("ETag","\"same-etag\"");
+        response.setHeader("Date", DateUtils.formatDate(now));
+        final String key = "http://foo.example.com:80/bar";
+        response.setHeader("Content-Location", key);
+
+        final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(new Header[] {
+           new BasicHeader("Date", DateUtils.formatDate(tenSecondsAgo)),
+           new BasicHeader("ETag", "\"same-etag\"")
+        });
+
+        cacheReturnsEntryForUri(key, entry);
+        impl.flushInvalidatedCacheEntries(host, request, response, cacheKeyResolver, mockStorage, operationCallback);
+
+        verify(mockStorage).getEntry(Mockito.eq(key), Mockito.<FutureCallback<HttpCacheEntry>>any());
+        verifyNoMoreInteractions(mockStorage);
+    }
+
+    @Test
+    public void doesNotFlushEntrySpecifiedByContentLocationIfOlder() throws Exception {
+        final HttpRequest request = new BasicHttpRequest("GET", "/");
+        final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_OK);
+        response.setHeader("ETag","\"new-etag\"");
+        response.setHeader("Date", DateUtils.formatDate(tenSecondsAgo));
+        final String key = "http://foo.example.com:80/bar";
+        response.setHeader("Content-Location", key);
+
+        final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(new Header[] {
+           new BasicHeader("Date", DateUtils.formatDate(now)),
+           new BasicHeader("ETag", "\"old-etag\"")
+        });
+
+        cacheReturnsEntryForUri(key, entry);
+
+        impl.flushInvalidatedCacheEntries(host, request, response, cacheKeyResolver, mockStorage, operationCallback);
+
+        verify(mockStorage).getEntry(Mockito.eq(key), Mockito.<FutureCallback<HttpCacheEntry>>any());
+        verifyNoMoreInteractions(mockStorage);
+    }
+
+    @Test
+    public void doesNotFlushEntryIfNotInCache() throws Exception {
+        final HttpRequest request = new BasicHttpRequest("GET", "/");
+        final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_OK);
+        response.setHeader("ETag","\"new-etag\"");
+        response.setHeader("Date", DateUtils.formatDate(now));
+        final String key = "http://foo.example.com:80/bar";
+        response.setHeader("Content-Location", key);
+
+        cacheReturnsEntryForUri(key, null);
+
+        impl.flushInvalidatedCacheEntries(host, request, response, cacheKeyResolver, mockStorage, operationCallback);
+
+        verify(mockStorage).getEntry(Mockito.eq(key), Mockito.<FutureCallback<HttpCacheEntry>>any());
+        verifyNoMoreInteractions(mockStorage);
+    }
+
+    @Test
+    public void doesNotFlushEntrySpecifiedByContentLocationIfResponseHasNoEtag() throws Exception {
+        final HttpRequest request = new BasicHttpRequest("GET", "/");
+        final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_OK);
+        response.removeHeaders("ETag");
+        response.setHeader("Date", DateUtils.formatDate(now));
+        final String key = "http://foo.example.com:80/bar";
+        response.setHeader("Content-Location", key);
+
+        final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(new Header[] {
+           new BasicHeader("Date", DateUtils.formatDate(tenSecondsAgo)),
+           new BasicHeader("ETag", "\"old-etag\"")
+        });
+
+        cacheReturnsEntryForUri(key, entry);
+
+        impl.flushInvalidatedCacheEntries(host, request, response, cacheKeyResolver, mockStorage, operationCallback);
+
+        verify(mockStorage).getEntry(Mockito.eq(key), Mockito.<FutureCallback<HttpCacheEntry>>any());
+        verifyNoMoreInteractions(mockStorage);
+    }
+
+    @Test
+    public void doesNotFlushEntrySpecifiedByContentLocationIfEntryHasNoEtag() throws Exception {
+        final HttpRequest request = new BasicHttpRequest("GET", "/");
+        final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_OK);
+        response.setHeader("ETag", "\"some-etag\"");
+        response.setHeader("Date", DateUtils.formatDate(now));
+        final String key = "http://foo.example.com:80/bar";
+        response.setHeader("Content-Location", key);
+
+        final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(new Header[] {
+           new BasicHeader("Date", DateUtils.formatDate(tenSecondsAgo)),
+        });
+
+        cacheReturnsEntryForUri(key, entry);
+
+        impl.flushInvalidatedCacheEntries(host, request, response, cacheKeyResolver, mockStorage, operationCallback);
+
+        verify(mockStorage).getEntry(Mockito.eq(key), Mockito.<FutureCallback<HttpCacheEntry>>any());
+        verifyNoMoreInteractions(mockStorage);
+    }
+
+    @Test
+    public void flushesEntrySpecifiedByContentLocationIfResponseHasNoDate() throws Exception {
+        final HttpRequest request = new BasicHttpRequest("GET", "/");
+        final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_OK);
+        response.setHeader("ETag", "\"new-etag\"");
+        response.removeHeaders("Date");
+        final String key = "http://foo.example.com:80/bar";
+        response.setHeader("Content-Location", key);
+
+        final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(new Header[] {
+                new BasicHeader("ETag", "\"old-etag\""),
+                new BasicHeader("Date", DateUtils.formatDate(tenSecondsAgo)),
+        });
+
+        cacheReturnsEntryForUri(key, entry);
+
+        impl.flushInvalidatedCacheEntries(host, request, response, cacheKeyResolver, mockStorage, operationCallback);
+
+        verify(mockStorage).getEntry(Mockito.eq(key), Mockito.<FutureCallback<HttpCacheEntry>>any());
+        verify(mockStorage).removeEntry(Mockito.eq(key), Mockito.<FutureCallback<Boolean>>any());
+        verifyNoMoreInteractions(mockStorage);
+    }
+
+    @Test
+    public void flushesEntrySpecifiedByContentLocationIfEntryHasNoDate() throws Exception {
+        final HttpRequest request = new BasicHttpRequest("GET", "/");
+        final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_OK);
+        response.setHeader("ETag","\"new-etag\"");
+        response.setHeader("Date", DateUtils.formatDate(now));
+        final String key = "http://foo.example.com:80/bar";
+        response.setHeader("Content-Location", key);
+
+        final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(new Header[] {
+           new BasicHeader("ETag", "\"old-etag\"")
+        });
+
+        cacheReturnsEntryForUri(key, entry);
+
+        impl.flushInvalidatedCacheEntries(host, request, response, cacheKeyResolver, mockStorage, operationCallback);
+
+        verify(mockStorage).getEntry(Mockito.eq(key), Mockito.<FutureCallback<HttpCacheEntry>>any());
+        verify(mockStorage).removeEntry(Mockito.eq(key), Mockito.<FutureCallback<Boolean>>any());
+        verifyNoMoreInteractions(mockStorage);
+    }
+
+    @Test
+    public void flushesEntrySpecifiedByContentLocationIfResponseHasMalformedDate() throws Exception {
+        final HttpRequest request = new BasicHttpRequest("GET", "/");
+        final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_OK);
+        response.setHeader("ETag","\"new-etag\"");
+        response.setHeader("Date", "blarg");
+        final String key = "http://foo.example.com:80/bar";
+        response.setHeader("Content-Location", key);
+
+        final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(new Header[] {
+                new BasicHeader("ETag", "\"old-etag\""),
+                new BasicHeader("Date", DateUtils.formatDate(tenSecondsAgo))
+        });
+
+        cacheReturnsEntryForUri(key, entry);
+
+        impl.flushInvalidatedCacheEntries(host, request, response, cacheKeyResolver, mockStorage, operationCallback);
+
+        verify(mockStorage).getEntry(Mockito.eq(key), Mockito.<FutureCallback<HttpCacheEntry>>any());
+        verify(mockStorage).removeEntry(Mockito.eq(key), Mockito.<FutureCallback<Boolean>>any());
+        verifyNoMoreInteractions(mockStorage);
+    }
+
+    @Test
+    public void flushesEntrySpecifiedByContentLocationIfEntryHasMalformedDate() throws Exception {
+        final HttpRequest request = new BasicHttpRequest("GET", "/");
+        final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_OK);
+        response.setHeader("ETag","\"new-etag\"");
+        response.setHeader("Date", DateUtils.formatDate(now));
+        final String key = "http://foo.example.com:80/bar";
+        response.setHeader("Content-Location", key);
+
+        final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(new Header[] {
+                new BasicHeader("ETag", "\"old-etag\""),
+                new BasicHeader("Date", "foo")
+        });
+
+        cacheReturnsEntryForUri(key, entry);
+
+        impl.flushInvalidatedCacheEntries(host, request, response, cacheKeyResolver, mockStorage, operationCallback);
+
+        verify(mockStorage).getEntry(Mockito.eq(key), Mockito.<FutureCallback<HttpCacheEntry>>any());
+        verify(mockStorage).removeEntry(Mockito.eq(key), Mockito.<FutureCallback<Boolean>>any());
+        verifyNoMoreInteractions(mockStorage);
+    }
+
+
+    // Expectations
+    private void cacheEntryHasVariantMap(final Map<String,String> variantMap) {
+        when(mockEntry.getVariantMap()).thenReturn(variantMap);
+    }
+
+    private void cacheReturnsEntryForUri(final String key, final HttpCacheEntry cacheEntry) {
+        Mockito.when(mockStorage.getEntry(
+                Mockito.eq(key),
+                Mockito.<FutureCallback<HttpCacheEntry>>any())).thenAnswer(new Answer<Cancellable>() {
+
+            @Override
+            public Cancellable answer(final InvocationOnMock invocation) throws Throwable {
+                final FutureCallback<HttpCacheEntry> callback = invocation.getArgument(1);
+                callback.completed(cacheEntry);
+                return cancellable;
+            }
+
+        });
+    }
+
+    private void cacheEntryisForMethod(final String httpMethod) {
+        when(mockEntry.getRequestMethod()).thenReturn(httpMethod);
+    }
+}

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/3f52d0bf/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestDefaultCacheInvalidator.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestDefaultCacheInvalidator.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestDefaultCacheInvalidator.java
index 5c18134..31f9af2 100644
--- a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestDefaultCacheInvalidator.java
+++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestDefaultCacheInvalidator.java
@@ -150,7 +150,7 @@ public class TestDefaultCacheInvalidator {
         verify(mockEntry).getVariantMap();
         verify(mockStorage).getEntry(key);
         verify(mockStorage).removeEntry(key);
-        verify(mockStorage).removeEntry(cacheKeyResolver.resolve(new URI(contentLocation)));
+        verify(mockStorage).removeEntry("http://foo.example.com:80/content");
     }
 
     @Test
@@ -459,21 +459,11 @@ public class TestDefaultCacheInvalidator {
         final String cacheKey = "http://baz.example.com:80/bar";
         response.setHeader("Content-Location", cacheKey);
 
-        final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(new Header[] {
-           new BasicHeader("Date", DateUtils.formatDate(tenSecondsAgo)),
-           new BasicHeader("ETag", "\"old-etag\"")
-        });
-
-        when(mockStorage.getEntry(cacheKey)).thenReturn(entry);
-
         impl.flushInvalidatedCacheEntries(host, request, response, cacheKeyResolver, mockStorage);
 
-        verify(mockStorage).getEntry(cacheKey);
         verifyNoMoreInteractions(mockStorage);
     }
 
-
-
     @Test
     public void doesNotFlushEntrySpecifiedByContentLocationIfEtagsMatch() throws Exception {
         final HttpRequest request = new BasicHttpRequest("GET", "/");