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 index 327fb362f5..03968f8bc7 100644 --- 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 @@ -41,7 +41,10 @@ * that this exchange would invalidate. * * @since 5.0 + * + * @deprecated Do not use. */ +@Deprecated @Internal public interface HttpAsyncCacheInvalidator { diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/HttpCacheInvalidator.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/HttpCacheInvalidator.java index df80379adb..102c692d56 100644 --- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/HttpCacheInvalidator.java +++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/HttpCacheInvalidator.java @@ -41,7 +41,10 @@ * that this exchange would invalidate. * * @since 4.3 + * + * @deprecated Do not use. */ +@Deprecated @Contract(threading = ThreadingBehavior.STATELESS) @Internal public interface HttpCacheInvalidator { diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AsyncCachingExec.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AsyncCachingExec.java index d113861cd7..e6ac0f091d 100644 --- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AsyncCachingExec.java +++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AsyncCachingExec.java @@ -65,7 +65,6 @@ 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.Method; import org.apache.hc.core5.http.impl.BasicEntityDetails; import org.apache.hc.core5.http.nio.AsyncDataConsumer; import org.apache.hc.core5.http.nio.AsyncEntityProducer; @@ -237,27 +236,7 @@ public void execute( final RequestCacheControl requestCacheControl = CacheControlHeaderParser.INSTANCE.parse(request); - if (!cacheableRequestPolicy.isServableFromCache(requestCacheControl, request)) { - LOG.debug("Request is not servable from cache"); - operation.setDependency(responseCache.flushCacheEntriesInvalidatedByRequest(target, request, new FutureCallback() { - - @Override - public void completed(final Boolean result) { - callBackend(target, request, entityProducer, scope, chain, asyncExecCallback); - } - - @Override - public void failed(final Exception cause) { - asyncExecCallback.failed(cause); - } - - @Override - public void cancelled() { - asyncExecCallback.failed(new InterruptedIOException()); - } - - })); - } else { + if (cacheableRequestPolicy.isServableFromCache(requestCacheControl, request)) { operation.setDependency(responseCache.match(target, request, new FutureCallback() { @Override @@ -291,6 +270,9 @@ public void cancelled() { })); + } else { + LOG.debug("Request is not servable from cache"); + callBackend(target, request, entityProducer, scope, chain, asyncExecCallback); } } @@ -479,7 +461,7 @@ public AsyncDataConsumer handleResponse( final HttpResponse backendResponse, final EntityDetails entityDetails) throws HttpException, IOException { responseCompliance.ensureProtocolCompliance(scope.originalRequest, request, backendResponse); - responseCache.flushCacheEntriesInvalidatedByExchange(target, request, backendResponse, new FutureCallback() { + responseCache.evictInvalidatedEntries(target, request, backendResponse, new FutureCallback() { @Override public void completed(final Boolean result) { @@ -502,24 +484,6 @@ public void cancelled() { storeRequestIfModifiedSinceFor304Response(request, backendResponse); } else { LOG.debug("Backend response is not cacheable"); - if (!Method.isSafe(request.getMethod())) { - responseCache.flushCacheEntriesFor(target, request, new FutureCallback() { - - @Override - public void completed(final Boolean result) { - } - - @Override - public void failed(final Exception ex) { - LOG.warn("Unable to flush invalidated entries from cache", ex); - } - - @Override - public void cancelled() { - } - - }); - } } final CachingAsyncDataConsumer cachingDataConsumer = cachingConsumerRef.get(); if (cachingDataConsumer != null) { diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/BasicHttpAsyncCache.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/BasicHttpAsyncCache.java index f3a07c6af2..7c2abcf06f 100644 --- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/BasicHttpAsyncCache.java +++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/BasicHttpAsyncCache.java @@ -26,16 +26,17 @@ */ package org.apache.hc.client5.http.impl.cache; +import java.net.URI; import java.time.Instant; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; -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.cache.HttpCacheEntryFactory; @@ -47,9 +48,12 @@ import org.apache.hc.core5.concurrent.Cancellable; import org.apache.hc.core5.concurrent.ComplexCancellable; import org.apache.hc.core5.concurrent.FutureCallback; +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; +import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.Method; import org.apache.hc.core5.http.message.RequestLine; import org.apache.hc.core5.http.message.StatusLine; @@ -64,27 +68,24 @@ class BasicHttpAsyncCache implements HttpAsyncCache { private final ResourceFactory resourceFactory; private final HttpCacheEntryFactory cacheEntryFactory; private final CacheKeyGenerator cacheKeyGenerator; - private final HttpAsyncCacheInvalidator cacheInvalidator; private final HttpAsyncCacheStorage storage; public BasicHttpAsyncCache( final ResourceFactory resourceFactory, final HttpCacheEntryFactory cacheEntryFactory, final HttpAsyncCacheStorage storage, - final CacheKeyGenerator cacheKeyGenerator, - final HttpAsyncCacheInvalidator cacheInvalidator) { + final CacheKeyGenerator cacheKeyGenerator) { this.resourceFactory = resourceFactory; this.cacheEntryFactory = cacheEntryFactory; this.cacheKeyGenerator = cacheKeyGenerator; this.storage = storage; - this.cacheInvalidator = cacheInvalidator; } public BasicHttpAsyncCache( final ResourceFactory resourceFactory, final HttpAsyncCacheStorage storage, final CacheKeyGenerator cacheKeyGenerator) { - this(resourceFactory, HttpCacheEntryFactory.INSTANCE, storage, cacheKeyGenerator, DefaultAsyncCacheInvalidator.INSTANCE); + this(resourceFactory, HttpCacheEntryFactory.INSTANCE, storage, cacheKeyGenerator); } public BasicHttpAsyncCache(final ResourceFactory resourceFactory, final HttpAsyncCacheStorage storage) { @@ -456,57 +457,124 @@ public Cancellable storeReusing( return store(request, originResponse, requestSent, responseReceived, rootKey, hit.entry, callback); } - @Override - public Cancellable flushCacheEntriesFor( - final HttpHost host, final HttpRequest request, final FutureCallback callback) { - final String rootKey = cacheKeyGenerator.generateKey(host, request); - if (LOG.isDebugEnabled()) { - LOG.debug("Flush cache entries: {}", rootKey); - } - return storage.removeEntry(rootKey, new FutureCallback() { + private void evictEntry(final String cacheKey) { + storage.removeEntry(cacheKey, new FutureCallback() { @Override public void completed(final Boolean result) { - callback.completed(result); } @Override public void failed(final Exception ex) { - if (ex instanceof ResourceIOException) { - if (LOG.isWarnEnabled()) { - LOG.warn("I/O error removing cache entry with key {}", rootKey); + if (LOG.isWarnEnabled()) { + if (ex instanceof ResourceIOException) { + LOG.warn("I/O error removing cache entry with key {}", cacheKey); + } else { + LOG.warn("Unexpected error removing cache entry with key {}", cacheKey, ex); } - callback.completed(Boolean.TRUE); - } else { - callback.failed(ex); } } @Override public void cancelled() { - callback.cancelled(); } }); } - @Override - public Cancellable flushCacheEntriesInvalidatedByRequest( - final HttpHost host, final HttpRequest request, final FutureCallback callback) { + private void evictAll(final HttpCacheEntry root, final String rootKey) { if (LOG.isDebugEnabled()) { - LOG.debug("Flush cache entries invalidated by request: {}; {}", host, new RequestLine(request)); + LOG.debug("Evicting root cache entry {}", rootKey); + } + evictEntry(rootKey); + if (root.isVariantRoot()) { + for (final String variantKey : root.getVariantMap().values()) { + if (LOG.isDebugEnabled()) { + LOG.debug("Evicting variant cache entry {}", variantKey); + } + evictEntry(variantKey); + } } - return cacheInvalidator.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyGenerator, storage, callback); + } + + private Cancellable evict(final String rootKey) { + return storage.getEntry(rootKey, new FutureCallback() { + + @Override + public void completed(final HttpCacheEntry root) { + if (root != null) { + if (LOG.isDebugEnabled()) { + LOG.debug("Evicting root cache entry {}", rootKey); + } + evictAll(root, rootKey); + } + } + + @Override + public void failed(final Exception ex) { + } + + @Override + public void cancelled() { + } + + }); + } + + private Cancellable evict(final String rootKey, final HttpResponse response) { + return storage.getEntry(rootKey, new FutureCallback() { + + @Override + public void completed(final HttpCacheEntry root) { + if (root != null) { + if (LOG.isDebugEnabled()) { + LOG.debug("Evicting root cache entry {}", rootKey); + } + final Header existingETag = root.getFirstHeader(HttpHeaders.ETAG); + final Header newETag = response.getFirstHeader(HttpHeaders.ETAG); + if (existingETag != null && newETag != null && + !Objects.equals(existingETag.getValue(), newETag.getValue()) && + !DateSupport.isBefore(response, root, HttpHeaders.DATE)) { + evictAll(root, rootKey); + } + } + } + + @Override + public void failed(final Exception ex) { + } + + @Override + public void cancelled() { + } + + }); } @Override - public Cancellable flushCacheEntriesInvalidatedByExchange( + public Cancellable evictInvalidatedEntries( final HttpHost host, final HttpRequest request, final HttpResponse response, final FutureCallback callback) { if (LOG.isDebugEnabled()) { LOG.debug("Flush cache entries invalidated by exchange: {}; {} -> {}", host, new RequestLine(request), new StatusLine(response)); } - if (!Method.isSafe(request.getMethod())) { - return cacheInvalidator.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyGenerator, storage, callback); + final int status = response.getCode(); + if (status >= HttpStatus.SC_SUCCESS && status < HttpStatus.SC_CLIENT_ERROR && + !Method.isSafe(request.getMethod())) { + final String rootKey = cacheKeyGenerator.generateKey(host, request); + evict(rootKey); + final URI requestUri = CacheSupport.normalize(CacheSupport.getRequestUri(request, host)); + if (requestUri != null) { + final URI contentLocation = CacheSupport.getLocationURI(requestUri, response, HttpHeaders.CONTENT_LOCATION); + if (contentLocation != null && CacheSupport.isSameOrigin(requestUri, contentLocation)) { + final String cacheKey = cacheKeyGenerator.generateKey(contentLocation); + evict(cacheKey, response); + } + final URI location = CacheSupport.getLocationURI(requestUri, response, HttpHeaders.LOCATION); + if (location != null && CacheSupport.isSameOrigin(requestUri, location)) { + final String cacheKey = cacheKeyGenerator.generateKey(location); + evict(cacheKey, response); + } + } } callback.completed(Boolean.TRUE); return Operations.nonCancellable(); diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/BasicHttpCache.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/BasicHttpCache.java index 0eecf016e1..29e7b5005a 100644 --- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/BasicHttpCache.java +++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/BasicHttpCache.java @@ -26,25 +26,29 @@ */ package org.apache.hc.client5.http.impl.cache; +import java.net.URI; import java.time.Instant; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import org.apache.hc.client5.http.cache.HttpCacheCASOperation; import org.apache.hc.client5.http.cache.HttpCacheEntry; import org.apache.hc.client5.http.cache.HttpCacheEntryFactory; -import org.apache.hc.client5.http.cache.HttpCacheInvalidator; import org.apache.hc.client5.http.cache.HttpCacheStorage; import org.apache.hc.client5.http.cache.HttpCacheUpdateException; import org.apache.hc.client5.http.cache.Resource; import org.apache.hc.client5.http.cache.ResourceFactory; import org.apache.hc.client5.http.cache.ResourceIOException; +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; +import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.Method; import org.apache.hc.core5.http.message.RequestLine; import org.apache.hc.core5.http.message.StatusLine; @@ -59,27 +63,24 @@ class BasicHttpCache implements HttpCache { private final ResourceFactory resourceFactory; private final HttpCacheEntryFactory cacheEntryFactory; private final CacheKeyGenerator cacheKeyGenerator; - private final HttpCacheInvalidator cacheInvalidator; private final HttpCacheStorage storage; public BasicHttpCache( final ResourceFactory resourceFactory, final HttpCacheEntryFactory cacheEntryFactory, final HttpCacheStorage storage, - final CacheKeyGenerator cacheKeyGenerator, - final HttpCacheInvalidator cacheInvalidator) { + final CacheKeyGenerator cacheKeyGenerator) { this.resourceFactory = resourceFactory; this.cacheEntryFactory = cacheEntryFactory; this.cacheKeyGenerator = cacheKeyGenerator; this.storage = storage; - this.cacheInvalidator = cacheInvalidator; } public BasicHttpCache( final ResourceFactory resourceFactory, final HttpCacheStorage storage, final CacheKeyGenerator cacheKeyGenerator) { - this(resourceFactory, HttpCacheEntryFactory.INSTANCE, storage, cacheKeyGenerator, new DefaultCacheInvalidator()); + this(resourceFactory, HttpCacheEntryFactory.INSTANCE, storage, cacheKeyGenerator); } public BasicHttpCache(final ResourceFactory resourceFactory, final HttpCacheStorage storage) { @@ -129,6 +130,16 @@ HttpCacheEntry getInternal(final String cacheKey) { } } + private void removeInternal(final String cacheKey) { + try { + storage.removeEntry(cacheKey); + } catch (final ResourceIOException ex) { + if (LOG.isWarnEnabled()) { + LOG.warn("I/O error removing cache entry with key {}", cacheKey); + } + } + } + @Override public CacheMatch match(final HttpHost host, final HttpRequest request) { final String rootKey = cacheKeyGenerator.generateKey(host, request); @@ -313,36 +324,66 @@ public CacheHit storeReusing( return store(request, originResponse, requestSent, responseReceived, rootKey, hit.entry); } - @Override - public void flushCacheEntriesFor(final HttpHost host, final HttpRequest request) { - final String rootKey = cacheKeyGenerator.generateKey(host, request); + private void evictAll(final HttpCacheEntry root, final String rootKey) { if (LOG.isDebugEnabled()) { - LOG.debug("Flush cache entries: {}", rootKey); + LOG.debug("Evicting root cache entry {}", rootKey); } - try { - storage.removeEntry(rootKey); - } catch (final ResourceIOException ex) { - if (LOG.isWarnEnabled()) { - LOG.warn("I/O error removing cache entry with key {}", rootKey); + removeInternal(rootKey); + if (root.isVariantRoot()) { + for (final String variantKey : root.getVariantMap().values()) { + if (LOG.isDebugEnabled()) { + LOG.debug("Evicting variant cache entry {}", variantKey); + } + removeInternal(variantKey); } } } - @Override - public void flushCacheEntriesInvalidatedByRequest(final HttpHost host, final HttpRequest request) { - if (LOG.isDebugEnabled()) { - LOG.debug("Flush cache entries invalidated by request: {}; {}", host, new RequestLine(request)); + private void evict(final String rootKey) { + final HttpCacheEntry root = getInternal(rootKey); + if (root == null) { + return; + } + evictAll(root, rootKey); + } + + private void evict(final String rootKey, final HttpResponse response) { + final HttpCacheEntry root = getInternal(rootKey); + if (root == null) { + return; + } + final Header existingETag = root.getFirstHeader(HttpHeaders.ETAG); + final Header newETag = response.getFirstHeader(HttpHeaders.ETAG); + if (existingETag != null && newETag != null && + !Objects.equals(existingETag.getValue(), newETag.getValue()) && + !DateSupport.isBefore(response, root, HttpHeaders.DATE)) { + evictAll(root, rootKey); } - cacheInvalidator.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyGenerator, storage); } @Override - public void flushCacheEntriesInvalidatedByExchange(final HttpHost host, final HttpRequest request, final HttpResponse response) { + public void evictInvalidatedEntries(final HttpHost host, final HttpRequest request, final HttpResponse response) { if (LOG.isDebugEnabled()) { - LOG.debug("Flush cache entries invalidated by exchange: {}; {} -> {}", host, new RequestLine(request), new StatusLine(response)); + LOG.debug("Evict cache entries invalidated by exchange: {}; {} -> {}", host, new RequestLine(request), new StatusLine(response)); } - if (!Method.isSafe(request.getMethod())) { - cacheInvalidator.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyGenerator, storage); + final int status = response.getCode(); + if (status >= HttpStatus.SC_SUCCESS && status < HttpStatus.SC_CLIENT_ERROR && + !Method.isSafe(request.getMethod())) { + final String rootKey = cacheKeyGenerator.generateKey(host, request); + evict(rootKey); + final URI requestUri = CacheSupport.normalize(CacheSupport.getRequestUri(request, host)); + if (requestUri != null) { + final URI contentLocation = CacheSupport.getLocationURI(requestUri, response, HttpHeaders.CONTENT_LOCATION); + if (contentLocation != null && CacheSupport.isSameOrigin(requestUri, contentLocation)) { + final String cacheKey = cacheKeyGenerator.generateKey(contentLocation); + evict(cacheKey, response); + } + final URI location = CacheSupport.getLocationURI(requestUri, response, HttpHeaders.LOCATION); + if (location != null && CacheSupport.isSameOrigin(requestUri, location)) { + final String cacheKey = cacheKeyGenerator.generateKey(location); + evict(cacheKey, response); + } + } } } 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 deleted file mode 100644 index 186edab14c..0000000000 --- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheInvalidatorBase.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * ==================================================================== - * 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.HttpCacheEntry; -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; -import org.apache.hc.core5.http.Method; - -class CacheInvalidatorBase { - - static boolean shouldInvalidateHeadCacheEntry(final HttpRequest req, final HttpCacheEntry parentCacheEntry) { - return requestIsGet(req) && isAHeadCacheEntry(parentCacheEntry); - } - - static boolean requestIsGet(final HttpRequest req) { - return Method.GET.isSame(req.getMethod()); - } - - static boolean isAHeadCacheEntry(final HttpCacheEntry parentCacheEntry) { - return parentCacheEntry != null && Method.HEAD.isSame(parentCacheEntry.getRequestMethod()); - } - - 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 !(Method.GET.isSame(method) || Method.HEAD.isSame(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 = CacheSupport.normalize(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(HttpHeaders.ETAG); - final Header responseEtag = response.getFirstHeader(HttpHeaders.ETAG); - if (entryEtag == null || responseEtag == null) { - return false; - } - return (!entryEtag.getValue().equals(responseEtag.getValue())); - } - - static boolean responseDateOlderThanEntryDate(final HttpResponse response, final HttpCacheEntry entry) { - return DateSupport.isBefore(response, entry, HttpHeaders.DATE); - } - -} diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheSupport.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheSupport.java index 4de39c4632..bd61bf3681 100644 --- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheSupport.java +++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheSupport.java @@ -29,6 +29,7 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.BitSet; +import java.util.Objects; import java.util.function.Consumer; import org.apache.hc.client5.http.utils.URIUtils; @@ -37,6 +38,7 @@ 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.MessageHeaders; import org.apache.hc.core5.http.URIScheme; import org.apache.hc.core5.http.message.ParserCursor; import org.apache.hc.core5.net.URIAuthority; @@ -173,4 +175,24 @@ public static void parseTokens(final Header header, final Consumer consu } } + public static URI getLocationURI(final URI requestUri, final MessageHeaders response, final String headerName) { + final Header h = response.getFirstHeader(headerName); + if (h == null) { + return null; + } + final URI locationUri = CacheSupport.normalize(h.getValue()); + if (locationUri == null) { + return requestUri; + } + if (locationUri.isAbsolute()) { + return locationUri; + } else { + return URIUtils.resolve(requestUri, locationUri); + } + } + + public static boolean isSameOrigin(final URI requestURI, final URI targetURI) { + return targetURI.isAbsolute() && Objects.equals(requestURI.getAuthority(), targetURI.getAuthority()); + } + } diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExec.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExec.java index f76785db39..ec5e6cc536 100644 --- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExec.java +++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExec.java @@ -57,7 +57,6 @@ import org.apache.hc.core5.http.HttpRequest; import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.HttpVersion; -import org.apache.hc.core5.http.Method; import org.apache.hc.core5.http.io.entity.ByteArrayEntity; import org.apache.hc.core5.http.io.entity.StringEntity; import org.apache.hc.core5.http.io.support.ClassicRequestBuilder; @@ -182,7 +181,6 @@ public ClassicHttpResponse execute( final RequestCacheControl requestCacheControl = CacheControlHeaderParser.INSTANCE.parse(request); if (!cacheableRequestPolicy.isServableFromCache(requestCacheControl, request)) { LOG.debug("Request is not servable from cache"); - responseCache.flushCacheEntriesInvalidatedByRequest(target, request); return callBackend(target, request, scope, chain); } @@ -386,7 +384,7 @@ ClassicHttpResponse handleBackendResponse( responseCompliance.ensureProtocolCompliance(scope.originalRequest, request, backendResponse); - responseCache.flushCacheEntriesInvalidatedByExchange(target, request, backendResponse); + responseCache.evictInvalidatedEntries(target, request, backendResponse); final ResponseCacheControl responseCacheControl = CacheControlHeaderParser.INSTANCE.parse(backendResponse); final boolean cacheable = responseCachingPolicy.isResponseCacheable(responseCacheControl, request, backendResponse); if (cacheable) { @@ -394,9 +392,6 @@ ClassicHttpResponse handleBackendResponse( return cacheAndReturnResponse(target, request, backendResponse, scope, requestDate, responseDate); } LOG.debug("Backend response is not cacheable"); - if (!Method.isSafe(request.getMethod())) { - responseCache.flushCacheEntriesFor(target, request); - } return backendResponse; } diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingH2AsyncClientBuilder.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingH2AsyncClientBuilder.java index 422230e7d2..38d96d0024 100644 --- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingH2AsyncClientBuilder.java +++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingH2AsyncClientBuilder.java @@ -58,7 +58,6 @@ public class CachingH2AsyncClientBuilder extends H2AsyncClientBuilder { private File cacheDir; private SchedulingStrategy schedulingStrategy; private CacheConfig cacheConfig; - private HttpAsyncCacheInvalidator httpCacheInvalidator; private boolean deleteCache; public static CachingH2AsyncClientBuilder create() { @@ -100,8 +99,11 @@ public final CachingH2AsyncClientBuilder setCacheConfig(final CacheConfig cacheC return this; } + /** + * @deprecated Do not use. + */ + @Deprecated public final CachingH2AsyncClientBuilder setHttpCacheInvalidator(final HttpAsyncCacheInvalidator cacheInvalidator) { - this.httpCacheInvalidator = cacheInvalidator; return this; } @@ -140,8 +142,7 @@ protected void customizeExecChain(final NamedElementChain resourceFactoryCopy, HttpCacheEntryFactory.INSTANCE, storageCopy, - CacheKeyGenerator.INSTANCE, - this.httpCacheInvalidator != null ? this.httpCacheInvalidator : new DefaultAsyncCacheInvalidator()); + CacheKeyGenerator.INSTANCE); DefaultAsyncCacheRevalidator cacheRevalidator = null; if (config.getAsynchronousWorkers() > 0) { diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingHttpAsyncClientBuilder.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingHttpAsyncClientBuilder.java index 5af29ba46e..ee951ef49e 100644 --- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingHttpAsyncClientBuilder.java +++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingHttpAsyncClientBuilder.java @@ -58,7 +58,6 @@ public class CachingHttpAsyncClientBuilder extends HttpAsyncClientBuilder { private File cacheDir; private SchedulingStrategy schedulingStrategy; private CacheConfig cacheConfig; - private HttpAsyncCacheInvalidator httpCacheInvalidator; private boolean deleteCache; public static CachingHttpAsyncClientBuilder create() { @@ -100,8 +99,11 @@ public final CachingHttpAsyncClientBuilder setCacheConfig(final CacheConfig cach return this; } + /** + * @deprecated Do not use. + */ + @Deprecated public final CachingHttpAsyncClientBuilder setHttpCacheInvalidator(final HttpAsyncCacheInvalidator cacheInvalidator) { - this.httpCacheInvalidator = cacheInvalidator; return this; } @@ -140,8 +142,7 @@ protected void customizeExecChain(final NamedElementChain resourceFactoryCopy, HttpCacheEntryFactory.INSTANCE, storageCopy, - CacheKeyGenerator.INSTANCE, - this.httpCacheInvalidator != null ? this.httpCacheInvalidator : new DefaultAsyncCacheInvalidator()); + CacheKeyGenerator.INSTANCE); DefaultAsyncCacheRevalidator cacheRevalidator = null; if (config.getAsynchronousWorkers() > 0) { diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingHttpClientBuilder.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingHttpClientBuilder.java index de50a76220..f13c186dbd 100644 --- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingHttpClientBuilder.java +++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingHttpClientBuilder.java @@ -54,7 +54,6 @@ public class CachingHttpClientBuilder extends HttpClientBuilder { private File cacheDir; private SchedulingStrategy schedulingStrategy; private CacheConfig cacheConfig; - private HttpCacheInvalidator httpCacheInvalidator; private boolean deleteCache; public static CachingHttpClientBuilder create() { @@ -92,8 +91,11 @@ public final CachingHttpClientBuilder setCacheConfig(final CacheConfig cacheConf return this; } + /** + * @deprecated Do not use. + */ + @Deprecated public final CachingHttpClientBuilder setHttpCacheInvalidator(final HttpCacheInvalidator cacheInvalidator) { - this.httpCacheInvalidator = cacheInvalidator; return this; } @@ -132,8 +134,7 @@ protected void customizeExecChain(final NamedElementChain exec resourceFactoryCopy, HttpCacheEntryFactory.INSTANCE, storageCopy, - CacheKeyGenerator.INSTANCE, - this.httpCacheInvalidator != null ? this.httpCacheInvalidator : new DefaultCacheInvalidator()); + CacheKeyGenerator.INSTANCE); DefaultCacheRevalidator cacheRevalidator = null; if (config.getAsynchronousWorkers() > 0) { 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 deleted file mode 100644 index 91d301ba4d..0000000000 --- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/DefaultAsyncCacheInvalidator.java +++ /dev/null @@ -1,266 +0,0 @@ -/* - * ==================================================================== - * 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.HttpHeaders; -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.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Given a particular HTTP request / response pair, flush any cache entries - * that this exchange would invalidate. - * - * @since 5.0 - */ -@Contract(threading = ThreadingBehavior.STATELESS) -@Internal -public class DefaultAsyncCacheInvalidator extends CacheInvalidatorBase implements HttpAsyncCacheInvalidator { - - public static final DefaultAsyncCacheInvalidator INSTANCE = new DefaultAsyncCacheInvalidator(); - - private static final Logger LOG = LoggerFactory.getLogger(DefaultAsyncCacheInvalidator.class); - - 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.booleanValue()) { - LOG.debug("Cache entry with key {} successfully flushed", cacheKey); - } else { - LOG.debug("Cache entry with key {} could not be flushed", cacheKey); - } - } - } - - @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 flushCacheEntriesInvalidatedByRequest( - final HttpHost host, - final HttpRequest request, - final Resolver cacheKeyResolver, - final HttpAsyncCacheStorage storage, - final FutureCallback callback) { - final String s = CacheSupport.getRequestUri(request, host); - final URI uri = CacheSupport.normalize(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("{} is not a valid URI", s); - } - final Header clHdr = request.getFirstHeader(HttpHeaders.CONTENT_LOCATION); - if (clHdr != null) { - final URI contentLocation = CacheSupport.normalize(clHdr.getValue()); - if (contentLocation != null) { - if (!flushAbsoluteUriFromSameHost(uri, contentLocation, cacheKeyResolver, storage)) { - flushRelativeUriFromSameHost(uri, contentLocation, cacheKeyResolver, storage); - } - } - } - final Header lHdr = request.getFirstHeader(HttpHeaders.LOCATION); - if (lHdr != null) { - final URI location = CacheSupport.normalize(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; - } - return false; - } - - @Override - public Cancellable flushCacheEntriesInvalidatedByExchange( - 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 = CacheSupport.getRequestUri(request, host); - final URI requestUri = CacheSupport.normalize(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 deleted file mode 100644 index d629857a0d..0000000000 --- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/DefaultCacheInvalidator.java +++ /dev/null @@ -1,194 +0,0 @@ -/* - * ==================================================================== - * 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.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.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; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Given a particular HTTP request / response pair, flush any cache entries - * that this exchange would invalidate. - * - * @since 4.1 - */ -@Contract(threading = ThreadingBehavior.STATELESS) -@Internal -public class DefaultCacheInvalidator extends CacheInvalidatorBase implements HttpCacheInvalidator { - - public static final DefaultCacheInvalidator INSTANCE = new DefaultCacheInvalidator(); - - private static final Logger LOG = LoggerFactory.getLogger(DefaultCacheInvalidator.class); - - private HttpCacheEntry getEntry(final HttpCacheStorage storage, final String cacheKey) { - try { - return storage.getEntry(cacheKey); - } catch (final ResourceIOException ex) { - if (LOG.isWarnEnabled()) { - LOG.warn("Unable to get cache entry with key {}", cacheKey, ex); - } - return null; - } - } - - private void removeEntry(final HttpCacheStorage storage, final String cacheKey) { - try { - storage.removeEntry(cacheKey); - } catch (final ResourceIOException ex) { - if (LOG.isWarnEnabled()) { - LOG.warn("Unable to flush cache entry with key {}", cacheKey, ex); - } - } - } - - @Override - public void flushCacheEntriesInvalidatedByRequest( - final HttpHost host, - final HttpRequest request, - final Resolver cacheKeyResolver, - final HttpCacheStorage storage) { - final String s = CacheSupport.getRequestUri(request, host); - final URI uri = CacheSupport.normalize(s); - final String cacheKey = uri != null ? cacheKeyResolver.resolve(uri) : s; - final HttpCacheEntry parent = getEntry(storage, cacheKey); - - if (requestShouldNotBeCached(request) || shouldInvalidateHeadCacheEntry(request, parent)) { - if (parent != null) { - if (LOG.isDebugEnabled()) { - LOG.debug("Invalidating parent cache entry with key {}", cacheKey); - } - for (final String variantURI : parent.getVariantMap().values()) { - removeEntry(storage, variantURI); - } - removeEntry(storage, cacheKey); - } - if (uri != null) { - if (LOG.isWarnEnabled()) { - LOG.warn("{} is not a valid URI", s); - } - final Header clHdr = request.getFirstHeader(HttpHeaders.CONTENT_LOCATION); - if (clHdr != null) { - final URI contentLocation = CacheSupport.normalize(clHdr.getValue()); - if (contentLocation != null) { - if (!flushAbsoluteUriFromSameHost(uri, contentLocation, cacheKeyResolver, storage)) { - flushRelativeUriFromSameHost(uri, contentLocation, cacheKeyResolver, storage); - } - } - } - final Header lHdr = request.getFirstHeader(HttpHeaders.LOCATION); - if (lHdr != null) { - final URI location = CacheSupport.normalize(lHdr.getValue()); - if (location != null) { - flushAbsoluteUriFromSameHost(uri, location, cacheKeyResolver, storage); - } - } - } - } - } - - 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 && 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 && isSameHost(requestUri, uri)) { - removeEntry(storage, cacheKeyResolver.resolve(uri)); - return true; - } - return false; - } - - @Override - public void flushCacheEntriesInvalidatedByExchange( - final HttpHost host, - final HttpRequest request, - final HttpResponse response, - final Resolver cacheKeyResolver, - final HttpCacheStorage storage) { - final int status = response.getCode(); - if (status < 200 || status > 299) { - return; - } - final String s = CacheSupport.getRequestUri(request, host); - final URI uri = CacheSupport.normalize(s); - if (uri == null) { - return; - } - final URI contentLocation = getContentLocationURI(uri, response); - if (contentLocation != null && isSameHost(uri, contentLocation)) { - flushLocationCacheEntry(response, contentLocation, storage, cacheKeyResolver); - } - final URI location = getLocationURI(uri, response); - if (location != null && isSameHost(uri, location)) { - flushLocationCacheEntry(response, location, storage, cacheKeyResolver); - } - } - - private void flushLocationCacheEntry( - final HttpResponse response, - final URI location, - final HttpCacheStorage storage, - final Resolver cacheKeyResolver) { - final String cacheKey = cacheKeyResolver.resolve(location); - final HttpCacheEntry entry = getEntry(storage, cacheKey); - 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, cacheKey); - } - } - } - -} diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/HttpAsyncCache.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/HttpAsyncCache.java index ba59f83d40..eb00627d5f 100644 --- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/HttpAsyncCache.java +++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/HttpAsyncCache.java @@ -99,21 +99,9 @@ Cancellable storeReusing( FutureCallback callback); /** - * Clear all matching {@link HttpCacheEntry}s. + * Evicts {@link HttpCacheEntry}s invalidated by the given message exchange. */ - Cancellable flushCacheEntriesFor( - HttpHost host, HttpRequest request, FutureCallback callback); - - /** - * Flush {@link HttpCacheEntry}s invalidated by the given request - */ - Cancellable flushCacheEntriesInvalidatedByRequest( - HttpHost host, HttpRequest request, FutureCallback callback); - - /** - * Flush {@link HttpCacheEntry}s invalidated by the given message exchange. - */ - Cancellable flushCacheEntriesInvalidatedByExchange( + Cancellable evictInvalidatedEntries( HttpHost host, HttpRequest request, HttpResponse response, FutureCallback callback); } diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/HttpCache.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/HttpCache.java index fa73d3e740..f732843074 100644 --- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/HttpCache.java +++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/HttpCache.java @@ -92,18 +92,8 @@ CacheHit storeReusing( Instant responseReceived); /** - * Clear all matching {@link HttpCacheEntry}s. + * Evicts {@link HttpCacheEntry}s invalidated by the given message exchange. */ - void flushCacheEntriesFor(HttpHost host, HttpRequest request); - - /** - * Flush {@link HttpCacheEntry}s invalidated by the given request - */ - void flushCacheEntriesInvalidatedByRequest(HttpHost host, HttpRequest request); - - /** - * Flush {@link HttpCacheEntry}s invalidated by the given message exchange. - */ - void flushCacheEntriesInvalidatedByExchange(HttpHost host, HttpRequest request, HttpResponse response); + void evictInvalidatedEntries(HttpHost host, HttpRequest request, HttpResponse response); } diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/HttpTestUtils.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/HttpTestUtils.java index 82be89d68b..109a629d40 100644 --- a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/HttpTestUtils.java +++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/HttpTestUtils.java @@ -29,16 +29,17 @@ import java.io.InputStream; import java.time.Duration; import java.time.Instant; -import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Objects; import java.util.Random; +import java.util.concurrent.CountDownLatch; import org.apache.hc.client5.http.cache.HttpCacheEntry; import org.apache.hc.client5.http.cache.HttpCacheEntryFactory; import org.apache.hc.client5.http.cache.Resource; import org.apache.hc.client5.http.utils.DateUtils; +import org.apache.hc.core5.concurrent.FutureCallback; import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ClassicHttpResponse; import org.apache.hc.core5.http.Header; @@ -389,11 +390,28 @@ public static ClassicHttpResponse make500Response() { return new BasicClassicHttpResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR, "Internal Server Error"); } - public static Map makeDefaultVariantMap(final String key, final String value) { - final Map variants = new HashMap<>(); - variants.put(key, value); + public static FutureCallback countDown(final CountDownLatch latch) { + return new FutureCallback() { + + @Override + public void completed(final T result) { + latch.countDown(); + } + + @Override + public void failed(final Exception ex) { + latch.countDown(); + Assertions.fail(ex); + } + + @Override + public void cancelled() { + latch.countDown(); + Assertions.fail("Unexpected cancellation"); + } + + }; - return variants; } } diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/SimpleHttpAsyncCacheStorage.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/SimpleHttpAsyncCacheStorage.java new file mode 100644 index 0000000000..efe73c2696 --- /dev/null +++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/SimpleHttpAsyncCacheStorage.java @@ -0,0 +1,114 @@ +/* + * ==================================================================== + * 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.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import org.apache.hc.client5.http.cache.HttpAsyncCacheStorage; +import org.apache.hc.client5.http.cache.HttpCacheCASOperation; +import org.apache.hc.client5.http.cache.HttpCacheEntry; +import org.apache.hc.client5.http.cache.ResourceIOException; +import org.apache.hc.client5.http.impl.Operations; +import org.apache.hc.core5.concurrent.Cancellable; +import org.apache.hc.core5.concurrent.FutureCallback; + +class SimpleHttpAsyncCacheStorage implements HttpAsyncCacheStorage { + + public final Map map; + + public SimpleHttpAsyncCacheStorage() { + map = new HashMap<>(); + } + + @Override + public Cancellable putEntry(final String key, final HttpCacheEntry entry, final FutureCallback callback) { + map.put(key, entry); + if (callback != null) { + callback.completed(true); + } + return Operations.nonCancellable(); + } + + public void putEntry(final String key, final HttpCacheEntry entry) { + map.put(key, entry); + } + + @Override + public Cancellable getEntry(final String key, final FutureCallback callback) { + final HttpCacheEntry entry = map.get(key); + if (callback != null) { + callback.completed(entry); + } + return Operations.nonCancellable(); + } + + public HttpCacheEntry getEntry(final String key) { + return map.get(key); + } + + @Override + public Cancellable removeEntry(final String key, final FutureCallback callback) { + final HttpCacheEntry removed = map.remove(key); + if (callback != null) { + callback.completed(removed != null); + } + return Operations.nonCancellable(); + } + + @Override + public Cancellable updateEntry(final String key, final HttpCacheCASOperation casOperation, final FutureCallback callback) { + final HttpCacheEntry v1 = map.get(key); + try { + final HttpCacheEntry v2 = casOperation.execute(v1); + map.put(key,v2); + if (callback != null) { + callback.completed(true); + } + } catch (final ResourceIOException ex) { + if (callback != null) { + callback.failed(ex); + } + } + return Operations.nonCancellable(); + } + + @Override + public Cancellable getEntries(final Collection keys, final FutureCallback> callback) { + final Map resultMap = new HashMap<>(keys.size()); + for (final String key: keys) { + final HttpCacheEntry entry = map.get(key); + if (entry != null) { + resultMap.put(key, entry); + } + } + callback.completed(resultMap); + return Operations.nonCancellable(); + } + +} diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestBasicHttpAsyncCache.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestBasicHttpAsyncCache.java new file mode 100644 index 0000000000..b809e93bd7 --- /dev/null +++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestBasicHttpAsyncCache.java @@ -0,0 +1,534 @@ +/* + * ==================================================================== + * 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 java.net.URI; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CountDownLatch; + +import org.apache.hc.client5.http.utils.DateUtils; +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.message.BasicHeader; +import org.apache.hc.core5.http.message.BasicHttpRequest; +import org.apache.hc.core5.net.URIBuilder; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +public class TestBasicHttpAsyncCache { + + private HttpHost host; + private Instant now; + private Instant tenSecondsAgo; + private SimpleHttpAsyncCacheStorage mockStorage; + private BasicHttpAsyncCache impl; + + @BeforeEach + public void setUp() { + host = new HttpHost("foo.example.com"); + now = Instant.now(); + tenSecondsAgo = now.minusSeconds(10); + mockStorage = Mockito.spy(new SimpleHttpAsyncCacheStorage()); + impl = new BasicHttpAsyncCache(HeapResourceFactory.INSTANCE, mockStorage); + } + + // Tests + @Test + public void testInvalidatesUnsafeRequests() throws Exception { + final HttpRequest request = new BasicHttpRequest("POST", "/path"); + final HttpResponse response = HttpTestUtils.make200Response(); + + final String key = CacheKeyGenerator.INSTANCE.generateKey(host, request); + + mockStorage.putEntry(key, HttpTestUtils.makeCacheEntry()); + + final CountDownLatch latch = new CountDownLatch(1); + impl.evictInvalidatedEntries(host, request, response, HttpTestUtils.countDown(latch)); + + latch.await(); + + verify(mockStorage).getEntry(Mockito.eq(key), Mockito.any()); + verify(mockStorage).removeEntry(Mockito.eq(key), Mockito.any()); + Assertions.assertNull(mockStorage.getEntry(key)); + } + + @Test + public void testDoesNotInvalidateSafeRequests() throws Exception { + final HttpRequest request1 = new BasicHttpRequest("GET", "/"); + final HttpResponse response1 = HttpTestUtils.make200Response(); + final CountDownLatch latch1 = new CountDownLatch(1); + + impl.evictInvalidatedEntries(host, request1, response1, HttpTestUtils.countDown(latch1)); + + latch1.await(); + + verifyNoMoreInteractions(mockStorage); + + final HttpRequest request2 = new BasicHttpRequest("HEAD", "/"); + final HttpResponse response2 = HttpTestUtils.make200Response(); + final CountDownLatch latch2 = new CountDownLatch(1); + + impl.evictInvalidatedEntries(host, request2, response2, HttpTestUtils.countDown(latch2)); + + latch2.await(); + + verifyNoMoreInteractions(mockStorage); + } + + @Test + public void testInvalidatesUnsafeRequestsWithVariants() throws Exception { + final HttpRequest request = new BasicHttpRequest("POST", "/path"); + final String rootKey = CacheKeyGenerator.INSTANCE.generateKey(host, request); + final String variantKey1 = "{var1}" + rootKey; + final String variantKey2 = "{var2}" + rootKey; + final Map variantMap = new HashMap<>(); + variantMap.put("{var1}", variantKey1); + variantMap.put("{var2}", variantKey2); + + final HttpResponse response = HttpTestUtils.make200Response(); + + mockStorage.putEntry(rootKey, HttpTestUtils.makeCacheEntry(variantMap)); + mockStorage.putEntry(variantKey1, HttpTestUtils.makeCacheEntry()); + mockStorage.putEntry(variantKey2, HttpTestUtils.makeCacheEntry()); + + final CountDownLatch latch = new CountDownLatch(1); + impl.evictInvalidatedEntries(host, request, response, HttpTestUtils.countDown(latch)); + + latch.await(); + + verify(mockStorage).getEntry(Mockito.eq(rootKey), Mockito.any()); + verify(mockStorage).removeEntry(Mockito.eq(rootKey), Mockito.any()); + verify(mockStorage).removeEntry(Mockito.eq(variantKey1), Mockito.any()); + verify(mockStorage).removeEntry(Mockito.eq(variantKey2), Mockito.any()); + + Assertions.assertNull(mockStorage.getEntry(rootKey)); + Assertions.assertNull(mockStorage.getEntry(variantKey1)); + Assertions.assertNull(mockStorage.getEntry(variantKey2)); + } + + @Test + public void testInvalidateUriSpecifiedByContentLocationAndFresher() throws Exception { + final HttpRequest request = new BasicHttpRequest("PUT", "/foo"); + final String rootKey = CacheKeyGenerator.INSTANCE.generateKey(host, request); + final URI contentUri = new URIBuilder() + .setHttpHost(host) + .setPath("/bar") + .build(); + final String contentKey = CacheKeyGenerator.INSTANCE.generateKey(contentUri); + + final HttpResponse response = HttpTestUtils.make200Response(); + response.setHeader("ETag","\"new-etag\""); + response.setHeader("Date", DateUtils.formatStandardDate(now)); + response.setHeader("Content-Location", contentUri.toASCIIString()); + + mockStorage.putEntry(rootKey, HttpTestUtils.makeCacheEntry()); + mockStorage.putEntry(contentKey, HttpTestUtils.makeCacheEntry( + new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)), + new BasicHeader("ETag", "\"old-etag\"") + )); + + final CountDownLatch latch = new CountDownLatch(1); + impl.evictInvalidatedEntries(host, request, response, HttpTestUtils.countDown(latch)); + + latch.await(); + + verify(mockStorage).getEntry(Mockito.eq(rootKey), Mockito.any()); + verify(mockStorage).removeEntry(Mockito.eq(rootKey), Mockito.any()); + verify(mockStorage).getEntry(Mockito.eq(contentKey), Mockito.any()); + verify(mockStorage).removeEntry(Mockito.eq(contentKey), Mockito.any()); + } + + @Test + public void testInvalidateUriSpecifiedByLocationAndFresher() throws Exception { + final HttpRequest request = new BasicHttpRequest("PUT", "/foo"); + final String rootKey = CacheKeyGenerator.INSTANCE.generateKey(host, request); + final URI contentUri = new URIBuilder() + .setHttpHost(host) + .setPath("/bar") + .build(); + final String contentKey = CacheKeyGenerator.INSTANCE.generateKey(contentUri); + + final HttpResponse response = HttpTestUtils.make200Response(); + response.setHeader("ETag","\"new-etag\""); + response.setHeader("Date", DateUtils.formatStandardDate(now)); + response.setHeader("Location", contentUri.toASCIIString()); + + mockStorage.putEntry(rootKey, HttpTestUtils.makeCacheEntry()); + mockStorage.putEntry(contentKey, HttpTestUtils.makeCacheEntry( + new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)), + new BasicHeader("ETag", "\"old-etag\"") + )); + + final CountDownLatch latch = new CountDownLatch(1); + impl.evictInvalidatedEntries(host, request, response, HttpTestUtils.countDown(latch)); + + latch.await(); + + verify(mockStorage).getEntry(Mockito.eq(rootKey), Mockito.any()); + verify(mockStorage).removeEntry(Mockito.eq(rootKey), Mockito.any()); + verify(mockStorage).getEntry(Mockito.eq(contentKey), Mockito.any()); + verify(mockStorage).removeEntry(Mockito.eq(contentKey), Mockito.any()); + } + + @Test + public void testDoesNotInvalidateForUnsuccessfulResponse() throws Exception { + final HttpRequest request = new BasicHttpRequest("PUT", "/foo"); + final URI contentUri = new URIBuilder() + .setHttpHost(host) + .setPath("/bar") + .build(); + final HttpResponse response = HttpTestUtils.make500Response(); + response.setHeader("ETag","\"new-etag\""); + response.setHeader("Date", DateUtils.formatStandardDate(now)); + response.setHeader("Content-Location", contentUri.toASCIIString()); + + final CountDownLatch latch = new CountDownLatch(1); + impl.evictInvalidatedEntries(host, request, response, HttpTestUtils.countDown(latch)); + + latch.await(); + + verifyNoMoreInteractions(mockStorage); + } + + @Test + public void testInvalidateUriSpecifiedByContentLocationNonCanonical() throws Exception { + final HttpRequest request = new BasicHttpRequest("PUT", "/foo"); + final String rootKey = CacheKeyGenerator.INSTANCE.generateKey(host, request); + final URI contentUri = new URIBuilder() + .setHttpHost(host) + .setPath("/bar") + .build(); + final String contentKey = CacheKeyGenerator.INSTANCE.generateKey(contentUri); + + final HttpResponse response = HttpTestUtils.make200Response(); + response.setHeader("ETag","\"new-etag\""); + response.setHeader("Date", DateUtils.formatStandardDate(now)); + + response.setHeader("Content-Location", contentUri.toASCIIString()); + + mockStorage.putEntry(rootKey, HttpTestUtils.makeCacheEntry()); + + mockStorage.putEntry(contentKey, HttpTestUtils.makeCacheEntry( + new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)), + new BasicHeader("ETag", "\"old-etag\""))); + + final CountDownLatch latch = new CountDownLatch(1); + impl.evictInvalidatedEntries(host, request, response, HttpTestUtils.countDown(latch)); + + latch.await(); + + verify(mockStorage).getEntry(Mockito.eq(rootKey), Mockito.any()); + verify(mockStorage).removeEntry(Mockito.eq(rootKey), Mockito.any()); + verify(mockStorage).getEntry(Mockito.eq(contentKey), Mockito.any()); + verify(mockStorage).removeEntry(Mockito.eq(contentKey), Mockito.any()); + Assertions.assertNull(mockStorage.getEntry(rootKey)); + Assertions.assertNull(mockStorage.getEntry(contentKey)); + } + + @Test + public void testInvalidateUriSpecifiedByContentLocationRelative() throws Exception { + final HttpRequest request = new BasicHttpRequest("PUT", "/foo"); + final String rootKey = CacheKeyGenerator.INSTANCE.generateKey(host, request); + final URI contentUri = new URIBuilder() + .setHttpHost(host) + .setPath("/bar") + .build(); + final String contentKey = CacheKeyGenerator.INSTANCE.generateKey(contentUri); + + final HttpResponse response = HttpTestUtils.make200Response(); + response.setHeader("ETag","\"new-etag\""); + response.setHeader("Date", DateUtils.formatStandardDate(now)); + + response.setHeader("Content-Location", "/bar"); + + mockStorage.putEntry(rootKey, HttpTestUtils.makeCacheEntry()); + + mockStorage.putEntry(contentKey, HttpTestUtils.makeCacheEntry( + new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)), + new BasicHeader("ETag", "\"old-etag\""))); + + final CountDownLatch latch = new CountDownLatch(1); + impl.evictInvalidatedEntries(host, request, response, HttpTestUtils.countDown(latch)); + + latch.await(); + + verify(mockStorage).getEntry(Mockito.eq(rootKey), Mockito.any()); + verify(mockStorage).removeEntry(Mockito.eq(rootKey), Mockito.any()); + verify(mockStorage).getEntry(Mockito.eq(contentKey), Mockito.any()); + verify(mockStorage).removeEntry(Mockito.eq(contentKey), Mockito.any()); + Assertions.assertNull(mockStorage.getEntry(rootKey)); + Assertions.assertNull(mockStorage.getEntry(contentKey)); + } + + @Test + public void testDoesNotInvalidateUriSpecifiedByContentLocationOtherOrigin() throws Exception { + final HttpRequest request = new BasicHttpRequest("PUT", "/"); + final URI contentUri = new URIBuilder() + .setHost("bar.example.com") + .setPath("/") + .build(); + final String contentKey = CacheKeyGenerator.INSTANCE.generateKey(contentUri); + + final HttpResponse response = HttpTestUtils.make200Response(); + response.setHeader("ETag","\"new-etag\""); + response.setHeader("Date", DateUtils.formatStandardDate(now)); + response.setHeader("Content-Location", contentUri.toASCIIString()); + + mockStorage.putEntry(contentKey, HttpTestUtils.makeCacheEntry()); + + final CountDownLatch latch = new CountDownLatch(1); + impl.evictInvalidatedEntries(host, request, response, HttpTestUtils.countDown(latch)); + + latch.await(); + + verify(mockStorage, Mockito.never()).getEntry(contentKey); + verify(mockStorage, Mockito.never()).removeEntry(Mockito.eq(contentKey), Mockito.any()); + } + + @Test + public void testDoesNotInvalidateUriSpecifiedByContentLocationIfEtagsMatch() throws Exception { + final HttpRequest request = new BasicHttpRequest("PUT", "/foo"); + final URI contentUri = new URIBuilder() + .setHttpHost(host) + .setPath("/bar") + .build(); + final String contentKey = CacheKeyGenerator.INSTANCE.generateKey(contentUri); + + final HttpResponse response = HttpTestUtils.make200Response(); + response.setHeader("ETag","\"same-etag\""); + response.setHeader("Date", DateUtils.formatStandardDate(now)); + response.setHeader("Content-Location", contentUri.toASCIIString()); + + mockStorage.putEntry(contentKey, HttpTestUtils.makeCacheEntry( + new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)), + new BasicHeader("ETag", "\"same-etag\""))); + + final CountDownLatch latch = new CountDownLatch(1); + impl.evictInvalidatedEntries(host, request, response, HttpTestUtils.countDown(latch)); + + latch.await(); + + verify(mockStorage).getEntry(Mockito.eq(contentKey), Mockito.any()); + verify(mockStorage, Mockito.never()).removeEntry(Mockito.eq(contentKey), Mockito.any()); + } + + @Test + public void testDoesNotInvalidateUriSpecifiedByContentLocationIfOlder() throws Exception { + final HttpRequest request = new BasicHttpRequest("PUT", "/foo"); + final URI contentUri = new URIBuilder() + .setHttpHost(host) + .setPath("/bar") + .build(); + final String contentKey = CacheKeyGenerator.INSTANCE.generateKey(contentUri); + + final HttpResponse response = HttpTestUtils.make200Response(); + response.setHeader("ETag","\"new-etag\""); + response.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)); + response.setHeader("Content-Location", contentUri.toASCIIString()); + + mockStorage.putEntry(contentKey, HttpTestUtils.makeCacheEntry( + new BasicHeader("Date", DateUtils.formatStandardDate(now)), + new BasicHeader("ETag", "\"old-etag\""))); + + final CountDownLatch latch = new CountDownLatch(1); + impl.evictInvalidatedEntries(host, request, response, HttpTestUtils.countDown(latch)); + + latch.await(); + + verify(mockStorage).getEntry(Mockito.eq(contentKey), Mockito.any()); + verify(mockStorage, Mockito.never()).removeEntry(Mockito.eq(contentKey), Mockito.any()); + } + + @Test + public void testDoesNotInvalidateUriSpecifiedByContentLocationIfResponseHasNoEtag() throws Exception { + final HttpRequest request = new BasicHttpRequest("PUT", "/foo"); + final URI contentUri = new URIBuilder() + .setHttpHost(host) + .setPath("/bar") + .build(); + final String contentKey = CacheKeyGenerator.INSTANCE.generateKey(contentUri); + + final HttpResponse response = HttpTestUtils.make200Response(); + response.removeHeaders("ETag"); + response.setHeader("Date", DateUtils.formatStandardDate(now)); + response.setHeader("Content-Location", contentUri.toASCIIString()); + + mockStorage.putEntry(contentKey, HttpTestUtils.makeCacheEntry( + new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)), + new BasicHeader("ETag", "\"old-etag\""))); + + final CountDownLatch latch = new CountDownLatch(1); + impl.evictInvalidatedEntries(host, request, response, HttpTestUtils.countDown(latch)); + + latch.await(); + + verify(mockStorage).getEntry(Mockito.eq(contentKey), Mockito.any()); + verify(mockStorage, Mockito.never()).removeEntry(Mockito.eq(contentKey), Mockito.any()); + } + + @Test + public void testDoesNotInvalidateUriSpecifiedByContentLocationIfEntryHasNoEtag() throws Exception { + final HttpRequest request = new BasicHttpRequest("PUT", "/foo"); + final URI contentUri = new URIBuilder() + .setHttpHost(host) + .setPath("/bar") + .build(); + final String contentKey = CacheKeyGenerator.INSTANCE.generateKey(contentUri); + + final HttpResponse response = HttpTestUtils.make200Response(); + response.setHeader("ETag", "\"some-etag\""); + response.setHeader("Date", DateUtils.formatStandardDate(now)); + response.setHeader("Content-Location", contentUri.toASCIIString()); + + mockStorage.putEntry(contentKey, HttpTestUtils.makeCacheEntry( + new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)))); + + final CountDownLatch latch = new CountDownLatch(1); + impl.evictInvalidatedEntries(host, request, response, HttpTestUtils.countDown(latch)); + + latch.await(); + + verify(mockStorage).getEntry(Mockito.eq(contentKey), Mockito.any()); + verify(mockStorage, Mockito.never()).removeEntry(Mockito.eq(contentKey), Mockito.any()); + } + + @Test + public void testInvalidatesUriSpecifiedByContentLocationIfResponseHasNoDate() throws Exception { + final HttpRequest request = new BasicHttpRequest("PUT", "/foo"); + final URI contentUri = new URIBuilder() + .setHttpHost(host) + .setPath("/bar") + .build(); + final String contentKey = CacheKeyGenerator.INSTANCE.generateKey(contentUri); + + final HttpResponse response = HttpTestUtils.make200Response(); + response.setHeader("ETag", "\"new-etag\""); + response.removeHeaders("Date"); + response.setHeader("Content-Location", contentUri.toASCIIString()); + + mockStorage.putEntry(contentKey, HttpTestUtils.makeCacheEntry( + new BasicHeader("ETag", "\"old-etag\""), + new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)))); + + final CountDownLatch latch = new CountDownLatch(1); + impl.evictInvalidatedEntries(host, request, response, HttpTestUtils.countDown(latch)); + + latch.await(); + + verify(mockStorage).getEntry(Mockito.eq(contentKey), Mockito.any()); + verify(mockStorage).removeEntry(Mockito.eq(contentKey), Mockito.any()); + } + + @Test + public void testInvalidatesUriSpecifiedByContentLocationIfEntryHasNoDate() throws Exception { + final HttpRequest request = new BasicHttpRequest("PUT", "/foo"); + final URI contentUri = new URIBuilder() + .setHttpHost(host) + .setPath("/bar") + .build(); + final String contentKey = CacheKeyGenerator.INSTANCE.generateKey(contentUri); + + final HttpResponse response = HttpTestUtils.make200Response(); + response.setHeader("ETag","\"new-etag\""); + response.setHeader("Date", DateUtils.formatStandardDate(now)); + response.setHeader("Content-Location", contentUri.toASCIIString()); + + mockStorage.putEntry(contentKey, HttpTestUtils.makeCacheEntry( + new BasicHeader("ETag", "\"old-etag\""))); + + final CountDownLatch latch = new CountDownLatch(1); + impl.evictInvalidatedEntries(host, request, response, HttpTestUtils.countDown(latch)); + + latch.await(); + + verify(mockStorage).getEntry(Mockito.eq(contentKey), Mockito.any()); + verify(mockStorage).removeEntry(Mockito.eq(contentKey), Mockito.any()); + } + + @Test + public void testInvalidatesUriSpecifiedByContentLocationIfResponseHasMalformedDate() throws Exception { + final HttpRequest request = new BasicHttpRequest("PUT", "/foo"); + final URI contentUri = new URIBuilder() + .setHttpHost(host) + .setPath("/bar") + .build(); + final String contentKey = CacheKeyGenerator.INSTANCE.generateKey(contentUri); + + final HttpResponse response = HttpTestUtils.make200Response(); + response.setHeader("ETag","\"new-etag\""); + response.setHeader("Date", "huh?"); + response.setHeader("Content-Location", contentUri.toASCIIString()); + + mockStorage.putEntry(contentKey, HttpTestUtils.makeCacheEntry( + new BasicHeader("ETag", "\"old-etag\""), + new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)))); + + final CountDownLatch latch = new CountDownLatch(1); + impl.evictInvalidatedEntries(host, request, response, HttpTestUtils.countDown(latch)); + + latch.await(); + + verify(mockStorage).getEntry(Mockito.eq(contentKey), Mockito.any()); + verify(mockStorage).removeEntry(Mockito.eq(contentKey), Mockito.any()); + } + + @Test + public void testInvalidatesUriSpecifiedByContentLocationIfEntryHasMalformedDate() throws Exception { + final HttpRequest request = new BasicHttpRequest("PUT", "/foo"); + final URI contentUri = new URIBuilder() + .setHttpHost(host) + .setPath("/bar") + .build(); + final String contentKey = CacheKeyGenerator.INSTANCE.generateKey(contentUri); + + final HttpResponse response = HttpTestUtils.make200Response(); + response.setHeader("ETag","\"new-etag\""); + response.setHeader("Date", DateUtils.formatStandardDate(now)); + response.setHeader("Content-Location", contentUri.toASCIIString()); + + mockStorage.putEntry(contentKey, HttpTestUtils.makeCacheEntry( + new BasicHeader("ETag", "\"old-etag\""), + new BasicHeader("Date", "huh?"))); + + final CountDownLatch latch = new CountDownLatch(1); + impl.evictInvalidatedEntries(host, request, response, HttpTestUtils.countDown(latch)); + + latch.await(); + + verify(mockStorage).getEntry(Mockito.eq(contentKey), Mockito.any()); + verify(mockStorage).removeEntry(Mockito.eq(contentKey), Mockito.any()); + } + +} diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestBasicHttpCache.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestBasicHttpCache.java index 24189bb3aa..2986fa607e 100644 --- a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestBasicHttpCache.java +++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestBasicHttpCache.java @@ -32,7 +32,10 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import java.net.URI; import java.time.Instant; import java.util.HashMap; import java.util.List; @@ -40,9 +43,7 @@ import java.util.stream.Collectors; import org.apache.hc.client5.http.cache.HttpCacheEntry; -import org.apache.hc.client5.http.classic.methods.HttpDelete; import org.apache.hc.client5.http.classic.methods.HttpGet; -import org.apache.hc.client5.http.classic.methods.HttpPost; import org.apache.hc.client5.http.utils.DateUtils; import org.apache.hc.core5.http.HttpHeaders; import org.apache.hc.core5.http.HttpHost; @@ -50,76 +51,33 @@ 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.apache.hc.core5.net.URIBuilder; import org.apache.hc.core5.util.ByteArrayBuffer; import org.hamcrest.MatcherAssert; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; public class TestBasicHttpCache { - private BasicHttpCache impl; + private HttpHost host; + private Instant now; + private Instant tenSecondsAgo; private SimpleHttpCacheStorage backing; + private BasicHttpCache impl; @BeforeEach public void setUp() throws Exception { - backing = new SimpleHttpCacheStorage(); + host = new HttpHost("foo.example.com"); + now = Instant.now(); + tenSecondsAgo = now.minusSeconds(10); + backing = Mockito.spy(new SimpleHttpCacheStorage()); impl = new BasicHttpCache(new HeapResourceFactory(), backing); } - @Test - public void testFlushContentLocationEntryIfUnSafeRequest() throws Exception { - final HttpHost host = new HttpHost("foo.example.com"); - final HttpRequest req = new HttpPost("/foo"); - final HttpResponse resp = HttpTestUtils.make200Response(); - resp.setHeader("Content-Location", "/bar"); - resp.setHeader(HttpHeaders.ETAG, "\"etag\""); - final String key = CacheKeyGenerator.INSTANCE.generateKey(host, new HttpGet("/bar")); - - final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry( - new BasicHeader("Date", DateUtils.formatStandardDate(Instant.now())), - new BasicHeader("ETag", "\"old-etag\"")); - - backing.map.put(key, entry); - - impl.flushCacheEntriesInvalidatedByExchange(host, req, resp); - - assertNull(backing.map.get(key)); - } - - @Test - public void testDoNotFlushContentLocationEntryIfSafeRequest() throws Exception { - final HttpHost host = new HttpHost("foo.example.com"); - final HttpRequest req = new HttpGet("/foo"); - final HttpResponse resp = HttpTestUtils.make200Response(); - resp.setHeader("Content-Location", "/bar"); - final String key = CacheKeyGenerator.INSTANCE.generateKey(host, new HttpGet("/bar")); - - final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry( - new BasicHeader("Date", DateUtils.formatStandardDate(Instant.now())), - new BasicHeader("ETag", "\"old-etag\"")); - - backing.map.put(key, entry); - - impl.flushCacheEntriesInvalidatedByExchange(host, req, resp); - - assertEquals(entry, backing.map.get(key)); - } - - @Test - public void testCanFlushCacheEntriesAtUri() throws Exception { - final HttpHost host = new HttpHost("foo.example.com"); - final HttpRequest req = new HttpDelete("/bar"); - final String key = CacheKeyGenerator.INSTANCE.generateKey(host, req); - final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(); - - backing.map.put(key, entry); - - impl.flushCacheEntriesFor(host, req); - - assertNull(backing.map.get(key)); - } - @Test public void testStoreInCachePutsNonVariantEntryInPlace() throws Exception { final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(); @@ -130,7 +88,7 @@ public void testStoreInCachePutsNonVariantEntryInPlace() throws Exception { final String key = CacheKeyGenerator.INSTANCE.generateKey(host, req); - impl.store(req, resp, Instant.now(), Instant.now(), key, entry); + impl.store(req, resp, now, now, key, entry); assertSame(entry, backing.map.get(key)); } @@ -161,20 +119,18 @@ public void testGetCacheEntryFetchesFromCacheOnCacheHitIfNoVariants() throws Exc @Test public void testGetCacheEntryReturnsNullIfNoVariantInCache() throws Exception { - final HttpHost host = new HttpHost("foo.example.com"); - final HttpRequest origRequest = new HttpGet("http://foo.example.com/bar"); origRequest.setHeader("Accept-Encoding","gzip"); final ByteArrayBuffer buf = HttpTestUtils.makeRandomBuffer(128); final HttpResponse origResponse = new BasicHttpResponse(HttpStatus.SC_OK, "OK"); - origResponse.setHeader("Date", DateUtils.formatStandardDate(Instant.now())); + origResponse.setHeader("Date", DateUtils.formatStandardDate(now)); origResponse.setHeader("Cache-Control", "max-age=3600, public"); origResponse.setHeader("ETag", "\"etag\""); origResponse.setHeader("Vary", "Accept-Encoding"); origResponse.setHeader("Content-Encoding","gzip"); - impl.store(host, origRequest, origResponse, buf, Instant.now(), Instant.now()); + impl.store(host, origRequest, origResponse, buf, now, now); final HttpRequest request = new HttpGet("http://foo.example.com/bar"); final CacheMatch result = impl.match(host, request); @@ -184,20 +140,18 @@ public void testGetCacheEntryReturnsNullIfNoVariantInCache() throws Exception { @Test public void testGetCacheEntryReturnsVariantIfPresentInCache() throws Exception { - final HttpHost host = new HttpHost("foo.example.com"); - final HttpRequest origRequest = new HttpGet("http://foo.example.com/bar"); origRequest.setHeader("Accept-Encoding","gzip"); final ByteArrayBuffer buf = HttpTestUtils.makeRandomBuffer(128); final HttpResponse origResponse = new BasicHttpResponse(HttpStatus.SC_OK, "OK"); - origResponse.setHeader("Date", DateUtils.formatStandardDate(Instant.now())); + origResponse.setHeader("Date", DateUtils.formatStandardDate(now)); origResponse.setHeader("Cache-Control", "max-age=3600, public"); origResponse.setHeader("ETag", "\"etag\""); origResponse.setHeader("Vary", "Accept-Encoding"); origResponse.setHeader("Content-Encoding","gzip"); - impl.store(host, origRequest, origResponse, buf, Instant.now(), Instant.now()); + impl.store(host, origRequest, origResponse, buf, now, now); final HttpRequest request = new HttpGet("http://foo.example.com/bar"); request.setHeader("Accept-Encoding","gzip"); @@ -208,8 +162,6 @@ public void testGetCacheEntryReturnsVariantIfPresentInCache() throws Exception { @Test public void testGetCacheEntryReturnsVariantWithMostRecentDateHeader() throws Exception { - final HttpHost host = new HttpHost("foo.example.com"); - final HttpRequest origRequest = new HttpGet("http://foo.example.com/bar"); origRequest.setHeader("Accept-Encoding", "gzip"); @@ -217,20 +169,20 @@ public void testGetCacheEntryReturnsVariantWithMostRecentDateHeader() throws Exc // Create two response variants with different Date headers final HttpResponse origResponse1 = new BasicHttpResponse(HttpStatus.SC_OK, "OK"); - origResponse1.setHeader(HttpHeaders.DATE, DateUtils.formatStandardDate(Instant.now().minusSeconds(3600))); + origResponse1.setHeader(HttpHeaders.DATE, DateUtils.formatStandardDate(now.minusSeconds(3600))); origResponse1.setHeader(HttpHeaders.CACHE_CONTROL, "max-age=3600, public"); origResponse1.setHeader(HttpHeaders.ETAG, "\"etag1\""); origResponse1.setHeader(HttpHeaders.VARY, "Accept-Encoding"); final HttpResponse origResponse2 = new BasicHttpResponse(HttpStatus.SC_OK, "OK"); - origResponse2.setHeader(HttpHeaders.DATE, DateUtils.formatStandardDate(Instant.now())); + origResponse2.setHeader(HttpHeaders.DATE, DateUtils.formatStandardDate(now)); origResponse2.setHeader(HttpHeaders.CACHE_CONTROL, "max-age=3600, public"); origResponse2.setHeader(HttpHeaders.ETAG, "\"etag2\""); origResponse2.setHeader(HttpHeaders.VARY, "Accept-Encoding"); // Store the two variants in cache - impl.store(host, origRequest, origResponse1, buf, Instant.now(), Instant.now()); - impl.store(host, origRequest, origResponse2, buf, Instant.now(), Instant.now()); + impl.store(host, origRequest, origResponse1, buf, now, now); + impl.store(host, origRequest, origResponse2, buf, now, now); final HttpRequest request = new HttpGet("http://foo.example.com/bar"); request.setHeader("Accept-Encoding", "gzip"); @@ -276,7 +228,7 @@ public void testGetVariantCacheEntriesReturnsAllVariants() throws Exception { req1.setHeader("Accept-Encoding", "gzip"); final HttpResponse resp1 = HttpTestUtils.make200Response(); - resp1.setHeader("Date", DateUtils.formatStandardDate(Instant.now())); + resp1.setHeader("Date", DateUtils.formatStandardDate(now)); resp1.setHeader("Cache-Control", "max-age=3600, public"); resp1.setHeader("ETag", "\"etag1\""); resp1.setHeader("Vary", "Accept-Encoding"); @@ -287,15 +239,15 @@ public void testGetVariantCacheEntriesReturnsAllVariants() throws Exception { req2.setHeader("Accept-Encoding", "identity"); final HttpResponse resp2 = HttpTestUtils.make200Response(); - resp2.setHeader("Date", DateUtils.formatStandardDate(Instant.now())); + resp2.setHeader("Date", DateUtils.formatStandardDate(now)); resp2.setHeader("Cache-Control", "max-age=3600, public"); resp2.setHeader("ETag", "\"etag2\""); resp2.setHeader("Vary", "Accept-Encoding"); resp2.setHeader("Content-Encoding","gzip"); resp2.setHeader("Vary", "Accept-Encoding"); - final CacheHit hit1 = impl.store(host, req1, resp1, null, Instant.now(), Instant.now()); - final CacheHit hit2 = impl.store(host, req2, resp2, null, Instant.now(), Instant.now()); + final CacheHit hit1 = impl.store(host, req1, resp1, null, now, now); + final CacheHit hit2 = impl.store(host, req2, resp2, null, now, now); final Map variantMap = new HashMap<>(); variantMap.put("variant-1", hit1.variantKey); @@ -311,4 +263,416 @@ public void testGetVariantCacheEntriesReturnsAllVariants() throws Exception { MatcherAssert.assertThat(variants.get(hit2.getEntryKey()), HttpCacheEntryMatcher.equivalent(hit2.entry)); } + @Test + public void testInvalidatesUnsafeRequests() throws Exception { + final HttpRequest request = new BasicHttpRequest("POST","/path"); + final String key = CacheKeyGenerator.INSTANCE.generateKey(host, request); + + final HttpResponse response = HttpTestUtils.make200Response(); + + backing.putEntry(key, HttpTestUtils.makeCacheEntry()); + + impl.evictInvalidatedEntries(host, request, response); + + verify(backing).getEntry(key); + verify(backing).removeEntry(key); + + Assertions.assertNull(backing.getEntry(key)); + } + + @Test + public void testDoesNotInvalidateSafeRequests() throws Exception { + final HttpRequest request1 = new BasicHttpRequest("GET","/"); + final HttpResponse response1 = HttpTestUtils.make200Response(); + + impl.evictInvalidatedEntries(host, request1, response1); + + verifyNoMoreInteractions(backing); + + final HttpRequest request2 = new BasicHttpRequest("HEAD","/"); + final HttpResponse response2 = HttpTestUtils.make200Response(); + impl.evictInvalidatedEntries(host, request2, response2); + + verifyNoMoreInteractions(backing); + } + + @Test + public void testInvalidatesUnsafeRequestsWithVariants() throws Exception { + final HttpRequest request = new BasicHttpRequest("POST","/path"); + final String rootKey = CacheKeyGenerator.INSTANCE.generateKey(host, request); + final String variantKey1 = "{var1}" + rootKey; + final String variantKey2 = "{var2}" + rootKey; + final Map variantMap = new HashMap<>(); + variantMap.put("{var1}", variantKey1); + variantMap.put("{var2}", variantKey2); + + final HttpResponse response = HttpTestUtils.make200Response(); + + backing.putEntry(rootKey, HttpTestUtils.makeCacheEntry(variantMap)); + backing.putEntry(variantKey1, HttpTestUtils.makeCacheEntry()); + backing.putEntry(variantKey2, HttpTestUtils.makeCacheEntry()); + + impl.evictInvalidatedEntries(host, request, response); + + verify(backing).getEntry(rootKey); + verify(backing).removeEntry(rootKey); + verify(backing).removeEntry(variantKey1); + verify(backing).removeEntry(variantKey2); + + Assertions.assertNull(backing.getEntry(rootKey)); + Assertions.assertNull(backing.getEntry(variantKey1)); + Assertions.assertNull(backing.getEntry(variantKey2)); + } + + @Test + public void testInvalidateUriSpecifiedByContentLocationAndFresher() throws Exception { + final HttpRequest request = new BasicHttpRequest("PUT", "/foo"); + final String rootKey = CacheKeyGenerator.INSTANCE.generateKey(host, request); + final URI contentUri = new URIBuilder() + .setHttpHost(host) + .setPath("/bar") + .build(); + final String contentKey = CacheKeyGenerator.INSTANCE.generateKey(contentUri); + + final HttpResponse response = HttpTestUtils.make200Response(); + response.setHeader("ETag","\"new-etag\""); + response.setHeader("Date", DateUtils.formatStandardDate(now)); + response.setHeader("Content-Location", contentUri.toASCIIString()); + + backing.putEntry(rootKey, HttpTestUtils.makeCacheEntry()); + backing.putEntry(contentKey, HttpTestUtils.makeCacheEntry( + new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)), + new BasicHeader("ETag", "\"old-etag\"") + )); + + impl.evictInvalidatedEntries(host, request, response); + + verify(backing).getEntry(rootKey); + verify(backing).removeEntry(rootKey); + verify(backing).getEntry(contentKey); + verify(backing).removeEntry(contentKey); + } + + @Test + public void testInvalidateUriSpecifiedByLocationAndFresher() throws Exception { + final HttpRequest request = new BasicHttpRequest("PUT", "/foo"); + final String rootKey = CacheKeyGenerator.INSTANCE.generateKey(host, request); + final URI contentUri = new URIBuilder() + .setHttpHost(host) + .setPath("/bar") + .build(); + final String contentKey = CacheKeyGenerator.INSTANCE.generateKey(contentUri); + + final HttpResponse response = HttpTestUtils.make200Response(); + response.setHeader("ETag","\"new-etag\""); + response.setHeader("Date", DateUtils.formatStandardDate(now)); + response.setHeader("Location", contentUri.toASCIIString()); + + backing.putEntry(rootKey, HttpTestUtils.makeCacheEntry()); + backing.putEntry(contentKey, HttpTestUtils.makeCacheEntry( + new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)), + new BasicHeader("ETag", "\"old-etag\"") + )); + + impl.evictInvalidatedEntries(host, request, response); + + verify(backing).getEntry(rootKey); + verify(backing).removeEntry(rootKey); + verify(backing).getEntry(contentKey); + verify(backing).removeEntry(contentKey); + } + + @Test + public void testDoesNotInvalidateForUnsuccessfulResponse() throws Exception { + final HttpRequest request = new BasicHttpRequest("PUT", "/foo"); + final URI contentUri = new URIBuilder() + .setHttpHost(host) + .setPath("/bar") + .build(); + final HttpResponse response = HttpTestUtils.make500Response(); + response.setHeader("ETag","\"new-etag\""); + response.setHeader("Date", DateUtils.formatStandardDate(now)); + response.setHeader("Content-Location", contentUri.toASCIIString()); + + impl.evictInvalidatedEntries(host, request, response); + + verifyNoMoreInteractions(backing); + } + + @Test + public void testInvalidateUriSpecifiedByContentLocationNonCanonical() throws Exception { + final HttpRequest request = new BasicHttpRequest("PUT", "/foo"); + final String rootKey = CacheKeyGenerator.INSTANCE.generateKey(host, request); + final URI contentUri = new URIBuilder() + .setHttpHost(host) + .setPath("/bar") + .build(); + final String contentKey = CacheKeyGenerator.INSTANCE.generateKey(contentUri); + + final HttpResponse response = HttpTestUtils.make200Response(); + response.setHeader("ETag","\"new-etag\""); + response.setHeader("Date", DateUtils.formatStandardDate(now)); + + response.setHeader("Content-Location", contentUri.toASCIIString()); + + backing.putEntry(rootKey, HttpTestUtils.makeCacheEntry()); + + backing.putEntry(contentKey, HttpTestUtils.makeCacheEntry( + new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)), + new BasicHeader("ETag", "\"old-etag\""))); + + impl.evictInvalidatedEntries(host, request, response); + + verify(backing).getEntry(rootKey); + verify(backing).removeEntry(rootKey); + verify(backing).getEntry(contentKey); + verify(backing).removeEntry(contentKey); + Assertions.assertNull(backing.getEntry(rootKey)); + Assertions.assertNull(backing.getEntry(contentKey)); + } + + @Test + public void testInvalidateUriSpecifiedByContentLocationRelative() throws Exception { + final HttpRequest request = new BasicHttpRequest("PUT", "/foo"); + final String rootKey = CacheKeyGenerator.INSTANCE.generateKey(host, request); + final URI contentUri = new URIBuilder() + .setHttpHost(host) + .setPath("/bar") + .build(); + final String contentKey = CacheKeyGenerator.INSTANCE.generateKey(contentUri); + + final HttpResponse response = HttpTestUtils.make200Response(); + response.setHeader("ETag","\"new-etag\""); + response.setHeader("Date", DateUtils.formatStandardDate(now)); + + response.setHeader("Content-Location", "/bar"); + + backing.putEntry(rootKey, HttpTestUtils.makeCacheEntry()); + + backing.putEntry(contentKey, HttpTestUtils.makeCacheEntry( + new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)), + new BasicHeader("ETag", "\"old-etag\""))); + + impl.evictInvalidatedEntries(host, request, response); + + verify(backing).getEntry(rootKey); + verify(backing).removeEntry(rootKey); + verify(backing).getEntry(contentKey); + verify(backing).removeEntry(contentKey); + Assertions.assertNull(backing.getEntry(rootKey)); + Assertions.assertNull(backing.getEntry(contentKey)); + } + + @Test + public void testDoesNotInvalidateUriSpecifiedByContentLocationOtherOrigin() throws Exception { + final HttpRequest request = new BasicHttpRequest("PUT", "/"); + final URI contentUri = new URIBuilder() + .setHost("bar.example.com") + .setPath("/") + .build(); + final String contentKey = CacheKeyGenerator.INSTANCE.generateKey(contentUri); + + final HttpResponse response = HttpTestUtils.make200Response(); + response.setHeader("ETag","\"new-etag\""); + response.setHeader("Date", DateUtils.formatStandardDate(now)); + response.setHeader("Content-Location", contentUri.toASCIIString()); + + backing.putEntry(contentKey, HttpTestUtils.makeCacheEntry()); + + impl.evictInvalidatedEntries(host, request, response); + + verify(backing, Mockito.never()).getEntry(contentKey); + verify(backing, Mockito.never()).removeEntry(contentKey); + } + + @Test + public void testDoesNotInvalidateUriSpecifiedByContentLocationIfEtagsMatch() throws Exception { + final HttpRequest request = new BasicHttpRequest("PUT", "/foo"); + final URI contentUri = new URIBuilder() + .setHttpHost(host) + .setPath("/bar") + .build(); + final String contentKey = CacheKeyGenerator.INSTANCE.generateKey(contentUri); + + final HttpResponse response = HttpTestUtils.make200Response(); + response.setHeader("ETag","\"same-etag\""); + response.setHeader("Date", DateUtils.formatStandardDate(now)); + response.setHeader("Content-Location", contentUri.toASCIIString()); + + backing.putEntry(contentKey, HttpTestUtils.makeCacheEntry( + new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)), + new BasicHeader("ETag", "\"same-etag\""))); + + impl.evictInvalidatedEntries(host, request, response); + + verify(backing).getEntry(contentKey); + verify(backing, Mockito.never()).removeEntry(contentKey); + } + + @Test + public void testDoesNotInvalidateUriSpecifiedByContentLocationIfOlder() throws Exception { + final HttpRequest request = new BasicHttpRequest("PUT", "/foo"); + final URI contentUri = new URIBuilder() + .setHttpHost(host) + .setPath("/bar") + .build(); + final String contentKey = CacheKeyGenerator.INSTANCE.generateKey(contentUri); + + final HttpResponse response = HttpTestUtils.make200Response(); + response.setHeader("ETag","\"new-etag\""); + response.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)); + response.setHeader("Content-Location", contentUri.toASCIIString()); + + backing.putEntry(contentKey, HttpTestUtils.makeCacheEntry( + new BasicHeader("Date", DateUtils.formatStandardDate(now)), + new BasicHeader("ETag", "\"old-etag\""))); + + impl.evictInvalidatedEntries(host, request, response); + + verify(backing).getEntry(contentKey); + verify(backing, Mockito.never()).removeEntry(contentKey); + } + + @Test + public void testDoesNotInvalidateUriSpecifiedByContentLocationIfResponseHasNoEtag() throws Exception { + final HttpRequest request = new BasicHttpRequest("PUT", "/foo"); + final URI contentUri = new URIBuilder() + .setHttpHost(host) + .setPath("/bar") + .build(); + final String contentKey = CacheKeyGenerator.INSTANCE.generateKey(contentUri); + + final HttpResponse response = HttpTestUtils.make200Response(); + response.removeHeaders("ETag"); + response.setHeader("Date", DateUtils.formatStandardDate(now)); + response.setHeader("Content-Location", contentUri.toASCIIString()); + + backing.putEntry(contentKey, HttpTestUtils.makeCacheEntry( + new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)), + new BasicHeader("ETag", "\"old-etag\""))); + + impl.evictInvalidatedEntries(host, request, response); + + verify(backing).getEntry(contentKey); + verify(backing, Mockito.never()).removeEntry(contentKey); + } + + @Test + public void testDoesNotInvalidateUriSpecifiedByContentLocationIfEntryHasNoEtag() throws Exception { + final HttpRequest request = new BasicHttpRequest("PUT", "/foo"); + final URI contentUri = new URIBuilder() + .setHttpHost(host) + .setPath("/bar") + .build(); + final String contentKey = CacheKeyGenerator.INSTANCE.generateKey(contentUri); + + final HttpResponse response = HttpTestUtils.make200Response(); + response.setHeader("ETag", "\"some-etag\""); + response.setHeader("Date", DateUtils.formatStandardDate(now)); + response.setHeader("Content-Location", contentUri.toASCIIString()); + + backing.putEntry(contentKey, HttpTestUtils.makeCacheEntry( + new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)))); + + impl.evictInvalidatedEntries(host, request, response); + + verify(backing).getEntry(contentKey); + verify(backing, Mockito.never()).removeEntry(contentKey); + } + + @Test + public void testInvalidatesUriSpecifiedByContentLocationIfResponseHasNoDate() throws Exception { + final HttpRequest request = new BasicHttpRequest("PUT", "/foo"); + final URI contentUri = new URIBuilder() + .setHttpHost(host) + .setPath("/bar") + .build(); + final String contentKey = CacheKeyGenerator.INSTANCE.generateKey(contentUri); + + final HttpResponse response = HttpTestUtils.make200Response(); + response.setHeader("ETag", "\"new-etag\""); + response.removeHeaders("Date"); + response.setHeader("Content-Location", contentUri.toASCIIString()); + + backing.putEntry(contentKey, HttpTestUtils.makeCacheEntry( + new BasicHeader("ETag", "\"old-etag\""), + new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)))); + + impl.evictInvalidatedEntries(host, request, response); + + verify(backing).getEntry(contentKey); + verify(backing).removeEntry(contentKey); + } + + @Test + public void testInvalidatesUriSpecifiedByContentLocationIfEntryHasNoDate() throws Exception { + final HttpRequest request = new BasicHttpRequest("PUT", "/foo"); + final URI contentUri = new URIBuilder() + .setHttpHost(host) + .setPath("/bar") + .build(); + final String contentKey = CacheKeyGenerator.INSTANCE.generateKey(contentUri); + + final HttpResponse response = HttpTestUtils.make200Response(); + response.setHeader("ETag","\"new-etag\""); + response.setHeader("Date", DateUtils.formatStandardDate(now)); + response.setHeader("Content-Location", contentUri.toASCIIString()); + + backing.putEntry(contentKey, HttpTestUtils.makeCacheEntry( + new BasicHeader("ETag", "\"old-etag\""))); + + impl.evictInvalidatedEntries(host, request, response); + + verify(backing).getEntry(contentKey); + verify(backing).removeEntry(contentKey); + } + + @Test + public void testInvalidatesUriSpecifiedByContentLocationIfResponseHasMalformedDate() throws Exception { + final HttpRequest request = new BasicHttpRequest("PUT", "/foo"); + final URI contentUri = new URIBuilder() + .setHttpHost(host) + .setPath("/bar") + .build(); + final String contentKey = CacheKeyGenerator.INSTANCE.generateKey(contentUri); + + final HttpResponse response = HttpTestUtils.make200Response(); + response.setHeader("ETag","\"new-etag\""); + response.setHeader("Date", "huh?"); + response.setHeader("Content-Location", contentUri.toASCIIString()); + + backing.putEntry(contentKey, HttpTestUtils.makeCacheEntry( + new BasicHeader("ETag", "\"old-etag\""), + new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)))); + + impl.evictInvalidatedEntries(host, request, response); + + verify(backing).getEntry(contentKey); + verify(backing).removeEntry(contentKey); + } + + @Test + public void testInvalidatesUriSpecifiedByContentLocationIfEntryHasMalformedDate() throws Exception { + final HttpRequest request = new BasicHttpRequest("PUT", "/foo"); + final URI contentUri = new URIBuilder() + .setHttpHost(host) + .setPath("/bar") + .build(); + final String contentKey = CacheKeyGenerator.INSTANCE.generateKey(contentUri); + + final HttpResponse response = HttpTestUtils.make200Response(); + response.setHeader("ETag","\"new-etag\""); + response.setHeader("Date", DateUtils.formatStandardDate(now)); + response.setHeader("Content-Location", contentUri.toASCIIString()); + + backing.putEntry(contentKey, HttpTestUtils.makeCacheEntry( + new BasicHeader("ETag", "\"old-etag\""), + new BasicHeader("Date", "huh?"))); + + impl.evictInvalidatedEntries(host, request, response); + + verify(backing).getEntry(contentKey); + verify(backing).removeEntry(contentKey); + } + } 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 deleted file mode 100644 index 08ab5d0b62..0000000000 --- a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestDefaultAsyncCacheInvalidator.java +++ /dev/null @@ -1,685 +0,0 @@ -/* - * ==================================================================== - * 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.time.Instant; -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.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentMatchers; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; -import org.mockito.stubbing.Answer; - -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 Instant now; - private Instant tenSecondsAgo; - - @BeforeEach - public void setUp() { - MockitoAnnotations.openMocks(this); - now = Instant.now(); - tenSecondsAgo = now.minusSeconds(10); - - when(cacheKeyResolver.resolve(ArgumentMatchers.any())).thenAnswer((Answer) invocation -> { - final URI uri = invocation.getArgument(0); - return CacheSupport.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.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage, operationCallback); - - verify(mockEntry).getVariantMap(); - verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any()); - verify(mockStorage).removeEntry(ArgumentMatchers.eq(key), ArgumentMatchers.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.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage, operationCallback); - - verify(mockEntry).getVariantMap(); - verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any()); - verify(mockStorage).removeEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any()); - verify(mockStorage).removeEntry(ArgumentMatchers.eq("http://foo.example.com:80/content"), ArgumentMatchers.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.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage, operationCallback); - - verify(mockEntry).getVariantMap(); - verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any()); - verify(mockStorage).removeEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any()); - verify(mockStorage).removeEntry(ArgumentMatchers.eq("http://foo.example.com:80/content"), ArgumentMatchers.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.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage, operationCallback); - - verify(mockEntry).getVariantMap(); - verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any()); - verify(mockStorage).removeEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any()); - verify(mockStorage).removeEntry(ArgumentMatchers.eq("http://foo.example.com:80/content"), ArgumentMatchers.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.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage, operationCallback); - - verify(mockEntry).getVariantMap(); - verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any()); - verify(mockStorage).removeEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any()); - } - - @Test - public void testDoesNotInvalidateGETRequest() throws Exception { - final HttpRequest request = new BasicHttpRequest("GET","/"); - impl.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage, operationCallback); - - verify(mockStorage).getEntry(ArgumentMatchers.eq("http://foo.example.com:80/"), ArgumentMatchers.any()); - verifyNoMoreInteractions(mockStorage); - } - - @Test - public void testDoesNotInvalidateHEADRequest() throws Exception { - final HttpRequest request = new BasicHttpRequest("HEAD","/"); - impl.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage, operationCallback); - - verify(mockStorage).getEntry(ArgumentMatchers.eq("http://foo.example.com:80/"), ArgumentMatchers.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.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage, operationCallback); - - verify(mockEntry).getRequestMethod(); - verify(mockEntry).getVariantMap(); - verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any()); - verify(mockStorage).removeEntry(ArgumentMatchers.eq(key), ArgumentMatchers.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.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage, operationCallback); - - verify(mockEntry).getRequestMethod(); - verify(mockEntry).getVariantMap(); - verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any()); - verify(mockStorage).removeEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any()); - verify(mockStorage).removeEntry(ArgumentMatchers.eq(theVariantURI), ArgumentMatchers.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.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage, operationCallback); - - verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.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.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage, operationCallback); - - verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.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.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage, operationCallback); - - verify(mockEntry).getRequestMethod(); - verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any()); - verifyNoMoreInteractions(mockStorage); - } - - @Test - public void testDoesNotInvalidateRequestsWithClientCacheControlHeaders() throws Exception { - final HttpRequest request = new BasicHttpRequest("GET","/"); - request.setHeader("Cache-Control","no-cache"); - - impl.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage, operationCallback); - - verify(mockStorage).getEntry(ArgumentMatchers.eq("http://foo.example.com:80/"), ArgumentMatchers.any()); - verifyNoMoreInteractions(mockStorage); - } - - @Test - public void testDoesNotInvalidateRequestsWithClientPragmaHeaders() throws Exception { - final HttpRequest request = new BasicHttpRequest("GET","/"); - request.setHeader("Pragma","no-cache"); - - impl.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage, operationCallback); - - verify(mockStorage).getEntry(ArgumentMatchers.eq("http://foo.example.com:80/"), ArgumentMatchers.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.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage, operationCallback); - - verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any()); - verify(mockEntry).getVariantMap(); - verify(mockStorage).removeEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any()); - verify(mockStorage).removeEntry(ArgumentMatchers.eq(variantUri), ArgumentMatchers.any()); - } - - @Test - public void doesNotFlushForResponsesWithoutContentLocation() throws Exception { - final HttpRequest request = new BasicHttpRequest("POST","/"); - final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_OK); - impl.flushCacheEntriesInvalidatedByExchange(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.formatStandardDate(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.formatStandardDate(tenSecondsAgo)), - new BasicHeader("ETag", "\"old-etag\"") - }); - - cacheReturnsEntryForUri(key, entry); - - impl.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage, operationCallback); - - verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any()); - verify(mockStorage).removeEntry(ArgumentMatchers.eq(key), ArgumentMatchers.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.formatStandardDate(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.formatStandardDate(tenSecondsAgo)), - new BasicHeader("ETag", "\"old-etag\"") - }); - - cacheReturnsEntryForUri(key, entry); - - impl.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage, operationCallback); - - verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any()); - verify(mockStorage).removeEntry(ArgumentMatchers.eq(key), ArgumentMatchers.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.formatStandardDate(now)); - final String key = "http://foo.example.com:80/bar"; - response.setHeader("Content-Location", key); - - impl.flushCacheEntriesInvalidatedByExchange(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.formatStandardDate(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.formatStandardDate(tenSecondsAgo)), - new BasicHeader("ETag", "\"old-etag\"") - }); - - cacheReturnsEntryForUri(key, entry); - - impl.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage, operationCallback); - - verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any()); - verify(mockStorage).removeEntry(ArgumentMatchers.eq(key), ArgumentMatchers.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.formatStandardDate(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.formatStandardDate(tenSecondsAgo)), - new BasicHeader("ETag", "\"old-etag\"") - }); - - cacheReturnsEntryForUri(key, entry); - - impl.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage, operationCallback); - - verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any()); - verify(mockStorage).removeEntry(ArgumentMatchers.eq(key), ArgumentMatchers.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.formatStandardDate(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.formatStandardDate(tenSecondsAgo)), - new BasicHeader("ETag", "\"old-etag\"") - }); - - cacheReturnsEntryForUri(key, entry); - - impl.flushCacheEntriesInvalidatedByExchange(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.formatStandardDate(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.formatStandardDate(tenSecondsAgo)), - new BasicHeader("ETag", "\"same-etag\"") - }); - - cacheReturnsEntryForUri(key, entry); - impl.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage, operationCallback); - - verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.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.formatStandardDate(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.formatStandardDate(now)), - new BasicHeader("ETag", "\"old-etag\"") - }); - - cacheReturnsEntryForUri(key, entry); - - impl.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage, operationCallback); - - verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.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.formatStandardDate(now)); - final String key = "http://foo.example.com:80/bar"; - response.setHeader("Content-Location", key); - - cacheReturnsEntryForUri(key, null); - - impl.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage, operationCallback); - - verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.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.formatStandardDate(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.formatStandardDate(tenSecondsAgo)), - new BasicHeader("ETag", "\"old-etag\"") - }); - - cacheReturnsEntryForUri(key, entry); - - impl.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage, operationCallback); - - verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.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.formatStandardDate(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.formatStandardDate(tenSecondsAgo)), - }); - - cacheReturnsEntryForUri(key, entry); - - impl.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage, operationCallback); - - verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.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.formatStandardDate(tenSecondsAgo)), - }); - - cacheReturnsEntryForUri(key, entry); - - impl.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage, operationCallback); - - verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any()); - verify(mockStorage).removeEntry(ArgumentMatchers.eq(key), ArgumentMatchers.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.formatStandardDate(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.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage, operationCallback); - - verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any()); - verify(mockStorage).removeEntry(ArgumentMatchers.eq(key), ArgumentMatchers.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.formatStandardDate(tenSecondsAgo)) - }); - - cacheReturnsEntryForUri(key, entry); - - impl.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage, operationCallback); - - verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any()); - verify(mockStorage).removeEntry(ArgumentMatchers.eq(key), ArgumentMatchers.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.formatStandardDate(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.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage, operationCallback); - - verify(mockStorage).getEntry(ArgumentMatchers.eq(key), ArgumentMatchers.any()); - verify(mockStorage).removeEntry(ArgumentMatchers.eq(key), ArgumentMatchers.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( - ArgumentMatchers.eq(key), - ArgumentMatchers.any())).thenAnswer((Answer) invocation -> { - 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 deleted file mode 100644 index 2a98d2f245..0000000000 --- a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestDefaultCacheInvalidator.java +++ /dev/null @@ -1,666 +0,0 @@ -/* - * ==================================================================== - * 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.io.IOException; -import java.net.URI; -import java.time.Instant; -import java.util.HashMap; -import java.util.Map; - -import org.apache.hc.client5.http.cache.HttpCacheEntry; -import org.apache.hc.client5.http.cache.HttpCacheStorage; -import org.apache.hc.client5.http.utils.DateUtils; -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.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentMatchers; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.mockito.stubbing.Answer; - -public class TestDefaultCacheInvalidator { - - private DefaultCacheInvalidator impl; - private HttpHost host; - @Mock - private HttpCacheEntry mockEntry; - @Mock - private Resolver cacheKeyResolver; - @Mock - private HttpCacheStorage mockStorage; - - private Instant now; - private Instant tenSecondsAgo; - - @BeforeEach - public void setUp() { - MockitoAnnotations.openMocks(this); - now = Instant.now(); - tenSecondsAgo = now.minusSeconds(10); - - when(cacheKeyResolver.resolve(ArgumentMatchers.any())).thenAnswer((Answer) invocation -> { - final URI uri = invocation.getArgument(0); - return CacheSupport.normalize(uri).toASCIIString(); - }); - - host = new HttpHost("foo.example.com"); - impl = new DefaultCacheInvalidator(); - } - - // 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); - - impl.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage); - - verify(mockEntry).getVariantMap(); - verify(mockStorage).getEntry(key); - verify(mockStorage).removeEntry(key); - } - - @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); - - impl.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage); - - verify(mockEntry).getVariantMap(); - verify(mockStorage).getEntry(key); - verify(mockStorage).removeEntry(key); - verify(mockStorage).removeEntry("http://foo.example.com:80/content"); - } - - @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); - - impl.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage); - - verify(mockEntry).getVariantMap(); - verify(mockStorage).getEntry(key); - verify(mockStorage).removeEntry(key); - verify(mockStorage).removeEntry("http://foo.example.com:80/content"); - } - - @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); - - impl.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage); - - verify(mockEntry).getVariantMap(); - verify(mockStorage).getEntry(key); - verify(mockStorage).removeEntry(key); - verify(mockStorage).removeEntry("http://foo.example.com:80/content"); - } - - @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); - - impl.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage); - - verify(mockEntry).getVariantMap(); - verify(mockStorage).getEntry(key); - verify(mockStorage).removeEntry(key); - } - - @Test - public void testDoesNotInvalidateGETRequest() throws Exception { - final HttpRequest request = new BasicHttpRequest("GET","/"); - impl.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage); - - verify(mockStorage).getEntry("http://foo.example.com:80/"); - verifyNoMoreInteractions(mockStorage); - } - - @Test - public void testDoesNotInvalidateHEADRequest() throws Exception { - final HttpRequest request = new BasicHttpRequest("HEAD","/"); - impl.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage); - - verify(mockStorage).getEntry("http://foo.example.com:80/"); - 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); - - impl.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage); - - verify(mockEntry).getRequestMethod(); - verify(mockEntry).getVariantMap(); - verify(mockStorage).getEntry(key); - verify(mockStorage).removeEntry(key); - } - - @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); - - impl.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage); - - verify(mockEntry).getRequestMethod(); - verify(mockEntry).getVariantMap(); - verify(mockStorage).getEntry(key); - verify(mockStorage).removeEntry(key); - verify(mockStorage).removeEntry(theVariantURI); - } - - @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); - - impl.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage); - - verify(mockStorage).getEntry(key); - 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); - - impl.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage); - - verify(mockStorage).getEntry(key); - 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); - - impl.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage); - - verify(mockEntry).getRequestMethod(); - verify(mockStorage).getEntry(key); - verifyNoMoreInteractions(mockStorage); - } - - @Test - public void testDoesNotInvalidateRequestsWithClientCacheControlHeaders() throws Exception { - final HttpRequest request = new BasicHttpRequest("GET","/"); - request.setHeader("Cache-Control","no-cache"); - - impl.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage); - - verify(mockStorage).getEntry("http://foo.example.com:80/"); - verifyNoMoreInteractions(mockStorage); - } - - @Test - public void testDoesNotInvalidateRequestsWithClientPragmaHeaders() throws Exception { - final HttpRequest request = new BasicHttpRequest("GET","/"); - request.setHeader("Pragma","no-cache"); - - impl.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage); - - verify(mockStorage).getEntry("http://foo.example.com:80/"); - 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); - cacheEntryHasVariantMap(mapOfURIs); - - impl.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyResolver, mockStorage); - - verify(mockStorage).getEntry(key); - verify(mockEntry).getVariantMap(); - verify(mockStorage).removeEntry(variantUri); - verify(mockStorage).removeEntry(key); - } - - @Test - public void doesNotFlushForResponsesWithoutContentLocation() throws Exception { - final HttpRequest request = new BasicHttpRequest("POST","/"); - final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_OK); - impl.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage); - - 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.formatStandardDate(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.formatStandardDate(tenSecondsAgo)), - new BasicHeader("ETag", "\"old-etag\"") - }); - - when(mockStorage.getEntry(key)).thenReturn(entry); - - impl.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage); - - verify(mockStorage).getEntry(key); - verify(mockStorage).removeEntry(key); - } - - @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.formatStandardDate(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.formatStandardDate(tenSecondsAgo)), - new BasicHeader("ETag", "\"old-etag\"") - }); - - when(mockStorage.getEntry(key)).thenReturn(entry); - - impl.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage); - - verify(mockStorage).getEntry(key); - verify(mockStorage).removeEntry(key); - } - - @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.formatStandardDate(now)); - final String key = "http://foo.example.com:80/bar"; - response.setHeader("Content-Location", key); - - impl.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage); - - 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.formatStandardDate(now)); - final String cacheKey = "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.formatStandardDate(tenSecondsAgo)), - new BasicHeader("ETag", "\"old-etag\"") - }); - - when(mockStorage.getEntry(cacheKey)).thenReturn(entry); - - impl.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage); - - verify(mockStorage).getEntry(cacheKey); - verify(mockStorage).removeEntry(cacheKey); - } - - @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.formatStandardDate(now)); - final String cacheKey = "http://foo.example.com:80/bar"; - response.setHeader("Content-Location", "/bar"); - - final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(new Header[] { - new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)), - new BasicHeader("ETag", "\"old-etag\"") - }); - - when(mockStorage.getEntry(cacheKey)).thenReturn(entry); - - impl.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage); - - verify(mockStorage).getEntry(cacheKey); - verify(mockStorage).removeEntry(cacheKey); - } - - @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.formatStandardDate(now)); - final String cacheKey = "http://baz.example.com:80/bar"; - response.setHeader("Content-Location", cacheKey); - - impl.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage); - - 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.formatStandardDate(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.formatStandardDate(tenSecondsAgo)), - new BasicHeader("ETag", "\"same-etag\"") - }); - - when(mockStorage.getEntry(key)).thenReturn(entry); - impl.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage); - - verify(mockStorage).getEntry(key); - 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.formatStandardDate(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.formatStandardDate(now)), - new BasicHeader("ETag", "\"old-etag\"") - }); - - when(mockStorage.getEntry(key)).thenReturn(entry); - - impl.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage); - - verify(mockStorage).getEntry(key); - 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.formatStandardDate(now)); - final String key = "http://foo.example.com:80/bar"; - response.setHeader("Content-Location", key); - - when(mockStorage.getEntry(key)).thenReturn(null); - - impl.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage); - - verify(mockStorage).getEntry(key); - 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.formatStandardDate(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.formatStandardDate(tenSecondsAgo)), - new BasicHeader("ETag", "\"old-etag\"") - }); - - when(mockStorage.getEntry(key)).thenReturn(entry); - - impl.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage); - - verify(mockStorage).getEntry(key); - 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.formatStandardDate(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.formatStandardDate(tenSecondsAgo)), - }); - - when(mockStorage.getEntry(key)).thenReturn(entry); - - impl.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage); - - verify(mockStorage).getEntry(key); - 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.formatStandardDate(tenSecondsAgo)), - }); - - when(mockStorage.getEntry(key)).thenReturn(entry); - - impl.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage); - - verify(mockStorage).getEntry(key); - verify(mockStorage).removeEntry(key); - 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.formatStandardDate(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\"") - }); - - when(mockStorage.getEntry(key)).thenReturn(entry); - - impl.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage); - - verify(mockStorage).getEntry(key); - verify(mockStorage).removeEntry(key); - 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.formatStandardDate(tenSecondsAgo)) - }); - - when(mockStorage.getEntry(key)).thenReturn(entry); - - impl.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage); - - verify(mockStorage).getEntry(key); - verify(mockStorage).removeEntry(key); - 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.formatStandardDate(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") - }); - - when(mockStorage.getEntry(key)).thenReturn(entry); - - impl.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyResolver, mockStorage); - - verify(mockStorage).getEntry(key); - verify(mockStorage).removeEntry(key); - verifyNoMoreInteractions(mockStorage); - } - - - // Expectations - private void cacheEntryHasVariantMap(final Map variantMap) { - when(mockEntry.getVariantMap()).thenReturn(variantMap); - } - - private void cacheReturnsEntryForUri(final String key) throws IOException { - when(mockStorage.getEntry(key)).thenReturn(mockEntry); - } - - private void cacheEntryisForMethod(final String httpMethod) { - when(mockEntry.getRequestMethod()).thenReturn(httpMethod); - } -}