From 3f52d0bf90c4d9ec09cf8e8a3d583cea05f94b0e Mon Sep 17 00:00:00 2001 From: Oleg Kalnichevski Date: Sun, 17 Dec 2017 14:34:20 +0100 Subject: [PATCH] HTTPCLIENT-1824: asynchronous HTTP cache invalidator --- .../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 +--- .../TestDefaultAsyncCacheInvalidator.java | 696 ++++++++++++++++++ .../cache/TestDefaultCacheInvalidator.java | 12 +- 7 files changed, 1170 insertions(+), 120 deletions(-) create mode 100644 httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/HttpAsyncCacheInvalidator.java create mode 100644 httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheInvalidatorBase.java create mode 100644 httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/DefaultAsyncCacheInvalidator.java create mode 100644 httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestDefaultAsyncCacheInvalidator.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 000000000..fa9684b77 --- /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 + * . + * + */ +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 cacheKeyResolver, + HttpAsyncCacheStorage cacheStorage, + FutureCallback 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 cacheKeyResolver, + HttpAsyncCacheStorage cacheStorage, + FutureCallback callback); + +} 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 000000000..d229011c0 --- /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 + * . + * + */ +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); + } + +} 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 39300f3d7..068432bc4 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; 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 000000000..c815ce6cf --- /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 + * . + * + */ +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() { + + @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 cacheKeyResolver, + final HttpAsyncCacheStorage storage, + final FutureCallback 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() { + + @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 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 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 cacheKeyResolver, + final HttpAsyncCacheStorage storage, + final FutureCallback 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 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() { + + @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>() { + + @Override + public void completed(final Map resultMap) { + for (final Map.Entry 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(); + } + +} 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 029928bd7..76ac99e63 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 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 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 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 cacheKeyResolver, - final HttpCacheStorage storage) { + final HttpCacheStorage storage, + final Resolver cacheKeyResolver) { final String cacheKey = cacheKeyResolver.resolve(location); final HttpCacheEntry entry = getEntry(storage, cacheKey); - if (entry == null) { - return; - } + if (entry != null) { + // do not invalidate if response is strictly older than entry + // or if the etags match - // 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); + if (!responseDateOlderThanEntryDate(response, entry) && responseAndEntryEtagsDiffer(response, entry)) { + removeEntry(storage, cacheKey); + } } } - 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; - } - return (!entryEtag.getValue().equals(responseEtag.getValue())); - } - - private boolean responseDateOlderThanEntryDate(final HttpResponse response, final HttpCacheEntry entry) { - return DateUtils.isBefore(response, entry, HttpHeaders.DATE); - } } 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 000000000..d64018d3e --- /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 + * . + * + */ +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 cacheKeyResolver; + @Mock + private HttpAsyncCacheStorage mockStorage; + @Mock + private FutureCallback 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.any())).thenAnswer(new Answer() { + + @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 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.>any()); + verify(mockStorage).removeEntry(Mockito.eq(key), Mockito.>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()); + + cacheReturnsEntryForUri(key, mockEntry); + + impl.flushInvalidatedCacheEntries(host, request, cacheKeyResolver, mockStorage, operationCallback); + + verify(mockEntry).getVariantMap(); + verify(mockStorage).getEntry(Mockito.eq(key), Mockito.>any()); + verify(mockStorage).removeEntry(Mockito.eq(key), Mockito.>any()); + verify(mockStorage).removeEntry(Mockito.eq("http://foo.example.com:80/content"), Mockito.>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()); + + cacheReturnsEntryForUri(key, mockEntry); + + impl.flushInvalidatedCacheEntries(host, request, cacheKeyResolver, mockStorage, operationCallback); + + verify(mockEntry).getVariantMap(); + verify(mockStorage).getEntry(Mockito.eq(key), Mockito.>any()); + verify(mockStorage).removeEntry(Mockito.eq(key), Mockito.>any()); + verify(mockStorage).removeEntry(Mockito.eq("http://foo.example.com:80/content"), Mockito.>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()); + + cacheReturnsEntryForUri(key, mockEntry); + + impl.flushInvalidatedCacheEntries(host, request, cacheKeyResolver, mockStorage, operationCallback); + + verify(mockEntry).getVariantMap(); + verify(mockStorage).getEntry(Mockito.eq(key), Mockito.>any()); + verify(mockStorage).removeEntry(Mockito.eq(key), Mockito.>any()); + verify(mockStorage).removeEntry(Mockito.eq("http://foo.example.com:80/content"), Mockito.>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()); + + cacheReturnsEntryForUri(key, mockEntry); + + impl.flushInvalidatedCacheEntries(host, request, cacheKeyResolver, mockStorage, operationCallback); + + verify(mockEntry).getVariantMap(); + verify(mockStorage).getEntry(Mockito.eq(key), Mockito.>any()); + verify(mockStorage).removeEntry(Mockito.eq(key), Mockito.>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.>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.>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()); + cacheReturnsEntryForUri(key, mockEntry); + + impl.flushInvalidatedCacheEntries(host, request, cacheKeyResolver, mockStorage, operationCallback); + + verify(mockEntry).getRequestMethod(); + verify(mockEntry).getVariantMap(); + verify(mockStorage).getEntry(Mockito.eq(key), Mockito.>any()); + verify(mockStorage).removeEntry(Mockito.eq(key), Mockito.>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 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.>any()); + verify(mockStorage).removeEntry(Mockito.eq(key), Mockito.>any()); + verify(mockStorage).removeEntry(Mockito.eq(theVariantURI), Mockito.>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.>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.>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.>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.>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.>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 mapOfURIs = HttpTestUtils.makeDefaultVariantMap(variantUri, variantUri); + + cacheReturnsEntryForUri(key, mockEntry); + cacheEntryHasVariantMap(mapOfURIs); + + impl.flushInvalidatedCacheEntries(host, request, cacheKeyResolver, mockStorage, operationCallback); + + verify(mockStorage).getEntry(Mockito.eq(key), Mockito.>any()); + verify(mockEntry).getVariantMap(); + verify(mockStorage).removeEntry(Mockito.eq(key), Mockito.>any()); + verify(mockStorage).removeEntry(Mockito.eq(variantUri), Mockito.>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.>any()); + verify(mockStorage).removeEntry(Mockito.eq(key), Mockito.>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.>any()); + verify(mockStorage).removeEntry(Mockito.eq(key), Mockito.>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.>any()); + verify(mockStorage).removeEntry(Mockito.eq(key), Mockito.>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.>any()); + verify(mockStorage).removeEntry(Mockito.eq(key), Mockito.>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.>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.>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.>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.>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.>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.>any()); + verify(mockStorage).removeEntry(Mockito.eq(key), Mockito.>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.>any()); + verify(mockStorage).removeEntry(Mockito.eq(key), Mockito.>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.>any()); + verify(mockStorage).removeEntry(Mockito.eq(key), Mockito.>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.>any()); + verify(mockStorage).removeEntry(Mockito.eq(key), Mockito.>any()); + verifyNoMoreInteractions(mockStorage); + } + + + // Expectations + private void cacheEntryHasVariantMap(final Map variantMap) { + when(mockEntry.getVariantMap()).thenReturn(variantMap); + } + + private void cacheReturnsEntryForUri(final String key, final HttpCacheEntry cacheEntry) { + Mockito.when(mockStorage.getEntry( + Mockito.eq(key), + Mockito.>any())).thenAnswer(new Answer() { + + @Override + public Cancellable answer(final InvocationOnMock invocation) throws Throwable { + final FutureCallback callback = invocation.getArgument(1); + callback.completed(cacheEntry); + return cancellable; + } + + }); + } + + private void cacheEntryisForMethod(final String httpMethod) { + when(mockEntry.getRequestMethod()).thenReturn(httpMethod); + } +} 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 5c1813466..31f9af2ee 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", "/");