diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/CacheContextBuilder.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/CacheContextBuilder.java new file mode 100644 index 000000000..85f1ab952 --- /dev/null +++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/CacheContextBuilder.java @@ -0,0 +1,117 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.hc.client5.http.cache; + +import org.apache.hc.client5.http.AbstractClientContextBuilder; +import org.apache.hc.client5.http.SchemePortResolver; +import org.apache.hc.client5.http.auth.AuthCache; +import org.apache.hc.client5.http.auth.AuthScheme; +import org.apache.hc.client5.http.auth.AuthSchemeFactory; +import org.apache.hc.client5.http.auth.CredentialsProvider; +import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; +import org.apache.hc.client5.http.cookie.CookieSpecFactory; +import org.apache.hc.client5.http.cookie.CookieStore; +import org.apache.hc.client5.http.impl.DefaultSchemePortResolver; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.config.Lookup; + +public class CacheContextBuilder extends AbstractClientContextBuilder { + + public static CacheContextBuilder create(final SchemePortResolver schemePortResolver) { + return new CacheContextBuilder(schemePortResolver); + } + + public static CacheContextBuilder create() { + return new CacheContextBuilder(DefaultSchemePortResolver.INSTANCE); + } + + private RequestCacheControl cacheControl; + + protected CacheContextBuilder(final SchemePortResolver schemePortResolver) { + super(schemePortResolver); + } + + @Override + public CacheContextBuilder useCookieSpecRegistry(final Lookup cookieSpecRegistry) { + super.useCookieSpecRegistry(cookieSpecRegistry); + return this; + } + + @Override + public CacheContextBuilder useAuthSchemeRegistry(final Lookup authSchemeRegistry) { + super.useAuthSchemeRegistry(authSchemeRegistry); + return this; + } + + @Override + public CacheContextBuilder useCookieStore(final CookieStore cookieStore) { + super.useCookieStore(cookieStore); + return this; + } + + @Override + public CacheContextBuilder useCredentialsProvider(final CredentialsProvider credentialsProvider) { + super.useCredentialsProvider(credentialsProvider); + return this; + } + + @Override + public CacheContextBuilder useAuthCache(final AuthCache authCache) { + super.useAuthCache(authCache); + return this; + } + + @Override + public CacheContextBuilder preemptiveAuth(final HttpHost host, final AuthScheme authScheme) { + super.preemptiveAuth(host, authScheme); + return this; + } + + @Override + public CacheContextBuilder preemptiveBasicAuth(final HttpHost host, final UsernamePasswordCredentials credentials) { + super.preemptiveBasicAuth(host, credentials); + return this; + } + + public CacheContextBuilder setCacheControl(final RequestCacheControl cacheControl) { + this.cacheControl = cacheControl; + return this; + } + + @Override + protected HttpCacheContext createContext() { + return HttpCacheContext.create(); + } + + @Override + public HttpCacheContext build() { + final HttpCacheContext context = super.build(); + context.setRequestCacheControl(cacheControl); + return context; + } + +} diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/HttpCacheContext.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/HttpCacheContext.java index 435bc8618..ba6976977 100644 --- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/HttpCacheContext.java +++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/HttpCacheContext.java @@ -45,6 +45,21 @@ public class HttpCacheContext extends HttpClientContext { */ public static final String CACHE_RESPONSE_STATUS = "http.cache.response.status"; + /** + * @since 5.4 + */ + public static final String CACHE_ENTRY = "http.cache.entry"; + + /** + * @since 5.4 + */ + public static final String CACHE_REQUEST_CONTROL = "http.cache.request.control"; + + /** + * @since 5.4 + */ + public static final String CACHE_RESPONSE_CONTROL = "http.cache.response.control"; + public static HttpCacheContext adapt(final HttpContext context) { if (context instanceof HttpCacheContext) { return (HttpCacheContext) context; @@ -69,4 +84,55 @@ public class HttpCacheContext extends HttpClientContext { return getAttribute(CACHE_RESPONSE_STATUS, CacheResponseStatus.class); } + /** + * @since 5.4 + */ + public void setCacheResponseStatus(final CacheResponseStatus status) { + setAttribute(CACHE_RESPONSE_STATUS, status); + } + + /** + * @since 5.4 + */ + public RequestCacheControl getRequestCacheControl() { + final RequestCacheControl cacheControl = getAttribute(CACHE_REQUEST_CONTROL, RequestCacheControl.class); + return cacheControl != null ? cacheControl : RequestCacheControl.DEFAULT; + } + + /** + * @since 5.4 + */ + public void setRequestCacheControl(final RequestCacheControl requestCacheControl) { + setAttribute(CACHE_REQUEST_CONTROL, requestCacheControl); + } + + /** + * @since 5.4 + */ + public ResponseCacheControl getResponseCacheControl() { + final ResponseCacheControl cacheControl = getAttribute(CACHE_RESPONSE_CONTROL, ResponseCacheControl.class); + return cacheControl != null ? cacheControl : ResponseCacheControl.DEFAULT; + } + + /** + * @since 5.4 + */ + public void setResponseCacheControl(final ResponseCacheControl responseCacheControl) { + setAttribute(CACHE_RESPONSE_CONTROL, responseCacheControl); + } + + /** + * @since 5.4 + */ + public HttpCacheEntry getCacheEntry() { + return getAttribute(CACHE_ENTRY, HttpCacheEntry.class); + } + + /** + * @since 5.4 + */ + public void setCacheEntry(final HttpCacheEntry entry) { + setAttribute(CACHE_ENTRY, entry); + } + } diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/RequestCacheControl.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/RequestCacheControl.java index f6ea444ee..54c85b4e4 100644 --- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/RequestCacheControl.java +++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/RequestCacheControl.java @@ -47,15 +47,8 @@ public final class RequestCacheControl implements CacheControl { private final boolean onlyIfCached; private final long staleIfError; - /** - * Flag for the 'no-transform' Cache-Control directive. - * If this field is true, then the 'no-transform' directive is present in the Cache-Control header. - * According to RFC 'no-transform' directive indicates that the cache MUST NOT transform the payload. - */ - private final boolean noTransform; - RequestCacheControl(final long maxAge, final long maxStale, final long minFresh, final boolean noCache, - final boolean noStore, final boolean onlyIfCached, final long staleIfError, final boolean noTransform) { + final boolean noStore, final boolean onlyIfCached, final long staleIfError) { this.maxAge = maxAge; this.maxStale = maxStale; this.minFresh = minFresh; @@ -63,7 +56,6 @@ public final class RequestCacheControl implements CacheControl { this.noStore = noStore; this.onlyIfCached = onlyIfCached; this.staleIfError = staleIfError; - this.noTransform = noTransform; } /** @@ -144,7 +136,7 @@ public final class RequestCacheControl implements CacheControl { buf.append("max-stale=").append(maxStale).append(","); } if (minFresh >= 0) { - buf.append("max-fresh=").append(minFresh).append(","); + buf.append("min-fresh=").append(minFresh).append(","); } if (noCache) { buf.append("no-cache").append(","); @@ -158,9 +150,6 @@ public final class RequestCacheControl implements CacheControl { if (staleIfError >= 0) { buf.append("stale-if-error").append(staleIfError).append(","); } - if (noTransform) { - buf.append("no-transform").append(","); - } if (buf.charAt(buf.length() - 1) == ',') { buf.setLength(buf.length() - 1); } @@ -172,6 +161,8 @@ public final class RequestCacheControl implements CacheControl { return new Builder(); } + public static final RequestCacheControl DEFAULT = builder().build(); + public static class Builder { private long maxAge = -1; @@ -181,7 +172,6 @@ public final class RequestCacheControl implements CacheControl { private boolean noStore; private boolean onlyIfCached; private long staleIfError = -1; - private boolean noTransform; Builder() { } @@ -249,18 +239,8 @@ public final class RequestCacheControl implements CacheControl { return this; } - public boolean isNoTransform() { - return noTransform; - } - - public Builder setNoTransform(final boolean noTransform) { - this.noTransform = noTransform; - return this; - } - - public RequestCacheControl build() { - return new RequestCacheControl(maxAge, maxStale, minFresh, noCache, noStore, onlyIfCached, staleIfError, noTransform); + return new RequestCacheControl(maxAge, maxStale, minFresh, noCache, noStore, onlyIfCached, staleIfError); } } diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/ResponseCacheControl.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/ResponseCacheControl.java index c9b71a066..7e7f4b01f 100644 --- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/ResponseCacheControl.java +++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/ResponseCacheControl.java @@ -341,6 +341,8 @@ public final class ResponseCacheControl implements CacheControl { return new Builder(); } + public static final ResponseCacheControl DEFAULT = builder().build(); + public static class Builder { private long maxAge = -1; diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AsyncCachingExec.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AsyncCachingExec.java index 428b8c9e2..8386558a0 100644 --- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AsyncCachingExec.java +++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AsyncCachingExec.java @@ -239,7 +239,7 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler final AsyncExecCallback asyncExecCallback) throws HttpException, IOException { final String exchangeId = scope.exchangeId; - final HttpClientContext context = scope.clientContext; + final HttpCacheContext context = HttpCacheContext.adapt(scope.clientContext); final CancellableDependency operation = scope.cancellableDependency; if (LOG.isDebugEnabled()) { @@ -247,6 +247,7 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler } context.setAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS, CacheResponseStatus.CACHE_MISS); + context.setAttribute(HttpCacheContext.CACHE_ENTRY, null); if (clientRequestsOurOptions(request)) { context.setAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS, CacheResponseStatus.CACHE_MODULE_RESPONSE); @@ -254,7 +255,15 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler return; } - final RequestCacheControl requestCacheControl = CacheControlHeaderParser.INSTANCE.parse(request); + final RequestCacheControl requestCacheControl; + if (request.containsHeader(HttpHeaders.CACHE_CONTROL)) { + requestCacheControl = CacheControlHeaderParser.INSTANCE.parse(request); + context.setRequestCacheControl(requestCacheControl); + } else { + requestCacheControl = context.getRequestCacheControl(); + CacheControlHeaderGenerator.INSTANCE.generate(requestCacheControl, request); + } + if (LOG.isDebugEnabled()) { LOG.debug("{} request cache control: {}", exchangeId, requestCacheControl); } @@ -273,6 +282,7 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler if (LOG.isDebugEnabled()) { LOG.debug("{} response cache control: {}", exchangeId, responseCacheControl); } + context.setResponseCacheControl(responseCacheControl); handleCacheHit(requestCacheControl, responseCacheControl, hit, target, request, entityProducer, scope, chain, asyncExecCallback); } } @@ -540,6 +550,7 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler void triggerNewCacheEntryResponse(final HttpResponse backendResponse, final Instant responseDate, final ByteArrayBuffer buffer) { final String exchangeId = scope.exchangeId; + final HttpClientContext context = scope.clientContext; final CancellableDependency operation = scope.cancellableDependency; operation.setDependency(responseCache.store( target, @@ -557,6 +568,7 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler } try { final SimpleHttpResponse cacheResponse = responseGenerator.generateResponse(request, hit.entry); + context.setAttribute(HttpCacheContext.CACHE_ENTRY, hit.entry); triggerResponse(cacheResponse, scope, asyncExecCallback); } catch (final ResourceIOException ex) { asyncExecCallback.failed(ex); @@ -578,8 +590,10 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler } void triggerCachedResponse(final HttpCacheEntry entry) { + final HttpClientContext context = scope.clientContext; try { final SimpleHttpResponse cacheResponse = responseGenerator.generateResponse(request, entry); + context.setAttribute(HttpCacheContext.CACHE_ENTRY, entry); triggerResponse(cacheResponse, scope, asyncExecCallback); } catch (final ResourceIOException ex) { asyncExecCallback.failed(ex); @@ -731,6 +745,7 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler } try { final SimpleHttpResponse cacheResponse = generateCachedResponse(request, hit.entry, now); + context.setAttribute(HttpCacheContext.CACHE_ENTRY, hit.entry); triggerResponse(cacheResponse, scope, asyncExecCallback); } catch (final ResourceIOException ex) { if (requestCacheControl.isOnlyIfCached()) { @@ -802,7 +817,8 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler asyncExecCallback, c -> revalidateCacheEntry(responseCacheControl, hit, target, request, entityProducer, fork, chain, c)); context.setAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS, CacheResponseStatus.CACHE_MODULE_RESPONSE); - final SimpleHttpResponse cacheResponse = unvalidatedCacheHit(request, hit.entry); + final SimpleHttpResponse cacheResponse = responseGenerator.generateResponse(request, hit.entry); + context.setAttribute(HttpCacheContext.CACHE_ENTRY, hit.entry); triggerResponse(cacheResponse, scope, asyncExecCallback); } catch (final IOException ex) { asyncExecCallback.failed(ex); @@ -861,6 +877,7 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler public void completed(final CacheHit updated) { try { final SimpleHttpResponse cacheResponse = generateCachedResponse(request, updated.entry, responseDate); + context.setAttribute(HttpCacheContext.CACHE_ENTRY, hit.entry); triggerResponse(cacheResponse, scope, asyncExecCallback); } catch (final ResourceIOException ex) { asyncExecCallback.failed(ex); @@ -1080,7 +1097,8 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler if (response == null) { try { context.setAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS, CacheResponseStatus.CACHE_MODULE_RESPONSE); - final SimpleHttpResponse cacheResponse = unvalidatedCacheHit(request, hit.entry); + final SimpleHttpResponse cacheResponse = responseGenerator.generateResponse(request, hit.entry); + context.setAttribute(HttpCacheContext.CACHE_ENTRY, hit.entry); triggerResponse(cacheResponse, scope, asyncExecCallback); } catch (final IOException ex) { asyncExecCallback.failed(ex); @@ -1104,7 +1122,8 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler LOG.debug("{} serving stale response due to IOException and stale-if-error enabled", exchangeId); } try { - final SimpleHttpResponse cacheResponse = unvalidatedCacheHit(request, hit.entry); + final SimpleHttpResponse cacheResponse = responseGenerator.generateResponse(request, hit.entry); + context.setAttribute(HttpCacheContext.CACHE_ENTRY, hit.entry); triggerResponse(cacheResponse, scope, asyncExecCallback); } catch (final IOException ex) { asyncExecCallback.failed(cause); @@ -1219,16 +1238,12 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler @Override public void completed(final CacheHit hit) { - if (shouldSendNotModifiedResponse(request, hit.entry, Instant.now())) { - final SimpleHttpResponse cacheResponse = responseGenerator.generateNotModifiedResponse(hit.entry); + try { + final SimpleHttpResponse cacheResponse = generateCachedResponse(request, hit.entry, responseDate); + context.setAttribute(HttpCacheContext.CACHE_ENTRY, hit.entry); triggerResponse(cacheResponse, scope, asyncExecCallback); - } else { - try { - final SimpleHttpResponse cacheResponse = responseGenerator.generateResponse(request, hit.entry); - triggerResponse(cacheResponse, scope, asyncExecCallback); - } catch (final ResourceIOException ex) { - asyncExecCallback.failed(ex); - } + } catch (final ResourceIOException ex) { + asyncExecCallback.failed(ex); } } @@ -1249,6 +1264,7 @@ class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler public AsyncDataConsumer handleResponse( final HttpResponse backendResponse, final EntityDetails entityDetails) throws HttpException, IOException { + final HttpClientContext context = scope.clientContext; final Instant responseDate = getCurrentDate(); final AsyncExecCallback callback; if (backendResponse.getCode() != HttpStatus.SC_NOT_MODIFIED) { diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheControlHeaderGenerator.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheControlHeaderGenerator.java new file mode 100644 index 000000000..360908be0 --- /dev/null +++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheControlHeaderGenerator.java @@ -0,0 +1,106 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.hc.client5.http.impl.cache; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.hc.client5.http.cache.RequestCacheControl; +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.http.Header; +import org.apache.hc.core5.http.HttpHeaders; +import org.apache.hc.core5.http.HttpMessage; +import org.apache.hc.core5.http.NameValuePair; +import org.apache.hc.core5.http.message.BasicHeader; +import org.apache.hc.core5.http.message.BasicHeaderValueFormatter; +import org.apache.hc.core5.http.message.BufferedHeader; +import org.apache.hc.core5.util.Args; +import org.apache.hc.core5.util.CharArrayBuffer; + +@Internal +@Contract(threading = ThreadingBehavior.IMMUTABLE) +class CacheControlHeaderGenerator { + + public static final CacheControlHeaderGenerator INSTANCE = new CacheControlHeaderGenerator(); + + public List convert(final RequestCacheControl cacheControl) { + Args.notNull(cacheControl, "Cache control"); + final List params = new ArrayList<>(10); + if (cacheControl.getMaxAge() >= 0) { + params.add(new BasicHeader("max-age", cacheControl.getMaxAge())); + } + if (cacheControl.getMaxStale() >= 0) { + params.add(new BasicHeader("max-stale", cacheControl.getMaxStale())); + } + if (cacheControl.getMinFresh() >= 0) { + params.add(new BasicHeader("min-fresh", cacheControl.getMinFresh())); + } + if (cacheControl.isNoCache()) { + params.add(new BasicHeader("no-cache", null)); + } + if (cacheControl.isNoStore()) { + params.add(new BasicHeader("no-store", null)); + } + if (cacheControl.isOnlyIfCached()) { + params.add(new BasicHeader("only-if-cached", null)); + } + if (cacheControl.getStaleIfError() >= 0) { + params.add(new BasicHeader("stale-if-error", cacheControl.getStaleIfError())); + } + return params; + } + + public Header generate(final RequestCacheControl cacheControl) { + final List params = convert(cacheControl); + if (!params.isEmpty()) { + final CharArrayBuffer buf = new CharArrayBuffer(1024); + buf.append(HttpHeaders.CACHE_CONTROL); + buf.append(": "); + for (int i = 0; i < params.size(); i++) { + if (i > 0) { + buf.append(", "); + } + BasicHeaderValueFormatter.INSTANCE.formatNameValuePair(buf, params.get(i), false); + } + return BufferedHeader.create(buf); + } else { + return null; + } + } + + public void generate(final RequestCacheControl cacheControl, final HttpMessage message) { + final Header h = generate(cacheControl); + if (h != null) { + message.addHeader(h); + } + } + +} + + diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheControlHeaderParser.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheControlHeaderParser.java index 79804907b..5d7b6c2d2 100644 --- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheControlHeaderParser.java +++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheControlHeaderParser.java @@ -70,7 +70,7 @@ import org.slf4j.LoggerFactory; *

*/ @Internal -@Contract(threading = ThreadingBehavior.SAFE) +@Contract(threading = ThreadingBehavior.IMMUTABLE) class CacheControlHeaderParser { /** diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExec.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExec.java index 322cc1ef8..6ce584945 100644 --- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExec.java +++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExec.java @@ -145,19 +145,28 @@ class CachingExec extends CachingExecBase implements ExecChainHandler { final ExecChain.Scope scope, final ExecChain chain) throws IOException, HttpException { final String exchangeId = scope.exchangeId; - final HttpClientContext context = scope.clientContext; + final HttpCacheContext context = HttpCacheContext.adapt(scope.clientContext); if (LOG.isDebugEnabled()) { LOG.debug("{} request via cache: {}", exchangeId, new RequestLine(request)); } context.setAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS, CacheResponseStatus.CACHE_MISS); + context.setAttribute(HttpCacheContext.CACHE_ENTRY, null); if (clientRequestsOurOptions(request)) { context.setAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS, CacheResponseStatus.CACHE_MODULE_RESPONSE); return new BasicClassicHttpResponse(HttpStatus.SC_NOT_IMPLEMENTED); } - final RequestCacheControl requestCacheControl = CacheControlHeaderParser.INSTANCE.parse(request); + + final RequestCacheControl requestCacheControl; + if (request.containsHeader(HttpHeaders.CACHE_CONTROL)) { + requestCacheControl = CacheControlHeaderParser.INSTANCE.parse(request); + } else { + requestCacheControl = context.getRequestCacheControl(); + CacheControlHeaderGenerator.INSTANCE.generate(requestCacheControl, request); + } + if (LOG.isDebugEnabled()) { LOG.debug("Request cache control: {}", requestCacheControl); } @@ -176,6 +185,7 @@ class CachingExec extends CachingExecBase implements ExecChainHandler { return handleCacheMiss(requestCacheControl, root, target, request, scope, chain); } else { final ResponseCacheControl responseCacheControl = CacheControlHeaderParser.INSTANCE.parse(hit.entry); + context.setResponseCacheControl(responseCacheControl); if (LOG.isDebugEnabled()) { LOG.debug("{} response cache control: {}", exchangeId, responseCacheControl); } @@ -220,7 +230,7 @@ class CachingExec extends CachingExecBase implements ExecChainHandler { } final ClassicHttpResponse backendResponse = chain.proceed(request, scope); try { - return handleBackendResponse(exchangeId, target, request, requestDate, getCurrentDate(), backendResponse); + return handleBackendResponse(target, request, scope, requestDate, getCurrentDate(), backendResponse); } catch (final IOException | RuntimeException ex) { backendResponse.close(); throw ex; @@ -256,7 +266,9 @@ class CachingExec extends CachingExecBase implements ExecChainHandler { LOG.debug("{} cache hit is fresh enough", exchangeId); } try { - return convert(generateCachedResponse(request, hit.entry, now)); + final SimpleHttpResponse cacheResponse = generateCachedResponse(request, hit.entry, now); + context.setAttribute(HttpCacheContext.CACHE_ENTRY, hit.entry); + return convert(cacheResponse); } catch (final ResourceIOException ex) { if (requestCacheControl.isOnlyIfCached()) { if (LOG.isDebugEnabled()) { @@ -315,7 +327,9 @@ class CachingExec extends CachingExecBase implements ExecChainHandler { hit.getEntryKey(), () -> revalidateCacheEntry(responseCacheControl, hit, target, request, fork, chain)); context.setAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS, CacheResponseStatus.CACHE_MODULE_RESPONSE); - return convert(unvalidatedCacheHit(request, hit.entry)); + final SimpleHttpResponse cacheResponse = responseGenerator.generateResponse(request, hit.entry); + context.setAttribute(HttpCacheContext.CACHE_ENTRY, hit.entry); + return convert(cacheResponse); } else { if (LOG.isDebugEnabled()) { LOG.debug("{} revalidating stale cache entry (asynchronous revalidation disabled)", exchangeId); @@ -368,9 +382,11 @@ class CachingExec extends CachingExecBase implements ExecChainHandler { } if (statusCode == HttpStatus.SC_NOT_MODIFIED) { final CacheHit updated = responseCache.update(hit, target, request, backendResponse, requestDate, responseDate); - return convert(generateCachedResponse(request, updated.entry, responseDate)); + final SimpleHttpResponse cacheResponse = generateCachedResponse(request, updated.entry, responseDate); + context.setAttribute(HttpCacheContext.CACHE_ENTRY, updated.entry); + return convert(cacheResponse); } - return handleBackendResponse(scope.exchangeId, target, conditionalRequest, requestDate, responseDate, backendResponse); + return handleBackendResponse(target, conditionalRequest, scope, requestDate, responseDate, backendResponse); } catch (final IOException | RuntimeException ex) { backendResponse.close(); throw ex; @@ -419,7 +435,9 @@ class CachingExec extends CachingExecBase implements ExecChainHandler { if (LOG.isDebugEnabled()) { LOG.debug("{} serving stale response due to IOException and stale-if-error enabled", exchangeId); } - return convert(unvalidatedCacheHit(request, hit.entry)); + final SimpleHttpResponse cacheResponse = responseGenerator.generateResponse(request, hit.entry); + context.setAttribute(HttpCacheContext.CACHE_ENTRY, hit.entry); + return convert(cacheResponse); } else { return convert(generateGatewayTimeout()); } @@ -432,19 +450,21 @@ class CachingExec extends CachingExecBase implements ExecChainHandler { } EntityUtils.consume(response.getEntity()); context.setAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS, CacheResponseStatus.CACHE_MODULE_RESPONSE); - return convert(unvalidatedCacheHit(request, hit.entry)); + final SimpleHttpResponse cacheResponse = responseGenerator.generateResponse(request, hit.entry); + context.setAttribute(HttpCacheContext.CACHE_ENTRY, hit.entry); + return convert(cacheResponse); } return response; } ClassicHttpResponse handleBackendResponse( - final String exchangeId, final HttpHost target, final ClassicHttpRequest request, + final ExecChain.Scope scope, final Instant requestDate, final Instant responseDate, final ClassicHttpResponse backendResponse) throws IOException { - + final String exchangeId = scope.exchangeId; responseCache.evictInvalidatedEntries(target, request, backendResponse); if (isResponseTooBig(backendResponse.getEntity())) { if (LOG.isDebugEnabled()) { @@ -459,7 +479,7 @@ class CachingExec extends CachingExecBase implements ExecChainHandler { if (LOG.isDebugEnabled()) { LOG.debug("{} caching backend response", exchangeId); } - return cacheAndReturnResponse(exchangeId, target, request, backendResponse, requestDate, responseDate); + return cacheAndReturnResponse(target, request, scope, backendResponse, requestDate, responseDate); } else { if (LOG.isDebugEnabled()) { LOG.debug("{} backend response is not cacheable", exchangeId); @@ -469,12 +489,14 @@ class CachingExec extends CachingExecBase implements ExecChainHandler { } ClassicHttpResponse cacheAndReturnResponse( - final String exchangeId, final HttpHost target, final HttpRequest request, + final ExecChain.Scope scope, final ClassicHttpResponse backendResponse, final Instant requestSent, final Instant responseReceived) throws IOException { + final String exchangeId = scope.exchangeId; + final HttpClientContext context = scope.clientContext; final int statusCode = backendResponse.getCode(); // handle 304 Not Modified responses if (statusCode == HttpStatus.SC_NOT_MODIFIED) { @@ -488,7 +510,9 @@ class CachingExec extends CachingExecBase implements ExecChainHandler { backendResponse, requestSent, responseReceived); - return convert(responseGenerator.generateResponse(request, updated.entry)); + final SimpleHttpResponse cacheResponse = responseGenerator.generateResponse(request, updated.entry); + context.setAttribute(HttpCacheContext.CACHE_ENTRY, hit.entry); + return convert(cacheResponse); } } @@ -536,7 +560,9 @@ class CachingExec extends CachingExecBase implements ExecChainHandler { LOG.debug("{} backend response successfully cached (freshness check skipped)", exchangeId); } } - return convert(responseGenerator.generateResponse(request, hit.entry)); + final SimpleHttpResponse cacheResponse = responseGenerator.generateResponse(request, hit.entry); + context.setAttribute(HttpCacheContext.CACHE_ENTRY, hit.entry); + return convert(cacheResponse); } private ClassicHttpResponse handleCacheMiss( @@ -597,7 +623,7 @@ class CachingExec extends CachingExecBase implements ExecChainHandler { final Instant responseDate = getCurrentDate(); if (backendResponse.getCode() != HttpStatus.SC_NOT_MODIFIED) { - return handleBackendResponse(exchangeId, target, request, requestDate, responseDate, backendResponse); + return handleBackendResponse(target, request, scope, requestDate, responseDate, backendResponse); } else { // 304 response are not expected to have an enclosed content body, but still backendResponse.close(); @@ -630,11 +656,9 @@ class CachingExec extends CachingExecBase implements ExecChainHandler { cacheUpdates.getAndIncrement(); final CacheHit hit = responseCache.storeFromNegotiated(match, target, request, backendResponse, requestDate, responseDate); - if (shouldSendNotModifiedResponse(request, hit.entry, responseDate)) { - return convert(responseGenerator.generateNotModifiedResponse(hit.entry)); - } else { - return convert(responseGenerator.generateResponse(request, hit.entry)); - } + final SimpleHttpResponse cacheResponse = generateCachedResponse(request, hit.entry, responseDate); + context.setAttribute(HttpCacheContext.CACHE_ENTRY, hit.entry); + return convert(cacheResponse); } catch (final IOException | RuntimeException ex) { backendResponse.close(); throw ex; diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExecBase.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExecBase.java index c2facf4ab..f886d5b3b 100644 --- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExecBase.java +++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CachingExecBase.java @@ -26,7 +26,6 @@ */ package org.apache.hc.client5.http.impl.cache; -import java.io.IOException; import java.time.Instant; import java.util.concurrent.atomic.AtomicLong; @@ -124,10 +123,6 @@ public class CachingExecBase { return SimpleHttpResponse.create(HttpStatus.SC_GATEWAY_TIMEOUT, "Gateway Timeout"); } - SimpleHttpResponse unvalidatedCacheHit(final HttpRequest request, final HttpCacheEntry entry) throws IOException { - return responseGenerator.generateResponse(request, entry); - } - Instant getCurrentDate() { return Instant.now(); } diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/cache/example/AsyncClientCacheControl.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/cache/example/AsyncClientCacheControl.java new file mode 100644 index 000000000..4535cebcd --- /dev/null +++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/cache/example/AsyncClientCacheControl.java @@ -0,0 +1,209 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.hc.client5.http.cache.example; + +import java.util.concurrent.Future; + +import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; +import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; +import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder; +import org.apache.hc.client5.http.async.methods.SimpleRequestProducer; +import org.apache.hc.client5.http.async.methods.SimpleResponseConsumer; +import org.apache.hc.client5.http.cache.CacheContextBuilder; +import org.apache.hc.client5.http.cache.HttpCacheContext; +import org.apache.hc.client5.http.cache.HttpCacheEntry; +import org.apache.hc.client5.http.cache.RequestCacheControl; +import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; +import org.apache.hc.client5.http.impl.cache.CacheConfig; +import org.apache.hc.client5.http.impl.cache.CachingHttpAsyncClients; +import org.apache.hc.client5.http.impl.cache.HeapResourceFactory; +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.message.StatusLine; +import org.apache.hc.core5.io.CloseMode; + +/** + * This is an example demonstrating how to control execution of cache + * operation and determine its outcome using the async HTTP cache API. + */ +public class AsyncClientCacheControl { + + public static void main(final String[] args) throws Exception { + + final HttpHost target = new HttpHost("https", "www.apache.org"); + + try (final CloseableHttpAsyncClient httpclient = CachingHttpAsyncClients.custom() + .setCacheConfig(CacheConfig.custom() + .setMaxObjectSize(200000) + .setHeuristicCachingEnabled(true) + .build()) + .setResourceFactory(HeapResourceFactory.INSTANCE) + .build()) { + + httpclient.start(); + + final SimpleHttpRequest httpget1 = SimpleRequestBuilder.get() + .setHttpHost(target) + .setPath("/") + .build(); + + // Use default cache control + final HttpCacheContext context = CacheContextBuilder.create() + .setCacheControl(RequestCacheControl.DEFAULT) + .build(); + + System.out.println("Executing request " + httpget1.getMethod() + " " + httpget1.getUri()); + final Future future = httpclient.execute( + SimpleRequestProducer.create(httpget1), + SimpleResponseConsumer.create(), + context, + new FutureCallback() { + + @Override + public void completed(final SimpleHttpResponse response) { + System.out.println(httpget1 + "->" + new StatusLine(response)); + System.out.println("Cache status: " + context.getCacheResponseStatus()); + System.out.println("Request cache control: " + context.getRequestCacheControl()); + System.out.println("Response cache control: " + context.getResponseCacheControl()); + final HttpCacheEntry cacheEntry = context.getCacheEntry(); + if (cacheEntry != null) { + System.out.println("Cache entry resource: " + cacheEntry.getResource()); + System.out.println("Date: " + cacheEntry.getInstant()); + System.out.println("Expires: " + cacheEntry.getExpires()); + System.out.println("Last modified: " + cacheEntry.getLastModified()); + } + } + + @Override + public void failed(final Exception ex) { + System.out.println(httpget1 + "->" + ex); + } + + @Override + public void cancelled() { + System.out.println(httpget1 + " cancelled"); + } + + }); + future.get(); + + final SimpleHttpRequest httpget2 = SimpleRequestBuilder.get() + .setHttpHost(target) + .setPath("/") + .build(); + + // Ensure a custom freshness for the cache entry + context.setRequestCacheControl(RequestCacheControl.builder() + .setMinFresh(100) + .build()); + + System.out.println("Executing request " + httpget2.getMethod() + " " + httpget2.getUri()); + final Future future2 = httpclient.execute( + SimpleRequestProducer.create(httpget2), + SimpleResponseConsumer.create(), + context, + new FutureCallback() { + + @Override + public void completed(final SimpleHttpResponse response) { + System.out.println(httpget2 + "->" + new StatusLine(response)); + System.out.println("Cache status: " + context.getCacheResponseStatus()); + System.out.println("Request cache control: " + context.getRequestCacheControl()); + System.out.println("Response cache control: " + context.getResponseCacheControl()); + final HttpCacheEntry cacheEntry = context.getCacheEntry(); + if (cacheEntry != null) { + System.out.println("Cache entry resource: " + cacheEntry.getResource()); + System.out.println("Date: " + cacheEntry.getInstant()); + System.out.println("Expires: " + cacheEntry.getExpires()); + System.out.println("Last modified: " + cacheEntry.getLastModified()); + } + } + + @Override + public void failed(final Exception ex) { + System.out.println(httpget2 + "->" + ex); + } + + @Override + public void cancelled() { + System.out.println(httpget2 + " cancelled"); + } + + }); + future2.get(); + + Thread.sleep(2000); + + final SimpleHttpRequest httpget3 = SimpleRequestBuilder.get() + .setHttpHost(target) + .setPath("/") + .build(); + + // Try to force cache entry re-validation + context.setRequestCacheControl(RequestCacheControl.builder() + .setMaxAge(0) + .build()); + + System.out.println("Executing request " + httpget3.getMethod() + " " + httpget3.getUri()); + final Future future3 = httpclient.execute( + SimpleRequestProducer.create(httpget3), + SimpleResponseConsumer.create(), + context, + new FutureCallback() { + + @Override + public void completed(final SimpleHttpResponse response) { + System.out.println(httpget3 + "->" + new StatusLine(response)); + System.out.println("Cache status: " + context.getCacheResponseStatus()); + System.out.println("Request cache control: " + context.getRequestCacheControl()); + System.out.println("Response cache control: " + context.getResponseCacheControl()); + final HttpCacheEntry cacheEntry = context.getCacheEntry(); + if (cacheEntry != null) { + System.out.println("Cache entry resource: " + cacheEntry.getResource()); + System.out.println("Date: " + cacheEntry.getInstant()); + System.out.println("Expires: " + cacheEntry.getExpires()); + System.out.println("Last modified: " + cacheEntry.getLastModified()); + } + } + + @Override + public void failed(final Exception ex) { + System.out.println(httpget3 + "->" + ex); + } + + @Override + public void cancelled() { + System.out.println(httpget3 + " cancelled"); + } + + }); + future3.get(); + + httpclient.close(CloseMode.GRACEFUL); + } + } +} diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/cache/example/ClientCacheControl.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/cache/example/ClientCacheControl.java new file mode 100644 index 000000000..b5ea29cd3 --- /dev/null +++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/cache/example/ClientCacheControl.java @@ -0,0 +1,150 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.hc.client5.http.cache.example; + +import org.apache.hc.client5.http.cache.CacheContextBuilder; +import org.apache.hc.client5.http.cache.HttpCacheContext; +import org.apache.hc.client5.http.cache.HttpCacheEntry; +import org.apache.hc.client5.http.cache.RequestCacheControl; +import org.apache.hc.client5.http.impl.cache.CacheConfig; +import org.apache.hc.client5.http.impl.cache.CachingHttpClients; +import org.apache.hc.client5.http.impl.cache.HeapResourceFactory; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.core5.http.ClassicHttpRequest; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.apache.hc.core5.http.io.support.ClassicRequestBuilder; +import org.apache.hc.core5.http.message.StatusLine; + +/** + * This is an example demonstrating how to control execution of cache + * operation and determine its outcome using the classic HTTP cache API. + */ +public class ClientCacheControl { + + public static void main(final String[] args) throws Exception { + + final HttpHost target = new HttpHost("https", "www.apache.org"); + + try (final CloseableHttpClient httpclient = CachingHttpClients.custom() + .setCacheConfig(CacheConfig.custom() + .setMaxObjectSize(200000) + .setHeuristicCachingEnabled(true) + .build()) + .setResourceFactory(HeapResourceFactory.INSTANCE) + .build()) { + + final ClassicHttpRequest httpget1 = ClassicRequestBuilder.get() + .setHttpHost(target) + .setPath("/") + .build(); + + // Use default cache control + final HttpCacheContext context = CacheContextBuilder.create() + .setCacheControl(RequestCacheControl.DEFAULT) + .build(); + + System.out.println("Executing request " + httpget1.getMethod() + " " + httpget1.getUri()); + httpclient.execute(httpget1, context, response -> { + System.out.println("----------------------------------------"); + System.out.println(httpget1 + "->" + new StatusLine(response)); + EntityUtils.consume(response.getEntity()); + System.out.println("Cache status: " + context.getCacheResponseStatus()); + System.out.println("Request cache control: " + context.getRequestCacheControl()); + System.out.println("Response cache control: " + context.getResponseCacheControl()); + final HttpCacheEntry cacheEntry = context.getCacheEntry(); + if (cacheEntry != null) { + System.out.println("Cache entry resource: " + cacheEntry.getResource()); + System.out.println("Date: " + cacheEntry.getInstant()); + System.out.println("Expires: " + cacheEntry.getExpires()); + System.out.println("Last modified: " + cacheEntry.getLastModified()); + } + return null; + }); + + final ClassicHttpRequest httpget2 = ClassicRequestBuilder.get() + .setHttpHost(target) + .setPath("/") + .build(); + + // Ensure a custom freshness for the cache entry + context.setRequestCacheControl(RequestCacheControl.builder() + .setMinFresh(100) + .build()); + + System.out.println("Executing request " + httpget2.getMethod() + " " + httpget2.getUri()); + httpclient.execute(httpget2, context, response -> { + System.out.println("----------------------------------------"); + System.out.println(httpget2 + "->" + new StatusLine(response)); + EntityUtils.consume(response.getEntity()); + System.out.println("Cache status: " + context.getCacheResponseStatus()); + System.out.println("Request cache control: " + context.getRequestCacheControl()); + System.out.println("Response cache control: " + context.getResponseCacheControl()); + final HttpCacheEntry cacheEntry = context.getCacheEntry(); + if (cacheEntry != null) { + System.out.println("Cache entry resource: " + cacheEntry.getResource()); + System.out.println("Date: " + cacheEntry.getInstant()); + System.out.println("Expires: " + cacheEntry.getExpires()); + System.out.println("Last modified: " + cacheEntry.getLastModified()); + } + return null; + }); + + Thread.sleep(2000); + + final ClassicHttpRequest httpget3 = ClassicRequestBuilder.get() + .setHttpHost(target) + .setPath("/") + .build(); + + // Try to force cache entry re-validation + context.setRequestCacheControl(RequestCacheControl.builder() + .setMaxAge(0) + .build()); + + System.out.println("Executing request " + httpget3.getMethod() + " " + httpget3.getUri()); + httpclient.execute(httpget3, context, response -> { + System.out.println("----------------------------------------"); + System.out.println(httpget3 + "->" + new StatusLine(response)); + EntityUtils.consume(response.getEntity()); + System.out.println("Cache status: " + context.getCacheResponseStatus()); + System.out.println("Request cache control: " + context.getRequestCacheControl()); + System.out.println("Response cache control: " + context.getResponseCacheControl()); + final HttpCacheEntry cacheEntry = context.getCacheEntry(); + if (cacheEntry != null) { + System.out.println("Cache entry resource: " + cacheEntry.getResource()); + System.out.println("Date: " + cacheEntry.getInstant()); + System.out.println("Expires: " + cacheEntry.getExpires()); + System.out.println("Last modified: " + cacheEntry.getLastModified()); + } + return null; + }); + } + + + } +} diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/CacheControlGeneratorTest.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/CacheControlGeneratorTest.java new file mode 100644 index 000000000..0efa6dad3 --- /dev/null +++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/CacheControlGeneratorTest.java @@ -0,0 +1,86 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.hc.client5.http.impl.cache; + +import static org.hamcrest.MatcherAssert.assertThat; + +import org.apache.hc.client5.http.HeaderMatcher; +import org.apache.hc.client5.http.cache.RequestCacheControl; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class CacheControlGeneratorTest { + + private final CacheControlHeaderGenerator generator = CacheControlHeaderGenerator.INSTANCE; + + @Test + public void testGenerateRequestCacheControlHeader() { + assertThat(generator.generate( + RequestCacheControl.builder() + .setMaxAge(12) + .setMaxStale(23) + .setMinFresh(34) + .setNoCache(true) + .setNoStore(true) + .setOnlyIfCached(true) + .setStaleIfError(56) + .build()), + HeaderMatcher.same("Cache-Control", "max-age=12, max-stale=23, " + + "min-fresh=34, no-cache, no-store, only-if-cached, stale-if-error=56")); + assertThat(generator.generate( + RequestCacheControl.builder() + .setMaxAge(12) + .setNoCache(true) + .setMinFresh(34) + .setMaxStale(23) + .setNoStore(true) + .setStaleIfError(56) + .setOnlyIfCached(true) + .build()), + HeaderMatcher.same("Cache-Control", "max-age=12, max-stale=23, " + + "min-fresh=34, no-cache, no-store, only-if-cached, stale-if-error=56")); + assertThat(generator.generate( + RequestCacheControl.builder() + .setMaxAge(0) + .build()), + HeaderMatcher.same("Cache-Control", "max-age=0")); + assertThat(generator.generate( + RequestCacheControl.builder() + .setMaxAge(-1) + .setMinFresh(10) + .build()), + HeaderMatcher.same("Cache-Control", "min-fresh=10")); + } + + @Test + public void testGenerateRequestCacheControlHeaderNoDirectives() { + final RequestCacheControl cacheControl = RequestCacheControl.builder() + .build(); + Assertions.assertNull(generator.generate(cacheControl)); + } + +} diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestCachingExecChain.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestCachingExecChain.java index b1616c331..4d96eedc5 100644 --- a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestCachingExecChain.java +++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestCachingExecChain.java @@ -27,7 +27,6 @@ package org.apache.hc.client5.http.impl.cache; -import static org.junit.jupiter.api.Assertions.assertSame; import static org.mockito.Mockito.mock; import java.io.IOException; @@ -50,7 +49,6 @@ import org.apache.hc.client5.http.classic.ExecChain; import org.apache.hc.client5.http.classic.ExecRuntime; import org.apache.hc.client5.http.classic.methods.HttpGet; import org.apache.hc.client5.http.classic.methods.HttpOptions; -import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.client5.http.utils.DateUtils; import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ClassicHttpResponse; @@ -91,6 +89,7 @@ public class TestCachingExecChain { HttpCache cache; CachingExec impl; CacheConfig customConfig; + ExecChain.Scope scope; @BeforeEach public void setUp() { @@ -101,6 +100,7 @@ public class TestCachingExecChain { context = HttpCacheContext.create(); entry = HttpTestUtils.makeCacheEntry(); customConfig = CacheConfig.DEFAULT; + scope = new ExecChain.Scope("test", route, request, mockExecRuntime, context); cache = Mockito.spy(new BasicHttpCache()); @@ -900,7 +900,7 @@ public class TestCachingExecChain { originResponse.setHeader("Date", DateUtils.formatStandardDate(responseGenerated)); originResponse.setHeader("ETag", "\"etag\""); - impl.cacheAndReturnResponse("exchange-id", host, request, originResponse, requestSent, responseReceived); + impl.cacheAndReturnResponse(host, request, scope, originResponse, requestSent, responseReceived); Mockito.verify(cache, Mockito.never()).store( Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); @@ -936,7 +936,7 @@ public class TestCachingExecChain { Mockito.eq(requestSent), Mockito.eq(responseReceived))).thenReturn(new CacheHit("key", httpCacheEntry)); - impl.cacheAndReturnResponse("exchange-id", host, request, originResponse, requestSent, responseReceived); + impl.cacheAndReturnResponse(host, request, scope, originResponse, requestSent, responseReceived); Mockito.verify(mockCache).store( Mockito.any(), @@ -956,45 +956,6 @@ public class TestCachingExecChain { Assertions.assertEquals(HttpStatus.SC_GATEWAY_TIMEOUT, resp.getCode()); } - @Test - public void testSetsRequestInContextOnCacheHit() throws Exception { - final ClassicHttpResponse response = HttpTestUtils.make200Response(); - response.setHeader("Cache-Control", "max-age=3600"); - Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(request), Mockito.any())).thenReturn(response); - - final HttpClientContext ctx = HttpClientContext.create(); - impl.execute(request, new ExecChain.Scope("test", route, request, mockExecRuntime, context), mockExecChain); - impl.execute(request, new ExecChain.Scope("test", route, request, mockExecRuntime, ctx), mockExecChain); - if (!HttpTestUtils.equivalent(request, ctx.getRequest())) { - assertSame(request, ctx.getRequest()); - } - } - - @Test - public void testSetsResponseInContextOnCacheHit() throws Exception { - final ClassicHttpResponse response = HttpTestUtils.make200Response(); - response.setHeader("Cache-Control", "max-age=3600"); - Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(request), Mockito.any())).thenReturn(response); - - final HttpClientContext ctx = HttpClientContext.create(); - impl.execute(request, new ExecChain.Scope("test", route, request, mockExecRuntime, context), mockExecChain); - final ClassicHttpResponse result = impl.execute(request, new ExecChain.Scope("test", route, request, mockExecRuntime, ctx), null); - if (!HttpTestUtils.equivalent(result, ctx.getResponse())) { - assertSame(result, ctx.getResponse()); - } - } - - @Test - public void testSetsRequestSentInContextOnCacheHit() throws Exception { - final ClassicHttpResponse response = HttpTestUtils.make200Response(); - response.setHeader("Cache-Control", "max-age=3600"); - Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(request), Mockito.any())).thenReturn(response); - - final HttpClientContext ctx = HttpClientContext.create(); - impl.execute(request, new ExecChain.Scope("test", route, request, mockExecRuntime, context), mockExecChain); - impl.execute(request, new ExecChain.Scope("test", route, request, mockExecRuntime, ctx), mockExecChain); - } - @Test public void testCanCacheAResponseWithoutABody() throws Exception { final ClassicHttpResponse response = new BasicClassicHttpResponse(HttpStatus.SC_NO_CONTENT, "No Content"); @@ -1002,8 +963,8 @@ public class TestCachingExecChain { response.setHeader("Cache-Control", "max-age=300"); Mockito.when(mockExecChain.proceed(RequestEquivalent.eq(request), Mockito.any())).thenReturn(response); - impl.execute(request, new ExecChain.Scope("test", route, request, mockExecRuntime, context), mockExecChain); - impl.execute(request, new ExecChain.Scope("test", route, request, mockExecRuntime, context), mockExecChain); + impl.execute(request, scope, mockExecChain); + impl.execute(request, scope, mockExecChain); Mockito.verify(mockExecChain).proceed(Mockito.any(), Mockito.any()); } @@ -1148,16 +1109,16 @@ public class TestCachingExecChain { response.setHeader("Cache-Control", "max-age=3600"); Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(response); - impl.execute(request, new ExecChain.Scope("test", route, request, mockExecRuntime, context), mockExecChain); + impl.execute(request, scope, mockExecChain); Mockito.verify(mockExecChain, Mockito.times(1)).proceed(Mockito.any(), Mockito.any()); request.setAuthority(new URIAuthority("bar.example.com")); - impl.execute(request, new ExecChain.Scope("test", route, request, mockExecRuntime, context), mockExecChain); + impl.execute(request, scope, mockExecChain); Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any()); - impl.execute(request, new ExecChain.Scope("test", route, request, mockExecRuntime, context), mockExecChain); + impl.execute(request, scope, mockExecChain); Mockito.verify(mockExecChain, Mockito.times(2)).proceed(Mockito.any(), Mockito.any()); } @@ -1255,8 +1216,6 @@ public class TestCachingExecChain { backendResponse.setHeader("Cache-Control", "public, max-age=3600"); backendResponse.setHeader("ETag", "\"etag\""); - final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, mockExecRuntime, context); - final Header[] headers = new Header[5]; for (int i = 0; i < headers.length; i++) { headers[i] = new BasicHeader("header" + i, "value" + i); @@ -1274,7 +1233,7 @@ public class TestCachingExecChain { .thenReturn(new CacheHit("key", cacheEntry)); // Call cacheAndReturnResponse with 304 Not Modified response - final ClassicHttpResponse cachedResponse = impl.cacheAndReturnResponse("exchange-id", host, request, backendResponse, requestSent, responseReceived); + final ClassicHttpResponse cachedResponse = impl.cacheAndReturnResponse(host, request, scope, backendResponse, requestSent, responseReceived); // Verify cache entry is updated Mockito.verify(mockCache).update(