HTTPCLIENT-2277: Revision and optimization of cache key generation

This commit is contained in:
Oleg Kalnichevski 2023-06-21 17:52:57 +02:00
parent 0c79379827
commit abbfd8202a
5 changed files with 240 additions and 308 deletions

View File

@ -28,6 +28,7 @@ package org.apache.hc.client5.http.impl.cache;
import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -90,11 +91,15 @@ class BasicHttpAsyncCache implements HttpAsyncCache {
@Override
public String generateKey(final HttpHost host, final HttpRequest request, final HttpCacheEntry cacheEntry) {
if (cacheEntry == null) {
return cacheKeyGenerator.generateKey(host, request);
} else {
return cacheKeyGenerator.generateKey(host, request, cacheEntry);
final String root = cacheKeyGenerator.generateKey(host, request);
if (cacheEntry != null && cacheEntry.isVariantRoot()) {
final List<String> variantNames = CacheKeyGenerator.variantNames(cacheEntry);
if (!variantNames.isEmpty()) {
final String variantKey = cacheKeyGenerator.generateVariantKey(request, variantNames);
return variantKey + root;
}
}
return root;
}
@Override
@ -104,8 +109,8 @@ class BasicHttpAsyncCache implements HttpAsyncCache {
LOG.debug("Flush cache entries: {}; {}", host, new RequestLine(request));
}
if (!Method.isSafe(request.getMethod())) {
final String cacheKey = cacheKeyGenerator.generateKey(host, request);
return storage.removeEntry(cacheKey, new FutureCallback<Boolean>() {
final String rootKey = cacheKeyGenerator.generateKey(host, request);
return storage.removeEntry(rootKey, new FutureCallback<Boolean>() {
@Override
public void completed(final Boolean result) {
@ -116,7 +121,7 @@ class BasicHttpAsyncCache implements HttpAsyncCache {
public void failed(final Exception ex) {
if (ex instanceof ResourceIOException) {
if (LOG.isWarnEnabled()) {
LOG.warn("I/O error removing cache entry with key {}", cacheKey);
LOG.warn("I/O error removing cache entry with key {}", rootKey);
}
callback.completed(Boolean.TRUE);
} else {
@ -158,18 +163,17 @@ class BasicHttpAsyncCache implements HttpAsyncCache {
}
Cancellable storeInCache(
final HttpHost host,
final HttpRequest request,
final HttpResponse originResponse,
final Instant requestSent,
final Instant responseReceived,
final String cacheKey,
final String rootKey,
final HttpCacheEntry entry,
final FutureCallback<Boolean> callback) {
if (entry.hasVariants()) {
return storeVariantEntry(host, request, originResponse, requestSent, responseReceived, cacheKey, entry, callback);
return storeVariantEntry(request, originResponse, requestSent, responseReceived, rootKey, entry, callback);
} else {
return storeEntry(cacheKey, entry, callback);
return storeEntry(rootKey, entry, callback);
}
}
@ -205,21 +209,21 @@ class BasicHttpAsyncCache implements HttpAsyncCache {
}
Cancellable storeVariantEntry(
final HttpHost host,
final HttpRequest request,
final HttpResponse originResponse,
final Instant requestSent,
final Instant responseReceived,
final String cacheKey,
final String rootKey,
final HttpCacheEntry entry,
final FutureCallback<Boolean> callback) {
final String variantKey = cacheKeyGenerator.generateVariantKey(request, entry);
final String variantCacheKey = cacheKeyGenerator.generateKey(host, request, entry);
final List<String> variantNames = CacheKeyGenerator.variantNames(entry);
final String variantKey = cacheKeyGenerator.generateVariantKey(request, variantNames);
final String variantCacheKey = variantKey + rootKey;
return storage.putEntry(variantCacheKey, entry, new FutureCallback<Boolean>() {
@Override
public void completed(final Boolean result) {
storage.updateEntry(cacheKey,
storage.updateEntry(rootKey,
existing -> {
final Map<String,String> variantMap = existing != null ? new HashMap<>(existing.getVariantMap()) : new HashMap<>();
variantMap.put(variantKey, variantCacheKey);
@ -236,11 +240,11 @@ class BasicHttpAsyncCache implements HttpAsyncCache {
public void failed(final Exception ex) {
if (ex instanceof HttpCacheUpdateException) {
if (LOG.isWarnEnabled()) {
LOG.warn("Cannot update cache entry with key {}", cacheKey);
LOG.warn("Cannot update cache entry with key {}", rootKey);
}
} else if (ex instanceof ResourceIOException) {
if (LOG.isWarnEnabled()) {
LOG.warn("I/O error updating cache entry with key {}", cacheKey);
LOG.warn("I/O error updating cache entry with key {}", rootKey);
}
} else {
callback.failed(ex);
@ -287,8 +291,8 @@ class BasicHttpAsyncCache implements HttpAsyncCache {
if (LOG.isDebugEnabled()) {
LOG.debug("Re-use variant entry: {}; {} / {}", host, new RequestLine(request), entry);
}
final String cacheKey = cacheKeyGenerator.generateKey(host, request);
return storeVariantEntry(host, request, originResponse, requestSent, responseReceived, cacheKey, entry, callback);
final String rootKey = cacheKeyGenerator.generateKey(host, request);
return storeVariantEntry(request, originResponse, requestSent, responseReceived, rootKey, entry, callback);
}
@Override
@ -303,19 +307,18 @@ class BasicHttpAsyncCache implements HttpAsyncCache {
if (LOG.isDebugEnabled()) {
LOG.debug("Update cache entry: {}; {}", host, new RequestLine(request));
}
final String cacheKey = cacheKeyGenerator.generateKey(host, request);
final String rootKey = cacheKeyGenerator.generateKey(host, request);
final HttpCacheEntry updatedEntry = cacheEntryFactory.createUpdated(
requestSent,
responseReceived,
originResponse,
stale);
return storeInCache(
host,
request,
originResponse,
requestSent,
responseReceived,
cacheKey,
rootKey,
updatedEntry,
new FutureCallback<Boolean>() {
@ -349,13 +352,13 @@ class BasicHttpAsyncCache implements HttpAsyncCache {
if (LOG.isDebugEnabled()) {
LOG.debug("Update variant cache entry: {}; {} / {}", host, new RequestLine(request), entry);
}
final String cacheKey = cacheKeyGenerator.generateKey(host, request);
final String rootKey = cacheKeyGenerator.generateKey(host, request);
final HttpCacheEntry updatedEntry = cacheEntryFactory.createUpdated(
requestSent,
responseReceived,
originResponse,
entry);
return storeEntry(cacheKey, updatedEntry, new FutureCallback<Boolean>() {
return storeEntry(rootKey, updatedEntry, new FutureCallback<Boolean>() {
@Override
public void completed(final Boolean result) {
@ -387,17 +390,16 @@ class BasicHttpAsyncCache implements HttpAsyncCache {
if (LOG.isDebugEnabled()) {
LOG.debug("Create cache entry: {}; {}", host, new RequestLine(request));
}
final String cacheKey = cacheKeyGenerator.generateKey(host, request);
final String rootKey = cacheKeyGenerator.generateKey(host, request);
try {
final HttpCacheEntry entry = cacheEntryFactory.create(requestSent, responseReceived, request, originResponse,
content != null ? resourceFactory.generate(request.getRequestUri(), content.array(), 0, content.length()) : null);
return storeInCache(
host,
request,
originResponse,
requestSent,
responseReceived,
cacheKey,
rootKey,
entry, new FutureCallback<Boolean>() {
@Override
@ -418,7 +420,7 @@ class BasicHttpAsyncCache implements HttpAsyncCache {
});
} catch (final ResourceIOException ex) {
if (LOG.isWarnEnabled()) {
LOG.warn("I/O error creating cache entry with key {}", cacheKey);
LOG.warn("I/O error creating cache entry with key {}", rootKey);
}
callback.completed(cacheEntryFactory.create(
requestSent,
@ -436,14 +438,15 @@ class BasicHttpAsyncCache implements HttpAsyncCache {
LOG.debug("Get cache entry: {}; {}", host, new RequestLine(request));
}
final ComplexCancellable complexCancellable = new ComplexCancellable();
final String cacheKey = cacheKeyGenerator.generateKey(host, request);
complexCancellable.setDependency(storage.getEntry(cacheKey, new FutureCallback<HttpCacheEntry>() {
final String rootKey = cacheKeyGenerator.generateKey(host, request);
complexCancellable.setDependency(storage.getEntry(rootKey, new FutureCallback<HttpCacheEntry>() {
@Override
public void completed(final HttpCacheEntry root) {
if (root != null) {
if (root.isVariantRoot()) {
final String variantKey = cacheKeyGenerator.generateVariantKey(request, root);
final List<String> variantNames = CacheKeyGenerator.variantNames(root);
final String variantKey = cacheKeyGenerator.generateVariantKey(request, variantNames);
final String variantCacheKey = root.getVariantMap().get(variantKey);
if (variantCacheKey != null) {
complexCancellable.setDependency(storage.getEntry(
@ -484,7 +487,7 @@ class BasicHttpAsyncCache implements HttpAsyncCache {
public void failed(final Exception ex) {
if (ex instanceof ResourceIOException) {
if (LOG.isWarnEnabled()) {
LOG.warn("I/O error retrieving cache entry with key {}", cacheKey);
LOG.warn("I/O error retrieving cache entry with key {}", rootKey);
}
callback.completed(null);
} else {
@ -508,9 +511,9 @@ class BasicHttpAsyncCache implements HttpAsyncCache {
LOG.debug("Get variant cache entries: {}; {}", host, new RequestLine(request));
}
final ComplexCancellable complexCancellable = new ComplexCancellable();
final String cacheKey = cacheKeyGenerator.generateKey(host, request);
final String rootKey = cacheKeyGenerator.generateKey(host, request);
final Map<String, Variant> variants = new HashMap<>();
complexCancellable.setDependency(storage.getEntry(cacheKey, new FutureCallback<HttpCacheEntry>() {
complexCancellable.setDependency(storage.getEntry(rootKey, new FutureCallback<HttpCacheEntry>() {
@Override
public void completed(final HttpCacheEntry rootEntry) {
@ -560,7 +563,7 @@ class BasicHttpAsyncCache implements HttpAsyncCache {
public void failed(final Exception ex) {
if (ex instanceof ResourceIOException) {
if (LOG.isWarnEnabled()) {
LOG.warn("I/O error retrieving cache entry with key {}", cacheKey);
LOG.warn("I/O error retrieving cache entry with key {}", rootKey);
}
callback.completed(variants);
} else {

View File

@ -28,6 +28,7 @@ package org.apache.hc.client5.http.impl.cache;
import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.hc.client5.http.cache.HttpCacheEntry;
@ -93,11 +94,15 @@ class BasicHttpCache implements HttpCache {
@Override
public String generateKey(final HttpHost host, final HttpRequest request, final HttpCacheEntry cacheEntry) {
if (cacheEntry == null) {
return cacheKeyGenerator.generateKey(host, request);
} else {
return cacheKeyGenerator.generateKey(host, request, cacheEntry);
final String root = cacheKeyGenerator.generateKey(host, request);
if (cacheEntry != null && cacheEntry.isVariantRoot()) {
final List<String> variantNames = CacheKeyGenerator.variantNames(cacheEntry);
if (!variantNames.isEmpty()) {
final String variantKey = cacheKeyGenerator.generateVariantKey(request, variantNames);
return variantKey + root;
}
}
return root;
}
@Override
@ -106,12 +111,12 @@ class BasicHttpCache implements HttpCache {
LOG.debug("Flush cache entries: {}; {}", host, new RequestLine(request));
}
if (!Method.isSafe(request.getMethod())) {
final String cacheKey = cacheKeyGenerator.generateKey(host, request);
final String rootKey = cacheKeyGenerator.generateKey(host, request);
try {
storage.removeEntry(cacheKey);
storage.removeEntry(rootKey);
} catch (final ResourceIOException ex) {
if (LOG.isWarnEnabled()) {
LOG.warn("I/O error removing cache entry with key {}", cacheKey);
LOG.warn("I/O error removing cache entry with key {}", rootKey);
}
}
}
@ -136,17 +141,16 @@ class BasicHttpCache implements HttpCache {
}
void storeInCache(
final HttpHost host,
final HttpRequest request,
final HttpResponse originResponse,
final Instant requestSent,
final Instant responseReceived,
final String cacheKey,
final String rootKey,
final HttpCacheEntry entry) {
if (entry.hasVariants()) {
storeVariantEntry(host, request, originResponse, requestSent, responseReceived, cacheKey, entry);
storeVariantEntry(request, originResponse, requestSent, responseReceived, rootKey, entry);
} else {
storeEntry(cacheKey, entry);
storeEntry(rootKey, entry);
}
}
@ -161,29 +165,29 @@ class BasicHttpCache implements HttpCache {
}
void storeVariantEntry(
final HttpHost host,
final HttpRequest request,
final HttpResponse originResponse,
final Instant requestSent,
final Instant responseReceived,
final String cacheKey,
final String rootKey,
final HttpCacheEntry entry) {
final String variantKey = cacheKeyGenerator.generateVariantKey(request, entry);
final String variantCacheKey = cacheKeyGenerator.generateKey(host, request, entry);
final List<String> variantNames = CacheKeyGenerator.variantNames(entry);
final String variantKey = cacheKeyGenerator.generateVariantKey(request, variantNames);
final String variantCacheKey = variantKey + request;
storeEntry(variantCacheKey, entry);
try {
storage.updateEntry(cacheKey, existing -> {
storage.updateEntry(rootKey, existing -> {
final Map<String,String> variantMap = existing != null ? new HashMap<>(existing.getVariantMap()) : new HashMap<>();
variantMap.put(variantKey, variantCacheKey);
return cacheEntryFactory.createRoot(requestSent, responseReceived, request, originResponse, variantMap);
});
} catch (final HttpCacheUpdateException ex) {
if (LOG.isWarnEnabled()) {
LOG.warn("Cannot update cache entry with key {}", cacheKey);
LOG.warn("Cannot update cache entry with key {}", rootKey);
}
} catch (final ResourceIOException ex) {
if (LOG.isWarnEnabled()) {
LOG.warn("I/O error updating cache entry with key {}", cacheKey);
LOG.warn("I/O error updating cache entry with key {}", rootKey);
}
}
}
@ -199,8 +203,8 @@ class BasicHttpCache implements HttpCache {
if (LOG.isDebugEnabled()) {
LOG.debug("Re-use variant entry: {}; {} / {}", host, new RequestLine(request), entry);
}
final String cacheKey = cacheKeyGenerator.generateKey(host, request);
storeVariantEntry(host, request, originResponse, requestSent, responseReceived, cacheKey, entry);
final String rootKey = cacheKeyGenerator.generateKey(host, request);
storeVariantEntry(request, originResponse, requestSent, responseReceived, rootKey, entry);
}
@Override
@ -214,13 +218,13 @@ class BasicHttpCache implements HttpCache {
if (LOG.isDebugEnabled()) {
LOG.debug("Update cache entry: {}; {}", host, new RequestLine(request));
}
final String cacheKey = cacheKeyGenerator.generateKey(host, request);
final String rootKey = cacheKeyGenerator.generateKey(host, request);
final HttpCacheEntry updatedEntry = cacheEntryFactory.createUpdated(
requestSent,
responseReceived,
originResponse,
stale);
storeInCache(host, request, originResponse, requestSent, responseReceived, cacheKey, updatedEntry);
storeInCache(request, originResponse, requestSent, responseReceived, rootKey, updatedEntry);
return updatedEntry;
}
@ -235,13 +239,13 @@ class BasicHttpCache implements HttpCache {
if (LOG.isDebugEnabled()) {
LOG.debug("Update variant cache entry: {}; {} / {}", host, new RequestLine(request), entry);
}
final String cacheKey = cacheKeyGenerator.generateKey(host, request);
final String rootKey = cacheKeyGenerator.generateKey(host, request);
final HttpCacheEntry updatedEntry = cacheEntryFactory.createUpdated(
requestSent,
responseReceived,
originResponse,
entry);
storeEntry(cacheKey, updatedEntry);
storeEntry(rootKey, updatedEntry);
return updatedEntry;
}
@ -256,15 +260,15 @@ class BasicHttpCache implements HttpCache {
if (LOG.isDebugEnabled()) {
LOG.debug("Create cache entry: {}; {}", host, new RequestLine(request));
}
final String cacheKey = cacheKeyGenerator.generateKey(host, request);
final String rootKey = cacheKeyGenerator.generateKey(host, request);
try {
final HttpCacheEntry entry = cacheEntryFactory.create(requestSent, responseReceived, request, originResponse,
content != null ? resourceFactory.generate(request.getRequestUri(), content.array(), 0, content.length()) : null);
storeInCache(host, request, originResponse, requestSent, responseReceived, cacheKey, entry);
storeInCache(request, originResponse, requestSent, responseReceived, rootKey, entry);
return entry;
} catch (final ResourceIOException ex) {
if (LOG.isWarnEnabled()) {
LOG.warn("I/O error creating cache entry with key {}", cacheKey);
LOG.warn("I/O error creating cache entry with key {}", rootKey);
}
return cacheEntryFactory.create(
requestSent,
@ -280,13 +284,13 @@ class BasicHttpCache implements HttpCache {
if (LOG.isDebugEnabled()) {
LOG.debug("Get cache entry: {}; {}", host, new RequestLine(request));
}
final String cacheKey = cacheKeyGenerator.generateKey(host, request);
final String rootKey = cacheKeyGenerator.generateKey(host, request);
final HttpCacheEntry root;
try {
root = storage.getEntry(cacheKey);
root = storage.getEntry(rootKey);
} catch (final ResourceIOException ex) {
if (LOG.isWarnEnabled()) {
LOG.warn("I/O error retrieving cache entry with key {}", cacheKey);
LOG.warn("I/O error retrieving cache entry with key {}", rootKey);
}
return null;
}
@ -294,7 +298,8 @@ class BasicHttpCache implements HttpCache {
return null;
}
if (root.isVariantRoot()) {
final String variantKey = cacheKeyGenerator.generateVariantKey(request, root);
final List<String> variantNames = CacheKeyGenerator.variantNames(root);
final String variantKey = cacheKeyGenerator.generateVariantKey(request, variantNames);
final String variantCacheKey = root.getVariantMap().get(variantKey);
if (variantCacheKey != null) {
try {
@ -317,13 +322,13 @@ class BasicHttpCache implements HttpCache {
LOG.debug("Get variant cache entries: {}; {}", host, new RequestLine(request));
}
final Map<String,Variant> variants = new HashMap<>();
final String cacheKey = cacheKeyGenerator.generateKey(host, request);
final String rootKey = cacheKeyGenerator.generateKey(host, request);
final HttpCacheEntry root;
try {
root = storage.getEntry(cacheKey);
root = storage.getEntry(rootKey);
} catch (final ResourceIOException ex) {
if (LOG.isWarnEnabled()) {
LOG.warn("I/O error retrieving cache entry with key {}", cacheKey);
LOG.warn("I/O error retrieving cache entry with key {}", rootKey);
}
return variants;
}

View File

@ -26,26 +26,27 @@
*/
package org.apache.hc.client5.http.impl.cache;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.hc.client5.http.cache.HttpCacheEntry;
import org.apache.hc.core5.annotation.Contract;
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.HeaderElement;
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.message.MessageSupport;
import org.apache.hc.core5.http.MessageHeaders;
import org.apache.hc.core5.net.PercentCodec;
import org.apache.hc.core5.util.Args;
/**
* @since 4.1
@ -78,7 +79,7 @@ public class CacheKeyGenerator implements Resolver<URI, String> {
}
/**
* Computes a key for the given {@link HttpHost} and {@link HttpRequest}
* Computes a root key for the given {@link HttpHost} and {@link HttpRequest}
* that can be used as a unique identifier for cached resources.
*
* @param host The host for this request
@ -94,18 +95,64 @@ public class CacheKeyGenerator implements Resolver<URI, String> {
}
}
private String getFullHeaderValue(final Header[] headers) {
if (headers == null) {
return "";
/**
* Returns all variant names contained in {@literal VARY} headers of the given message.
*
* @since 5.3
*/
public static List<String> variantNames(final MessageHeaders message) {
if (message == null) {
return null;
}
final StringBuilder buf = new StringBuilder();
for (int i = 0; i < headers.length; i++) {
final Header hdr = headers[i];
if (i > 0) {
buf.append(", ");
}
buf.append(hdr.getValue().trim());
final List<String> names = new ArrayList<>();
for (final Iterator<Header> it = message.headerIterator(HttpHeaders.VARY); it.hasNext(); ) {
final Header header = it.next();
CacheSupport.parseTokens(header, names::add);
}
return names;
}
/**
* Computes a "variant key" for the given request and the given variants.
* @param request originating request
* @param variantNames variant names
* @return variant key
*
* @since 5.3
*/
public String generateVariantKey(final HttpRequest request, final Collection<String> variantNames) {
Args.notNull(variantNames, "Variant names");
final StringBuilder buf = new StringBuilder("{");
final AtomicBoolean firstHeader = new AtomicBoolean();
variantNames.stream()
.map(h -> h.toLowerCase(Locale.ROOT))
.sorted()
.distinct()
.forEach(h -> {
if (!firstHeader.compareAndSet(false, true)) {
buf.append("&");
}
buf.append(PercentCodec.encode(h, StandardCharsets.UTF_8)).append("=");
final List<String> tokens = new ArrayList<>();
final Iterator<Header> headerIterator = request.headerIterator(h);
while (headerIterator.hasNext()) {
final Header header = headerIterator.next();
CacheSupport.parseTokens(header, tokens::add);
}
final AtomicBoolean firstToken = new AtomicBoolean();
tokens.stream()
.filter(t -> !t.isEmpty())
.map(t -> t.toLowerCase(Locale.ROOT))
.sorted()
.distinct()
.forEach(t -> {
if (!firstToken.compareAndSet(false, true)) {
buf.append(",");
}
buf.append(PercentCodec.encode(t, StandardCharsets.UTF_8));
});
});
buf.append("}");
return buf.toString();
}
@ -118,12 +165,18 @@ public class CacheKeyGenerator implements Resolver<URI, String> {
* @param request the {@link HttpRequest}
* @param entry the parent entry used to track the variants
* @return cache key
*
* @deprecated Use {@link #generateKey(HttpHost, HttpRequest)} or {@link #generateVariantKey(HttpRequest, Collection)}
*/
@Deprecated
public String generateKey(final HttpHost host, final HttpRequest request, final HttpCacheEntry entry) {
if (!entry.hasVariants()) {
return generateKey(host, request);
final String rootKey = generateKey(host, request);
final List<String> variantNames = variantNames(entry);
if (variantNames.isEmpty()) {
return rootKey;
} else {
return generateVariantKey(request, variantNames) + rootKey;
}
return generateVariantKey(request, entry) + generateKey(host, request);
}
/**
@ -134,35 +187,12 @@ public class CacheKeyGenerator implements Resolver<URI, String> {
* @param req originating request
* @param entry cache entry in question that has variants
* @return variant key
*
* @deprecated Use {@link #generateVariantKey(HttpRequest, Collection)}.
*/
@Deprecated
public String generateVariantKey(final HttpRequest req, final HttpCacheEntry entry) {
final List<String> variantHeaderNames = new ArrayList<>();
final Iterator<HeaderElement> it = MessageSupport.iterate(entry, HttpHeaders.VARY);
while (it.hasNext()) {
final HeaderElement elt = it.next();
variantHeaderNames.add(elt.getName());
}
Collections.sort(variantHeaderNames);
final StringBuilder buf;
try {
buf = new StringBuilder("{");
boolean first = true;
for (final String headerName : variantHeaderNames) {
if (!first) {
buf.append("&");
}
buf.append(URLEncoder.encode(headerName, StandardCharsets.UTF_8.name()));
buf.append("=");
buf.append(URLEncoder.encode(getFullHeaderValue(req.getHeaders(headerName)),
StandardCharsets.UTF_8.name()));
first = false;
}
buf.append("}");
} catch (final UnsupportedEncodingException uee) {
throw new RuntimeException("couldn't encode to UTF-8", uee);
}
return buf.toString();
return generateVariantKey(req, variantNames(entry));
}
}

View File

@ -187,7 +187,7 @@ public class TestBasicHttpCache {
final String key = CacheKeyGenerator.INSTANCE.generateKey(host, req);
impl.storeInCache(host, req, resp, Instant.now(), Instant.now(), key, entry);
impl.storeInCache(req, resp, Instant.now(), Instant.now(), key, entry);
assertSame(entry, backing.map.get(key));
}

View File

@ -26,18 +26,15 @@
*/
package org.apache.hc.client5.http.impl.cache;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.Arrays;
import java.util.Collections;
import org.apache.hc.client5.http.cache.HttpCacheEntry;
import org.apache.hc.client5.http.classic.methods.HttpGet;
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.message.BasicHeader;
import org.apache.hc.core5.http.message.BasicHeaderIterator;
import org.apache.hc.core5.http.message.BasicHttpRequest;
import org.apache.hc.core5.http.support.BasicRequestBuilder;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -45,20 +42,12 @@ import org.junit.jupiter.api.Test;
@SuppressWarnings({"boxing","static-access"}) // this is test code
public class TestCacheKeyGenerator {
private static final BasicHttpRequest REQUEST_FULL_EPISODES = new BasicHttpRequest("GET",
"/full_episodes");
private static final BasicHttpRequest REQUEST_ROOT = new BasicHttpRequest("GET", "/");
private CacheKeyGenerator extractor;
private HttpHost defaultHost;
private HttpCacheEntry mockEntry;
private HttpRequest mockRequest;
@BeforeEach
public void setUp() throws Exception {
defaultHost = new HttpHost("foo.example.com");
mockEntry = mock(HttpCacheEntry.class);
mockRequest = mock(HttpRequest.class);
extractor = CacheKeyGenerator.INSTANCE;
}
@ -71,203 +60,46 @@ public class TestCacheKeyGenerator {
@Test
public void testGetURIWithDefaultPortAndScheme() {
Assertions.assertEquals("http://www.comcast.net:80/", extractor.generateKey(new HttpHost(
"www.comcast.net"), REQUEST_ROOT));
Assertions.assertEquals("http://www.comcast.net:80/", extractor.generateKey(
new HttpHost("www.comcast.net"),
new BasicHttpRequest("GET", "/")));
Assertions.assertEquals("http://www.fancast.com:80/full_episodes", extractor.generateKey(new HttpHost(
"www.fancast.com"), REQUEST_FULL_EPISODES));
Assertions.assertEquals("http://www.fancast.com:80/full_episodes", extractor.generateKey(
new HttpHost("www.fancast.com"),
new BasicHttpRequest("GET", "/full_episodes")));
}
@Test
public void testGetURIWithDifferentScheme() {
Assertions.assertEquals("https://www.comcast.net:443/", extractor.generateKey(
new HttpHost("https", "www.comcast.net", -1), REQUEST_ROOT));
new HttpHost("https", "www.comcast.net", -1),
new BasicHttpRequest("GET", "/")));
Assertions.assertEquals("myhttp://www.fancast.com/full_episodes", extractor.generateKey(
new HttpHost("myhttp", "www.fancast.com", -1), REQUEST_FULL_EPISODES));
new HttpHost("myhttp", "www.fancast.com", -1),
new BasicHttpRequest("GET", "/full_episodes")));
}
@Test
public void testGetURIWithDifferentPort() {
Assertions.assertEquals("http://www.comcast.net:8080/", extractor.generateKey(new HttpHost(
"www.comcast.net", 8080), REQUEST_ROOT));
Assertions.assertEquals("http://www.comcast.net:8080/", extractor.generateKey(
new HttpHost("www.comcast.net", 8080),
new BasicHttpRequest("GET", "/")));
Assertions.assertEquals("http://www.fancast.com:9999/full_episodes", extractor.generateKey(
new HttpHost("www.fancast.com", 9999), REQUEST_FULL_EPISODES));
new HttpHost("www.fancast.com", 9999),
new BasicHttpRequest("GET", "/full_episodes")));
}
@Test
public void testGetURIWithDifferentPortAndScheme() {
Assertions.assertEquals("https://www.comcast.net:8080/", extractor.generateKey(
new HttpHost("https", "www.comcast.net", 8080), REQUEST_ROOT));
new HttpHost("https", "www.comcast.net", 8080),
new BasicHttpRequest("GET", "/")));
Assertions.assertEquals("myhttp://www.fancast.com:9999/full_episodes", extractor.generateKey(
new HttpHost("myhttp", "www.fancast.com", 9999), REQUEST_FULL_EPISODES));
}
@Test
public void testGetURIWithQueryParameters() {
Assertions.assertEquals("http://www.comcast.net:80/?foo=bar", extractor.generateKey(
new HttpHost("http", "www.comcast.net", -1), new BasicHttpRequest("GET", "/?foo=bar")));
Assertions.assertEquals("http://www.fancast.com:80/full_episodes?foo=bar", extractor.generateKey(
new HttpHost("http", "www.fancast.com", -1), new BasicHttpRequest("GET",
"/full_episodes?foo=bar")));
}
@Test
public void testGetVariantURIWithNoVaryHeaderReturnsNormalURI() {
final String theURI = "theURI";
when(mockEntry.hasVariants()).thenReturn(false);
extractor = new CacheKeyGenerator() {
@Override
public String generateKey(final HttpHost h, final HttpRequest request) {
Assertions.assertSame(defaultHost, h);
Assertions.assertSame(mockRequest, request);
return theURI;
}
};
final String result = extractor.generateKey(defaultHost, mockRequest, mockEntry);
verify(mockEntry).hasVariants();
Assertions.assertSame(theURI, result);
}
@Test
public void testGetVariantURIWithSingleValueVaryHeaderPrepends() {
final String theURI = "theURI";
final Header[] varyHeaders = { new BasicHeader("Vary", "Accept-Encoding") };
final Header[] encHeaders = { new BasicHeader("Accept-Encoding", "gzip") };
extractor = new CacheKeyGenerator() {
@Override
public String generateKey(final HttpHost h, final HttpRequest request) {
Assertions.assertSame(defaultHost, h);
Assertions.assertSame(mockRequest, request);
return theURI;
}
};
when(mockEntry.hasVariants()).thenReturn(true);
when(mockEntry.headerIterator("Vary")).thenReturn(new BasicHeaderIterator(varyHeaders, "Vary"));
when(mockRequest.getHeaders("Accept-Encoding")).thenReturn(encHeaders);
final String result = extractor.generateKey(defaultHost, mockRequest, mockEntry);
verify(mockEntry).hasVariants();
verify(mockEntry).headerIterator("Vary");
verify(mockRequest).getHeaders("Accept-Encoding");
Assertions.assertEquals("{Accept-Encoding=gzip}" + theURI, result);
}
@Test
public void testGetVariantURIWithMissingRequestHeader() {
final String theURI = "theURI";
final Header[] noHeaders = new Header[0];
final Header[] varyHeaders = { new BasicHeader("Vary", "Accept-Encoding") };
extractor = new CacheKeyGenerator() {
@Override
public String generateKey(final HttpHost h, final HttpRequest request) {
Assertions.assertSame(defaultHost, h);
Assertions.assertSame(mockRequest, request);
return theURI;
}
};
when(mockEntry.hasVariants()).thenReturn(true);
when(mockEntry.headerIterator("Vary")).thenReturn(new BasicHeaderIterator(varyHeaders, "Vary"));
when(mockRequest.getHeaders("Accept-Encoding"))
.thenReturn(noHeaders);
final String result = extractor.generateKey(defaultHost, mockRequest, mockEntry);
verify(mockEntry).hasVariants();
verify(mockEntry).headerIterator("Vary");
verify(mockRequest).getHeaders("Accept-Encoding");
Assertions.assertEquals("{Accept-Encoding=}" + theURI, result);
}
@Test
public void testGetVariantURIAlphabetizesWithMultipleVaryingHeaders() {
final String theURI = "theURI";
final Header[] varyHeaders = { new BasicHeader("Vary", "User-Agent, Accept-Encoding") };
final Header[] encHeaders = { new BasicHeader("Accept-Encoding", "gzip") };
final Header[] uaHeaders = { new BasicHeader("User-Agent", "browser") };
extractor = new CacheKeyGenerator() {
@Override
public String generateKey(final HttpHost h, final HttpRequest request) {
Assertions.assertSame(defaultHost, h);
Assertions.assertSame(mockRequest, request);
return theURI;
}
};
when(mockEntry.hasVariants()).thenReturn(true);
when(mockEntry.headerIterator("Vary")).thenReturn(new BasicHeaderIterator(varyHeaders, "Vary"));
when(mockRequest.getHeaders("Accept-Encoding")).thenReturn(encHeaders);
when(mockRequest.getHeaders("User-Agent")).thenReturn(uaHeaders);
final String result = extractor.generateKey(defaultHost, mockRequest, mockEntry);
verify(mockEntry).hasVariants();
verify(mockEntry).headerIterator("Vary");
verify(mockRequest).getHeaders("Accept-Encoding");
verify(mockRequest).getHeaders("User-Agent");
Assertions.assertEquals("{Accept-Encoding=gzip&User-Agent=browser}" + theURI, result);
}
@Test
public void testGetVariantURIHandlesMultipleVaryHeaders() {
final String theURI = "theURI";
final Header[] varyHeaders = { new BasicHeader("Vary", "User-Agent"),
new BasicHeader("Vary", "Accept-Encoding") };
final Header[] encHeaders = { new BasicHeader("Accept-Encoding", "gzip") };
final Header[] uaHeaders = { new BasicHeader("User-Agent", "browser") };
extractor = new CacheKeyGenerator() {
@Override
public String generateKey(final HttpHost h, final HttpRequest request) {
Assertions.assertSame(defaultHost, h);
Assertions.assertSame(mockRequest, request);
return theURI;
}
};
when(mockEntry.hasVariants()).thenReturn(true);
when(mockEntry.headerIterator("Vary")).thenReturn(new BasicHeaderIterator(varyHeaders, "Vary"));
when(mockRequest.getHeaders("Accept-Encoding")).thenReturn(encHeaders);
when(mockRequest.getHeaders("User-Agent")).thenReturn(uaHeaders);
final String result = extractor.generateKey(defaultHost, mockRequest, mockEntry);
verify(mockEntry).hasVariants();
verify(mockEntry).headerIterator("Vary");
verify(mockRequest).getHeaders("Accept-Encoding");
verify(mockRequest).getHeaders("User-Agent");
Assertions.assertEquals("{Accept-Encoding=gzip&User-Agent=browser}" + theURI, result);
}
@Test
public void testGetVariantURIHandlesMultipleLineRequestHeaders() {
final String theURI = "theURI";
final Header[] varyHeaders = { new BasicHeader("Vary", "User-Agent, Accept-Encoding") };
final Header[] encHeaders = { new BasicHeader("Accept-Encoding", "gzip"),
new BasicHeader("Accept-Encoding", "deflate") };
final Header[] uaHeaders = { new BasicHeader("User-Agent", "browser") };
extractor = new CacheKeyGenerator() {
@Override
public String generateKey(final HttpHost h, final HttpRequest request) {
Assertions.assertSame(defaultHost, h);
Assertions.assertSame(mockRequest, request);
return theURI;
}
};
when(mockEntry.hasVariants()).thenReturn(true);
when(mockEntry.headerIterator("Vary")).thenReturn(new BasicHeaderIterator(varyHeaders, "Vary"));
when(mockRequest.getHeaders("Accept-Encoding")).thenReturn(encHeaders);
when(mockRequest.getHeaders("User-Agent")).thenReturn(uaHeaders);
final String result = extractor.generateKey(defaultHost, mockRequest, mockEntry);
verify(mockEntry).hasVariants();
verify(mockEntry).headerIterator("Vary");
verify(mockRequest).getHeaders("Accept-Encoding");
verify(mockRequest).getHeaders("User-Agent");
Assertions.assertEquals("{Accept-Encoding=gzip%2C+deflate&User-Agent=browser}" + theURI, result);
new HttpHost("myhttp", "www.fancast.com", 9999),
new BasicHttpRequest("GET", "/full_episodes")));
}
/*
@ -417,4 +249,66 @@ public class TestCacheKeyGenerator {
final HttpRequest req2 = new BasicHttpRequest("GET", "/%7Esmith/home%20folder.html");
Assertions.assertEquals(extractor.generateKey(host, req1), extractor.generateKey(host, req2));
}
@Test
public void testGetURIWithQueryParameters() {
Assertions.assertEquals("http://www.comcast.net:80/?foo=bar", extractor.generateKey(
new HttpHost("http", "www.comcast.net", -1), new BasicHttpRequest("GET", "/?foo=bar")));
Assertions.assertEquals("http://www.fancast.com:80/full_episodes?foo=bar", extractor.generateKey(
new HttpHost("http", "www.fancast.com", -1), new BasicHttpRequest("GET",
"/full_episodes?foo=bar")));
}
@Test
public void testGetVariantKey() {
final HttpRequest request = BasicRequestBuilder.get("/blah")
.addHeader(HttpHeaders.USER_AGENT, "some-agent")
.addHeader(HttpHeaders.ACCEPT_ENCODING, "gzip,zip")
.addHeader(HttpHeaders.ACCEPT_ENCODING, "deflate")
.build();
Assertions.assertEquals("{user-agent=some-agent}",
extractor.generateVariantKey(request, Collections.singletonList(HttpHeaders.USER_AGENT)));
Assertions.assertEquals("{accept-encoding=deflate,gzip,zip}",
extractor.generateVariantKey(request, Collections.singletonList(HttpHeaders.ACCEPT_ENCODING)));
Assertions.assertEquals("{accept-encoding=deflate,gzip,zip&user-agent=some-agent}",
extractor.generateVariantKey(request, Arrays.asList(HttpHeaders.USER_AGENT, HttpHeaders.ACCEPT_ENCODING)));
}
@Test
public void testGetVariantKeyInputNormalization() {
final HttpRequest request = BasicRequestBuilder.get("/blah")
.addHeader(HttpHeaders.USER_AGENT, "Some-Agent")
.addHeader(HttpHeaders.ACCEPT_ENCODING, "gzip, ZIP,,")
.addHeader(HttpHeaders.ACCEPT_ENCODING, "deflate")
.build();
Assertions.assertEquals("{user-agent=some-agent}",
extractor.generateVariantKey(request, Collections.singletonList(HttpHeaders.USER_AGENT)));
Assertions.assertEquals("{accept-encoding=deflate,gzip,zip}",
extractor.generateVariantKey(request, Collections.singletonList(HttpHeaders.ACCEPT_ENCODING)));
Assertions.assertEquals("{accept-encoding=deflate,gzip,zip&user-agent=some-agent}",
extractor.generateVariantKey(request, Arrays.asList(HttpHeaders.USER_AGENT, HttpHeaders.ACCEPT_ENCODING)));
Assertions.assertEquals("{accept-encoding=deflate,gzip,zip&user-agent=some-agent}",
extractor.generateVariantKey(request, Arrays.asList(HttpHeaders.USER_AGENT, HttpHeaders.ACCEPT_ENCODING, "USER-AGENT", HttpHeaders.ACCEPT_ENCODING)));
}
@Test
public void testGetVariantKeyInputNormalizationReservedChars() {
final HttpRequest request = BasicRequestBuilder.get("/blah")
.addHeader(HttpHeaders.USER_AGENT, "*===some-agent===*")
.build();
Assertions.assertEquals("{user-agent=%2A%3D%3D%3Dsome-agent%3D%3D%3D%2A}",
extractor.generateVariantKey(request, Collections.singletonList(HttpHeaders.USER_AGENT)));
}
@Test
public void testGetVariantKeyInputNoMatchingHeaders() {
final HttpRequest request = BasicRequestBuilder.get("/blah")
.build();
Assertions.assertEquals("{accept-encoding=&user-agent=}",
extractor.generateVariantKey(request, Arrays.asList(HttpHeaders.ACCEPT_ENCODING, HttpHeaders.USER_AGENT)));
}
}