HTTPCLIENT-2277: Revision and simplification of the cache eviction logic; conformance to RFC 9111 section 4.4

This commit is contained in:
Oleg Kalnichevski 2023-06-27 14:46:15 +02:00
parent a1d9d19b5b
commit 9bde706ae7
21 changed files with 1331 additions and 2139 deletions

View File

@ -41,7 +41,10 @@ import org.apache.hc.core5.http.HttpResponse;
* that this exchange would invalidate.
*
* @since 5.0
*
* @deprecated Do not use.
*/
@Deprecated
@Internal
public interface HttpAsyncCacheInvalidator {

View File

@ -41,7 +41,10 @@ import org.apache.hc.core5.http.HttpResponse;
* that this exchange would invalidate.
*
* @since 4.3
*
* @deprecated Do not use.
*/
@Deprecated
@Contract(threading = ThreadingBehavior.STATELESS)
@Internal
public interface HttpCacheInvalidator {

View File

@ -65,7 +65,6 @@ 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.impl.BasicEntityDetails;
import org.apache.hc.core5.http.nio.AsyncDataConsumer;
import org.apache.hc.core5.http.nio.AsyncEntityProducer;
@ -237,27 +236,7 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
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<Boolean>() {
@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<CacheMatch>() {
@Override
@ -291,6 +270,9 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
}));
} else {
LOG.debug("Request is not servable from cache");
callBackend(target, request, entityProducer, scope, chain, asyncExecCallback);
}
}
@ -479,7 +461,7 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
final HttpResponse backendResponse,
final EntityDetails entityDetails) throws HttpException, IOException {
responseCompliance.ensureProtocolCompliance(scope.originalRequest, request, backendResponse);
responseCache.flushCacheEntriesInvalidatedByExchange(target, request, backendResponse, new FutureCallback<Boolean>() {
responseCache.evictInvalidatedEntries(target, request, backendResponse, new FutureCallback<Boolean>() {
@Override
public void completed(final Boolean result) {
@ -502,24 +484,6 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler
storeRequestIfModifiedSinceFor304Response(request, backendResponse);
} else {
LOG.debug("Backend response is not cacheable");
if (!Method.isSafe(request.getMethod())) {
responseCache.flushCacheEntriesFor(target, request, new FutureCallback<Boolean>() {
@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) {

View File

@ -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.client5.http.impl.Operations;
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 @@ class BasicHttpAsyncCache implements HttpAsyncCache {
return store(request, originResponse, requestSent, responseReceived, rootKey, hit.entry, callback);
}
@Override
public Cancellable flushCacheEntriesFor(
final HttpHost host, final HttpRequest request, final FutureCallback<Boolean> callback) {
final String rootKey = cacheKeyGenerator.generateKey(host, request);
if (LOG.isDebugEnabled()) {
LOG.debug("Flush cache entries: {}", rootKey);
}
return storage.removeEntry(rootKey, new FutureCallback<Boolean>() {
private void evictEntry(final String cacheKey) {
storage.removeEntry(cacheKey, new FutureCallback<Boolean>() {
@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();
}
});
}
private void evictAll(final HttpCacheEntry root, final String rootKey) {
if (LOG.isDebugEnabled()) {
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);
}
}
}
private Cancellable evict(final String rootKey) {
return storage.getEntry(rootKey, new FutureCallback<HttpCacheEntry>() {
@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<HttpCacheEntry>() {
@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 flushCacheEntriesInvalidatedByRequest(
final HttpHost host, final HttpRequest request, final FutureCallback<Boolean> callback) {
if (LOG.isDebugEnabled()) {
LOG.debug("Flush cache entries invalidated by request: {}; {}", host, new RequestLine(request));
}
return cacheInvalidator.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyGenerator, storage, callback);
}
@Override
public Cancellable flushCacheEntriesInvalidatedByExchange(
public Cancellable evictInvalidatedEntries(
final HttpHost host, final HttpRequest request, final HttpResponse response, final FutureCallback<Boolean> 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();

View File

@ -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 @@ class BasicHttpCache implements HttpCache {
}
}
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 @@ class BasicHttpCache implements HttpCache {
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);
}
}
}
}

View File

@ -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
* <http://www.apache.org/>.
*
*/
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);
}
}

View File

@ -29,6 +29,7 @@ package org.apache.hc.client5.http.impl.cache;
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.FormattedHeader;
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;
@ -170,4 +172,24 @@ public final class CacheSupport {
}
}
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());
}
}

View File

@ -57,7 +57,6 @@ import org.apache.hc.core5.http.HttpHost;
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 @@ class CachingExec extends CachingExecBase implements ExecChainHandler {
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 @@ class CachingExec extends CachingExecBase implements ExecChainHandler {
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 @@ class CachingExec extends CachingExecBase implements ExecChainHandler {
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;
}

View File

@ -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 class CachingH2AsyncClientBuilder extends H2AsyncClientBuilder {
return this;
}
/**
* @deprecated Do not use.
*/
@Deprecated
public final CachingH2AsyncClientBuilder setHttpCacheInvalidator(final HttpAsyncCacheInvalidator cacheInvalidator) {
this.httpCacheInvalidator = cacheInvalidator;
return this;
}
@ -140,8 +142,7 @@ public class CachingH2AsyncClientBuilder extends H2AsyncClientBuilder {
resourceFactoryCopy,
HttpCacheEntryFactory.INSTANCE,
storageCopy,
CacheKeyGenerator.INSTANCE,
this.httpCacheInvalidator != null ? this.httpCacheInvalidator : new DefaultAsyncCacheInvalidator());
CacheKeyGenerator.INSTANCE);
DefaultAsyncCacheRevalidator cacheRevalidator = null;
if (config.getAsynchronousWorkers() > 0) {

View File

@ -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 class CachingHttpAsyncClientBuilder extends HttpAsyncClientBuilder {
return this;
}
/**
* @deprecated Do not use.
*/
@Deprecated
public final CachingHttpAsyncClientBuilder setHttpCacheInvalidator(final HttpAsyncCacheInvalidator cacheInvalidator) {
this.httpCacheInvalidator = cacheInvalidator;
return this;
}
@ -140,8 +142,7 @@ public class CachingHttpAsyncClientBuilder extends HttpAsyncClientBuilder {
resourceFactoryCopy,
HttpCacheEntryFactory.INSTANCE,
storageCopy,
CacheKeyGenerator.INSTANCE,
this.httpCacheInvalidator != null ? this.httpCacheInvalidator : new DefaultAsyncCacheInvalidator());
CacheKeyGenerator.INSTANCE);
DefaultAsyncCacheRevalidator cacheRevalidator = null;
if (config.getAsynchronousWorkers() > 0) {

View File

@ -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 class CachingHttpClientBuilder extends HttpClientBuilder {
return this;
}
/**
* @deprecated Do not use.
*/
@Deprecated
public final CachingHttpClientBuilder setHttpCacheInvalidator(final HttpCacheInvalidator cacheInvalidator) {
this.httpCacheInvalidator = cacheInvalidator;
return this;
}
@ -132,8 +134,7 @@ public class CachingHttpClientBuilder extends HttpClientBuilder {
resourceFactoryCopy,
HttpCacheEntryFactory.INSTANCE,
storageCopy,
CacheKeyGenerator.INSTANCE,
this.httpCacheInvalidator != null ? this.httpCacheInvalidator : new DefaultCacheInvalidator());
CacheKeyGenerator.INSTANCE);
DefaultCacheRevalidator cacheRevalidator = null;
if (config.getAsynchronousWorkers() > 0) {

View File

@ -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
* <http://www.apache.org/>.
*
*/
package org.apache.hc.client5.http.impl.cache;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.apache.hc.client5.http.cache.HttpAsyncCacheInvalidator;
import org.apache.hc.client5.http.cache.HttpAsyncCacheStorage;
import org.apache.hc.client5.http.cache.HttpCacheEntry;
import org.apache.hc.client5.http.impl.Operations;
import org.apache.hc.client5.http.utils.URIUtils;
import org.apache.hc.core5.annotation.Contract;
import org.apache.hc.core5.annotation.Internal;
import org.apache.hc.core5.annotation.ThreadingBehavior;
import org.apache.hc.core5.concurrent.Cancellable;
import org.apache.hc.core5.concurrent.FutureCallback;
import org.apache.hc.core5.function.Resolver;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.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<Boolean>() {
@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<URI, String> cacheKeyResolver,
final HttpAsyncCacheStorage storage,
final FutureCallback<Boolean> 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<HttpCacheEntry>() {
@Override
public void completed(final HttpCacheEntry parentEntry) {
if (requestShouldNotBeCached(request) || shouldInvalidateHeadCacheEntry(request, parentEntry)) {
if (parentEntry != null) {
if (LOG.isDebugEnabled()) {
LOG.debug("Invalidating parentEntry cache entry with key {}", cacheKey);
}
for (final String variantURI : parentEntry.getVariantMap().values()) {
removeEntry(storage, variantURI);
}
removeEntry(storage, cacheKey);
}
if (uri != null) {
if (LOG.isWarnEnabled()) {
LOG.warn("{} 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<URI, String> cacheKeyResolver,
final HttpAsyncCacheStorage storage) {
final URI resolvedUri = uri != null ? URIUtils.resolve(requestUri, uri) : null;
if (resolvedUri != null && isSameHost(requestUri, resolvedUri)) {
removeEntry(storage, cacheKeyResolver.resolve(resolvedUri));
}
}
private boolean flushAbsoluteUriFromSameHost(
final URI requestUri,
final URI uri,
final Resolver<URI, String> cacheKeyResolver,
final HttpAsyncCacheStorage storage) {
if (uri != null && isSameHost(requestUri, uri)) {
removeEntry(storage, cacheKeyResolver.resolve(uri));
return true;
}
return false;
}
@Override
public Cancellable flushCacheEntriesInvalidatedByExchange(
final HttpHost host,
final HttpRequest request,
final HttpResponse response,
final Resolver<URI, String> cacheKeyResolver,
final HttpAsyncCacheStorage storage,
final FutureCallback<Boolean> callback) {
final int status = response.getCode();
if (status >= HttpStatus.SC_SUCCESS && status < HttpStatus.SC_REDIRECTION) {
final String s = CacheSupport.getRequestUri(request, host);
final URI requestUri = CacheSupport.normalize(s);
if (requestUri != null) {
final List<String> cacheKeys = new ArrayList<>(2);
final URI contentLocation = getContentLocationURI(requestUri, response);
if (contentLocation != null && isSameHost(requestUri, contentLocation)) {
cacheKeys.add(cacheKeyResolver.resolve(contentLocation));
}
final URI location = getLocationURI(requestUri, response);
if (location != null && isSameHost(requestUri, location)) {
cacheKeys.add(cacheKeyResolver.resolve(location));
}
if (cacheKeys.size() == 1) {
final String key = cacheKeys.get(0);
storage.getEntry(key, new FutureCallback<HttpCacheEntry>() {
@Override
public void completed(final HttpCacheEntry entry) {
if (entry != null) {
// do not invalidate if response is strictly older than entry
// or if the etags match
if (!responseDateOlderThanEntryDate(response, entry) && responseAndEntryEtagsDiffer(response, entry)) {
removeEntry(storage, key);
}
}
callback.completed(Boolean.TRUE);
}
@Override
public void failed(final Exception ex) {
callback.failed(ex);
}
@Override
public void cancelled() {
callback.cancelled();
}
});
} else if (cacheKeys.size() > 1) {
storage.getEntries(cacheKeys, new FutureCallback<Map<String, HttpCacheEntry>>() {
@Override
public void completed(final Map<String, HttpCacheEntry> resultMap) {
for (final Map.Entry<String, HttpCacheEntry> resultEntry: resultMap.entrySet()) {
// do not invalidate if response is strictly older than entry
// or if the etags match
final String key = resultEntry.getKey();
final HttpCacheEntry entry = resultEntry.getValue();
if (!responseDateOlderThanEntryDate(response, entry) && responseAndEntryEtagsDiffer(response, entry)) {
removeEntry(storage, key);
}
}
callback.completed(Boolean.TRUE);
}
@Override
public void failed(final Exception ex) {
callback.failed(ex);
}
@Override
public void cancelled() {
callback.cancelled();
}
});
}
}
}
callback.completed(Boolean.TRUE);
return Operations.nonCancellable();
}
}

View File

@ -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
* <http://www.apache.org/>.
*
*/
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<URI, String> 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<URI, String> 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<URI, String> 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<URI, String> 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<URI, String> 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);
}
}
}
}

View File

@ -99,21 +99,9 @@ interface HttpAsyncCache {
FutureCallback<CacheHit> callback);
/**
* Clear all matching {@link HttpCacheEntry}s.
* Evicts {@link HttpCacheEntry}s invalidated by the given message exchange.
*/
Cancellable flushCacheEntriesFor(
HttpHost host, HttpRequest request, FutureCallback<Boolean> callback);
/**
* Flush {@link HttpCacheEntry}s invalidated by the given request
*/
Cancellable flushCacheEntriesInvalidatedByRequest(
HttpHost host, HttpRequest request, FutureCallback<Boolean> callback);
/**
* Flush {@link HttpCacheEntry}s invalidated by the given message exchange.
*/
Cancellable flushCacheEntriesInvalidatedByExchange(
Cancellable evictInvalidatedEntries(
HttpHost host, HttpRequest request, HttpResponse response, FutureCallback<Boolean> callback);
}

View File

@ -92,18 +92,8 @@ interface HttpCache {
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);
}

View File

@ -29,16 +29,17 @@ package org.apache.hc.client5.http.impl.cache;
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 class HttpTestUtils {
return new BasicClassicHttpResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR, "Internal Server Error");
}
public static Map<String, String> makeDefaultVariantMap(final String key, final String value) {
final Map<String, String> variants = new HashMap<>();
variants.put(key, value);
public static <T> FutureCallback<T> countDown(final CountDownLatch latch) {
return new FutureCallback<T>() {
@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;
}
}

View File

@ -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
* <http://www.apache.org/>.
*
*/
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<String,HttpCacheEntry> map;
public SimpleHttpAsyncCacheStorage() {
map = new HashMap<>();
}
@Override
public Cancellable putEntry(final String key, final HttpCacheEntry entry, final FutureCallback<Boolean> 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<HttpCacheEntry> 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<Boolean> 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<Boolean> 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<String> keys, final FutureCallback<Map<String, HttpCacheEntry>> callback) {
final Map<String, HttpCacheEntry> 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();
}
}

View File

@ -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
* <http://www.apache.org/>.
*
*/
package org.apache.hc.client5.http.impl.cache;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import 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<String, String> 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());
}
}

View File

@ -32,7 +32,10 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
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.Map;
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.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.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 class TestBasicHttpCache {
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 class TestBasicHttpCache {
@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 class TestBasicHttpCache {
@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 class TestBasicHttpCache {
@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 class TestBasicHttpCache {
// 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 class TestBasicHttpCache {
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 class TestBasicHttpCache {
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<String, String> variantMap = new HashMap<>();
variantMap.put("variant-1", hit1.variantKey);
@ -311,4 +263,416 @@ public class TestBasicHttpCache {
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<String, String> 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);
}
}

View File

@ -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
* <http://www.apache.org/>.
*
*/
package org.apache.hc.client5.http.impl.cache;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import java.net.URI;
import java.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<URI, String> cacheKeyResolver;
@Mock
private HttpAsyncCacheStorage mockStorage;
@Mock
private FutureCallback<Boolean> 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<String>) 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<String,String> 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<String, String> 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<String,String> 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<String,String> 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<Cancellable>) invocation -> {
final FutureCallback<HttpCacheEntry> callback = invocation.getArgument(1);
callback.completed(cacheEntry);
return cancellable;
});
}
private void cacheEntryisForMethod(final String httpMethod) {
when(mockEntry.getRequestMethod()).thenReturn(httpMethod);
}
}

View File

@ -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
* <http://www.apache.org/>.
*
*/
package org.apache.hc.client5.http.impl.cache;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import java.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<URI, String> 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<String>) 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<String,String> 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<String, String> 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<String,String> 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<String,String> 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);
}
}