From 3f81f21cab1ab437353f53255179c55c2664fe8b Mon Sep 17 00:00:00 2001 From: Oleg Kalnichevski Date: Mon, 15 May 2023 22:18:17 +0200 Subject: [PATCH] Request / response Cache-Control APIs --- .../client5/http/cache/HeaderConstants.java | 21 + .../client5/http/impl/cache/CacheControl.java | 205 +--------- .../impl/cache/CacheControlHeaderParser.java | 184 +++++---- .../http/impl/cache/CachingExecBase.java | 2 +- .../http/impl/cache/RequestCacheControl.java | 228 +++++++++++ .../http/impl/cache/ResponseCacheControl.java | 382 ++++++++++++++++++ .../impl/cache/ResponseCachingPolicy.java | 20 +- .../impl/cache/CacheControlParserTest.java | 69 ++-- .../impl/cache/TestResponseCachingPolicy.java | 2 +- 9 files changed, 807 insertions(+), 306 deletions(-) create mode 100644 httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/RequestCacheControl.java create mode 100644 httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/ResponseCacheControl.java diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/HeaderConstants.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/HeaderConstants.java index e45c07206..f3a86017e 100644 --- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/HeaderConstants.java +++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/HeaderConstants.java @@ -56,10 +56,20 @@ public class HeaderConstants { public static final String VARY = "Vary"; public static final String ALLOW = "Allow"; public static final String VIA = "Via"; + /** + * @deprecated Use {@link #CACHE_CONTROL_PUBLIC} + */ + @Deprecated public static final String PUBLIC = "public"; + /** + * @deprecated Use {@link #CACHE_CONTROL_PRIVATE} + */ + @Deprecated public static final String PRIVATE = "private"; public static final String CACHE_CONTROL = "Cache-Control"; + public static final String CACHE_CONTROL_PUBLIC = "public"; + public static final String CACHE_CONTROL_PRIVATE = "private"; public static final String CACHE_CONTROL_NO_STORE = "no-store"; public static final String CACHE_CONTROL_NO_CACHE = "no-cache"; public static final String CACHE_CONTROL_MAX_AGE = "max-age"; @@ -68,7 +78,18 @@ public class HeaderConstants { public static final String CACHE_CONTROL_MIN_FRESH = "min-fresh"; public static final String CACHE_CONTROL_MUST_REVALIDATE = "must-revalidate"; public static final String CACHE_CONTROL_PROXY_REVALIDATE = "proxy-revalidate"; + public static final String CACHE_CONTROL_STALE_IF_ERROR = "stale-if-error"; + public static final String CACHE_CONTROL_STALE_WHILE_REVALIDATE = "stale-while-revalidate"; + public static final String CACHE_CONTROL_ONLY_IF_CACHED = "only-if-cached"; + /** + * @deprecated Use {@link #CACHE_CONTROL_STALE_IF_ERROR} + */ + @Deprecated public static final String STALE_IF_ERROR = "stale-if-error"; + /** + * @deprecated Use {@link #CACHE_CONTROL_STALE_WHILE_REVALIDATE} + */ + @Deprecated public static final String STALE_WHILE_REVALIDATE = "stale-while-revalidate"; public static final String WARNING = "Warning"; diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheControl.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheControl.java index dadd80152..8ad37633d 100644 --- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheControl.java +++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheControl.java @@ -31,229 +31,42 @@ import org.apache.hc.core5.annotation.Contract; import org.apache.hc.core5.annotation.Internal; import org.apache.hc.core5.annotation.ThreadingBehavior; -import java.util.Collections; -import java.util.Set; - /** - * Represents the values of the Cache-Control header in an HTTP response, which indicate whether and for how long + * Represents the values of the Cache-Control header in an HTTP message, which indicate whether and for how long * the response can be cached by the client and intermediary proxies. - *

- * The class provides methods to retrieve the maximum age of the response and the maximum age that applies to shared - * caches. The values are expressed in seconds, with -1 indicating that the value was not specified in the header. - *

- * Instances of this class are immutable, meaning that their values cannot be changed once they are set. To create an - * instance, use one of the constructors that take the desired values as arguments. Alternatively, use the default - * constructor to create an instance with both values set to -1, indicating that the header was not present in the - * response. - *

- * Example usage: - *

- * HttpResponse response = httpClient.execute(httpGet);
- * CacheControlHeader cacheControlHeader = CacheControlHeaderParser.INSTANCE.parse(response.getHeaders("Cache-Control"));
- * long maxAge = cacheControlHeader.getMaxAge();
- * long sharedMaxAge = cacheControlHeader.getSharedMaxAge();
- * 
+ * * @since 5.3 */ @Internal @Contract(threading = ThreadingBehavior.IMMUTABLE) -final class CacheControl { - - /** - * The max-age directive value. - */ - private final long maxAge; - /** - * The shared-max-age directive value. - */ - private final long sharedMaxAge; - /** - * The isNoCache flag indicates whether the Cache-Control header includes the no-cache directive. - */ - private final boolean noCache; - /** - * The isNoStore flag indicates whether the Cache-Control header includes the no-store directive. - */ - private final boolean noStore; - /** - * The isPrivate flag indicates whether the Cache-Control header includes the private directive. - */ - private final boolean cachePrivate; - /** - * Indicates whether the Cache-Control header includes the "must-revalidate" directive. - */ - private final boolean mustRevalidate; - /** - * Indicates whether the Cache-Control header includes the "proxy-revalidate" directive. - */ - private final boolean proxyRevalidate; - /** - * Indicates whether the Cache-Control header includes the "public" directive. - */ - private final boolean cachePublic; - /** - * The number of seconds that a stale response is considered fresh for the purpose - * of serving a response while a revalidation request is made to the origin server. - */ - private final long staleWhileRevalidate; - - /** - * A set of field names specified in the "no-cache" directive of the Cache-Control header. - */ - private final Set noCacheFields; - - - /** - * Creates a new instance of {@code CacheControl} with default values. - * The default values are: max-age=-1, shared-max-age=-1, must-revalidate=false, no-cache=false, - * no-store=false, private=false, proxy-revalidate=false, public=false, stale_while_revalidate=-1, - * and noCacheFields as an empty set. - */ - public CacheControl() { - this(-1, -1, false, false, false, false, false, false,-1, Collections.emptySet()); - } - - /** - * Creates a new instance of {@code CacheControl} with the specified max-age, shared-max-age, no-cache, no-store, - * private, must-revalidate, proxy-revalidate, and public values. - * - * @param maxAge The max-age value from the Cache-Control header. - * @param sharedMaxAge The shared-max-age value from the Cache-Control header. - * @param mustRevalidate The must-revalidate value from the Cache-Control header. - * @param noCache The no-cache value from the Cache-Control header. - * @param noStore The no-store value from the Cache-Control header. - * @param cachePrivate The private value from the Cache-Control header. - * @param proxyRevalidate The proxy-revalidate value from the Cache-Control header. - * @param cachePublic The public value from the Cache-Control header. - * @param noCacheFields The set of field names specified in the "no-cache" directive of the Cache-Control header. - */ - public CacheControl(final long maxAge, final long sharedMaxAge, final boolean mustRevalidate, final boolean noCache, final boolean noStore, - final boolean cachePrivate, final boolean proxyRevalidate, final boolean cachePublic, - final long staleWhileRevalidate, - final Set noCacheFields) { - this.maxAge = maxAge; - this.sharedMaxAge = sharedMaxAge; - this.noCache = noCache; - this.noStore = noStore; - this.cachePrivate = cachePrivate; - this.mustRevalidate = mustRevalidate; - this.proxyRevalidate = proxyRevalidate; - this.cachePublic = cachePublic; - this.staleWhileRevalidate = staleWhileRevalidate; - this.noCacheFields = Collections.unmodifiableSet(noCacheFields); - } - +interface CacheControl { /** * Returns the max-age value from the Cache-Control header. * * @return The max-age value. */ - public long getMaxAge() { - return maxAge; - } - - /** - * Returns the shared-max-age value from the Cache-Control header. - * - * @return The shared-max-age value. - */ - public long getSharedMaxAge() { - return sharedMaxAge; - } + long getMaxAge(); /** * Returns the no-cache flag from the Cache-Control header. * * @return The no-cache flag. */ - public boolean isNoCache() { - return noCache; - } + boolean isNoCache(); /** * Returns the no-store flag from the Cache-Control header. * * @return The no-store flag. */ - public boolean isNoStore() { - return noStore; - } + boolean isNoStore(); /** - * Returns the private flag from the Cache-Control header. + * Returns the stale-if-error value from the Cache-Control header. * - * @return The private flag. + * @return The stale-if-error value. */ - public boolean isCachePrivate() { - return cachePrivate; - } + long getStaleIfError(); - /** - * Returns whether the must-revalidate directive is present in the Cache-Control header. - * - * @return {@code true} if the must-revalidate directive is present, otherwise {@code false} - */ - public boolean isMustRevalidate() { - return mustRevalidate; - } - - - /** - * Returns whether the proxy-revalidate value is set in the Cache-Control header. - * - * @return {@code true} if proxy-revalidate is set, {@code false} otherwise. - */ - public boolean isProxyRevalidate() { - return proxyRevalidate; - } - - /** - * Returns whether the public value is set in the Cache-Control header. - * - * @return {@code true} if public is set, {@code false} otherwise. - */ - public boolean isPublic() { - return cachePublic; - } - - /** - * Returns the stale-while-revalidate value from the Cache-Control header. - * - * @return The stale-while-revalidate value. - */ - public long getStaleWhileRevalidate() { - return staleWhileRevalidate; - } - - /** - * Returns an unmodifiable set of field names specified in the "no-cache" directive of the Cache-Control header. - * - * @return The set of field names specified in the "no-cache" directive. - */ - public Set getNoCacheFields() { - return noCacheFields; - } - - /** - * Returns a string representation of the {@code CacheControl} object, including the max-age, shared-max-age, no-cache, - * no-store, private, must-revalidate, proxy-revalidate, public values, and noCacheFields. - * - * @return A string representation of the object. - */ - @Override - public String toString() { - return "CacheControl{" + - "maxAge=" + maxAge + - ", sharedMaxAge=" + sharedMaxAge + - ", noCache=" + noCache + - ", noCacheFields=" + noCacheFields + - ", noStore=" + noStore + - ", cachePrivate=" + cachePrivate + - ", mustRevalidate=" + mustRevalidate + - ", proxyRevalidate=" + proxyRevalidate + - ", cachePublic=" + cachePublic + - ", stale_while_revalidate=" + staleWhileRevalidate + - '}'; - } } \ No newline at end of file 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 03aca745f..c7cce11ec 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 @@ -30,6 +30,7 @@ import java.util.BitSet; import java.util.HashSet; import java.util.Iterator; import java.util.Set; +import java.util.function.BiConsumer; import org.apache.hc.client5.http.cache.HeaderConstants; import org.apache.hc.core5.annotation.Contract; @@ -37,6 +38,9 @@ import org.apache.hc.core5.annotation.Internal; import org.apache.hc.core5.annotation.ThreadingBehavior; import org.apache.hc.core5.http.FormattedHeader; import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpHeaders; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpResponse; import org.apache.hc.core5.http.message.ParserCursor; import org.apache.hc.core5.util.Args; import org.apache.hc.core5.util.CharArrayBuffer; @@ -51,14 +55,14 @@ import org.slf4j.LoggerFactory; * This class is thread-safe and has a singleton instance ({@link #INSTANCE}). *

*

- * The {@link #parse(Iterator)} method takes an HTTP header and returns a {@link CacheControl} object containing + * The {@link #parseResponse(Iterator)} method takes an HTTP header and returns a {@link ResponseCacheControl} object containing * the relevant caching directives. The header can be either a {@link FormattedHeader} object, which contains a * pre-parsed {@link CharArrayBuffer}, or a plain {@link Header} object, in which case the value will be parsed and * stored in a new {@link CharArrayBuffer}. *

*

* This parser only supports two directives: "max-age" and "s-maxage". If either of these directives are present in the - * header, their values will be parsed and stored in the {@link CacheControl} object. If both directives are + * header, their values will be parsed and stored in the {@link ResponseCacheControl} object. If both directives are * present, the value of "s-maxage" takes precedence. *

*/ @@ -102,39 +106,7 @@ class CacheControlHeaderParser { this.tokenParser = Tokenizer.INSTANCE; } - /** - * Parses the specified header and returns a new {@link CacheControl} instance containing the relevant caching - *

- * directives. - * - *

The returned {@link CacheControl} instance will contain the values for "max-age" and "s-maxage" caching - * directives parsed from the input header. If the input header does not contain any caching directives or if the - *

- * directives are malformed, the returned {@link CacheControl} instance will have default values for "max-age" and - *

- * "s-maxage" (-1).

- * - * @param headerIterator the header to parse, cannot be {@code null} - * @return a new {@link CacheControl} instance containing the relevant caching directives parsed from the header - * @throws IllegalArgumentException if the input header is {@code null} - */ - public final CacheControl parse(final Iterator
headerIterator) { - Args.notNull(headerIterator, "headerIterator"); - - // Initialize variables to hold the Cache-Control directives - long maxAge = -1; - long sharedMaxAge = -1; - boolean noCache = false; - boolean noStore = false; - boolean cachePrivate = false; - boolean mustRevalidate = false; - boolean proxyRevalidate = false; - boolean cachePublic = false; - long staleWhileRevalidate = -1; - // Declare a new Set variable at the beginning of the method to store the field-names - final Set noCacheFields = new HashSet<>(); - - // Iterate over each header + public void parse(final Iterator
headerIterator, final BiConsumer consumer) { while (headerIterator.hasNext()) { final Header header = headerIterator.next(); final CharArrayBuffer buffer; @@ -145,7 +117,7 @@ class CacheControlHeaderParser { } else { final String s = header.getValue(); if (s == null) { - return new CacheControl(); + return; } buffer = new CharArrayBuffer(s.length()); buffer.append(s); @@ -166,53 +138,121 @@ class CacheControlHeaderParser { } } } - // Update the Cache-Control directives based on the current header - if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_S_MAX_AGE)) { - sharedMaxAge = parseSeconds(name, value); - } else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_MAX_AGE)) { - maxAge = parseSeconds(name, value); - } else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_MUST_REVALIDATE)) { - mustRevalidate = true; - } else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_NO_CACHE)) { - noCache = true; - if (value != null) { - final Tokenizer.Cursor valCursor = new ParserCursor(0, value.length()); - while (!valCursor.atEnd()) { - final String token = tokenParser.parseToken(value, valCursor, VALUE_DELIMS); - if (!TextUtils.isBlank(token)) { - noCacheFields.add(token); - } - if (!valCursor.atEnd()) { - valCursor.updatePos(valCursor.getPos() + 1); - } - } - } - } else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_NO_STORE)) { - noStore = true; - } else if (name.equalsIgnoreCase(HeaderConstants.PRIVATE)) { - cachePrivate = true; - } else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_PROXY_REVALIDATE)) { - proxyRevalidate = true; - } else if (name.equalsIgnoreCase(HeaderConstants.PUBLIC)) { - cachePublic = true; - } else if (name.equalsIgnoreCase(HeaderConstants.STALE_WHILE_REVALIDATE)) { - staleWhileRevalidate = parseSeconds(name, value); - } + consumer.accept(name, value); } } - // Return a single CacheControl object with the combined directives - return new CacheControl(maxAge, sharedMaxAge, mustRevalidate, noCache, noStore, cachePrivate, proxyRevalidate, cachePublic, staleWhileRevalidate, noCacheFields); + } + + /** + * Parses the specified response header and returns a new {@link ResponseCacheControl} instance containing + * the relevant caching directives. + * + *

The returned {@link ResponseCacheControl} instance will contain the values for "max-age" and "s-maxage" + * caching directives parsed from the input header. If the input header does not contain any caching directives + * or if the directives are malformed, the returned {@link ResponseCacheControl} instance will have default values + * for "max-age" and "s-maxage" (-1).

+ * + * @param headerIterator the header to parse, cannot be {@code null} + * @return a new {@link ResponseCacheControl} instance containing the relevant caching directives parsed + * from the response header + * @throws IllegalArgumentException if the input header is {@code null} + */ + public final ResponseCacheControl parseResponse(final Iterator
headerIterator) { + Args.notNull(headerIterator, "headerIterator"); + final ResponseCacheControl.Builder builder = new ResponseCacheControl.Builder(); + parse(headerIterator, (name, value) -> { + if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_S_MAX_AGE)) { + builder.setSharedMaxAge(parseSeconds(name, value)); + } else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_MAX_AGE)) { + builder.setMaxAge(parseSeconds(name, value)); + } else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_MUST_REVALIDATE)) { + builder.setMustRevalidate(true); + } else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_NO_CACHE)) { + builder.setNoCache(true); + if (value != null) { + final Tokenizer.Cursor valCursor = new ParserCursor(0, value.length()); + final Set noCacheFields = new HashSet<>(); + while (!valCursor.atEnd()) { + final String token = tokenParser.parseToken(value, valCursor, VALUE_DELIMS); + if (!TextUtils.isBlank(token)) { + noCacheFields.add(token); + } + if (!valCursor.atEnd()) { + valCursor.updatePos(valCursor.getPos() + 1); + } + } + builder.setNoCacheFields(noCacheFields); + } + } else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_NO_STORE)) { + builder.setNoStore(true); + } else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_PRIVATE)) { + builder.setCachePrivate(true); + } else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_PROXY_REVALIDATE)) { + builder.setProxyRevalidate(true); + } else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_PUBLIC)) { + builder.setCachePublic(true); + } else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_STALE_WHILE_REVALIDATE)) { + builder.setStaleWhileRevalidate(parseSeconds(name, value)); + } else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_STALE_IF_ERROR)) { + builder.setStaleIfError(parseSeconds(name, value)); + } + }); + return builder.build(); + } + + public final ResponseCacheControl parse(final HttpResponse response) { + return parseResponse(response.headerIterator(HttpHeaders.CACHE_CONTROL)); + } + + /** + * Parses the specified request header and returns a new {@link RequestCacheControl} instance containing + * the relevant caching directives. + * + * @param headerIterator the header to parse, cannot be {@code null} + * @return a new {@link RequestCacheControl} instance containing the relevant caching directives parsed + * from the request header + * @throws IllegalArgumentException if the input header is {@code null} + */ + public final RequestCacheControl parseRequest(final Iterator
headerIterator) { + Args.notNull(headerIterator, "headerIterator"); + final RequestCacheControl.Builder builder = new RequestCacheControl.Builder(); + parse(headerIterator, (name, value) -> { + if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_MAX_AGE)) { + builder.setMaxAge(parseSeconds(name, value)); + } else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_MAX_STALE)) { + builder.setMaxStale(parseSeconds(name, value)); + } else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_MIN_FRESH)) { + builder.setMinFresh(parseSeconds(name, value)); + } else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_NO_STORE)) { + builder.setNoStore(true); + } else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_NO_CACHE)) { + builder.setNoCache(true); + } else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_ONLY_IF_CACHED)) { + builder.setOnlyIfCached(true); + } else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_STALE_IF_ERROR)) { + builder.setStaleIfError(parseSeconds(name, value)); + } + }); + return builder.build(); + } + + public final RequestCacheControl parse(final HttpRequest request) { + return parseRequest(request.headerIterator(HttpHeaders.CACHE_CONTROL)); } private static long parseSeconds(final String name, final String value) { + if (TextUtils.isEmpty(value)) { + return -1; + } try { - return value != null ? Long.parseLong(value) : -1; + final long n = Long.parseLong(value); + return n < 0 ? -1 : n; } catch (final NumberFormatException e) { if (LOG.isDebugEnabled()) { LOG.debug("Directive {} was malformed: {}", name, value); } - return -1; } + return 0; } } 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 2790e9894..481f27195 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 @@ -407,7 +407,7 @@ public class CachingExecBase { if (it == null || !it.hasNext()) { return false; } else { - final CacheControl cacheControl = CacheControlHeaderParser.INSTANCE.parse(it); + final ResponseCacheControl cacheControl = CacheControlHeaderParser.INSTANCE.parseResponse(it); return cacheControl.isNoCache(); } } diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/RequestCacheControl.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/RequestCacheControl.java new file mode 100644 index 000000000..a1f442ab1 --- /dev/null +++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/RequestCacheControl.java @@ -0,0 +1,228 @@ +/* + * ==================================================================== + * 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 org.apache.hc.core5.annotation.Contract; +import org.apache.hc.core5.annotation.Internal; +import org.apache.hc.core5.annotation.ThreadingBehavior; + +/** + * Represents the values of the Cache-Control header in an HTTP request, which indicate whether and for how long + * the client is willing to cache the response. + * + * @since 5.3 + */ +@Internal +@Contract(threading = ThreadingBehavior.IMMUTABLE) +final class RequestCacheControl implements CacheControl { + + private final long maxAge; + private final long maxStale; + private final long minFresh; + private final boolean noCache; + private final boolean noStore; + private final boolean onlyIfCached; + private final long staleIfError; + + RequestCacheControl(final long maxAge, final long maxStale, final long minFresh, final boolean noCache, + final boolean noStore, final boolean onlyIfCached, final long staleIfError) { + this.maxAge = maxAge; + this.maxStale = maxStale; + this.minFresh = minFresh; + this.noCache = noCache; + this.noStore = noStore; + this.onlyIfCached = onlyIfCached; + this.staleIfError = staleIfError; + } + + /** + * Returns the max-age value from the Cache-Control header. + * + * @return The max-age value. + */ + @Override + public long getMaxAge() { + return maxAge; + } + + /** + * Returns the max-stale value from the Cache-Control header. + * + * @return The max-stale value. + */ + public long getMaxStale() { + return maxStale; + } + + /** + * Returns the min-fresh value from the Cache-Control header. + * + * @return The min-fresh value. + */ + public long getMinFresh() { + return minFresh; + } + + /** + * Returns the no-cache flag from the Cache-Control header. + * + * @return The no-cache flag. + */ + @Override + public boolean isNoCache() { + return noCache; + } + + /** + * Returns the no-store flag from the Cache-Control header. + * + * @return The no-store flag. + */ + @Override + public boolean isNoStore() { + return noStore; + } + + /** + * Returns the only-if-cached flag from the Cache-Control header. + * + * @return The only-if-cached flag. + */ + public boolean isOnlyIfCached() { + return onlyIfCached; + } + + /** + * Returns the stale-if-error value from the Cache-Control header. + * + * @return The stale-if-error value. + */ + @Override + public long getStaleIfError() { + return staleIfError; + } + + @Override + public String toString() { + return "RequestCacheControl{" + + "maxAge=" + maxAge + + ", maxStale=" + maxStale + + ", minFresh=" + minFresh + + ", noCache=" + noCache + + ", noStore=" + noStore + + ", onlyIfCached=" + onlyIfCached + + ", staleIfError=" + staleIfError + + '}'; + } + + static Builder builder() { + return new Builder(); + } + + static class Builder { + + private long maxAge = -1; + private long maxStale = -1; + private long minFresh = -1; + private boolean noCache; + private boolean noStore; + private boolean onlyIfCached; + private long staleIfError = -1; + + Builder() { + } + + public long getMaxAge() { + return maxAge; + } + + public Builder setMaxAge(final long maxAge) { + this.maxAge = maxAge; + return this; + } + + public long getMaxStale() { + return maxStale; + } + + public Builder setMaxStale(final long maxStale) { + this.maxStale = maxStale; + return this; + } + + public long getMinFresh() { + return minFresh; + } + + public Builder setMinFresh(final long minFresh) { + this.minFresh = minFresh; + return this; + } + + public boolean isNoCache() { + return noCache; + } + + public Builder setNoCache(final boolean noCache) { + this.noCache = noCache; + return this; + } + + public boolean isNoStore() { + return noStore; + } + + public Builder setNoStore(final boolean noStore) { + this.noStore = noStore; + return this; + } + + public boolean isOnlyIfCached() { + return onlyIfCached; + } + + public Builder setOnlyIfCached(final boolean onlyIfCached) { + this.onlyIfCached = onlyIfCached; + return this; + } + + public long getStaleIfError() { + return staleIfError; + } + + public Builder setStaleIfError(final long staleIfError) { + this.staleIfError = staleIfError; + return this; + } + + public RequestCacheControl build() { + return new RequestCacheControl(maxAge, maxStale, minFresh, noCache, noStore, onlyIfCached, staleIfError); + } + + } +} \ No newline at end of file diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/ResponseCacheControl.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/ResponseCacheControl.java new file mode 100644 index 000000000..845fcc702 --- /dev/null +++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/ResponseCacheControl.java @@ -0,0 +1,382 @@ +/* + * ==================================================================== + * 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.Collections; +import java.util.Set; + +import org.apache.hc.core5.annotation.Contract; +import org.apache.hc.core5.annotation.Internal; +import org.apache.hc.core5.annotation.ThreadingBehavior; + +/** + * Represents the values of the Cache-Control header in an HTTP response, which indicate whether and for how long + * the response can be cached by the client and intermediary proxies. + *

+ * The class provides methods to retrieve the maximum age of the response and the maximum age that applies to shared + * caches. The values are expressed in seconds, with -1 indicating that the value was not specified in the header. + *

+ * Instances of this class are immutable, meaning that their values cannot be changed once they are set. To create an + * instance, use one of the constructors that take the desired values as arguments. Alternatively, use the default + * constructor to create an instance with both values set to -1, indicating that the header was not present in the + * response. + * + * @since 5.3 + */ +@Internal +@Contract(threading = ThreadingBehavior.IMMUTABLE) +final class ResponseCacheControl implements CacheControl { + + /** + * The max-age directive value. + */ + private final long maxAge; + /** + * The shared-max-age directive value. + */ + private final long sharedMaxAge; + /** + * The isNoCache flag indicates whether the Cache-Control header includes the no-cache directive. + */ + private final boolean noCache; + /** + * The isNoStore flag indicates whether the Cache-Control header includes the no-store directive. + */ + private final boolean noStore; + /** + * The isPrivate flag indicates whether the Cache-Control header includes the private directive. + */ + private final boolean cachePrivate; + /** + * Indicates whether the Cache-Control header includes the "must-revalidate" directive. + */ + private final boolean mustRevalidate; + /** + * Indicates whether the Cache-Control header includes the "proxy-revalidate" directive. + */ + private final boolean proxyRevalidate; + /** + * Indicates whether the Cache-Control header includes the "public" directive. + */ + private final boolean cachePublic; + /** + * The number of seconds that a stale response is considered fresh for the purpose + * of serving a response while a revalidation request is made to the origin server. + */ + private final long staleWhileRevalidate; + /** + * The number of seconds that a cached stale response MAY be used to satisfy the request, + * regardless of other freshness information.. + */ + private final long staleIfError; + /** + * A set of field names specified in the "no-cache" directive of the Cache-Control header. + */ + private final Set noCacheFields; + + /** + * Creates a new instance of {@code CacheControl} with the specified values. + * + * @param maxAge The max-age value from the Cache-Control header. + * @param sharedMaxAge The shared-max-age value from the Cache-Control header. + * @param mustRevalidate The must-revalidate value from the Cache-Control header. + * @param noCache The no-cache value from the Cache-Control header. + * @param noStore The no-store value from the Cache-Control header. + * @param cachePrivate The private value from the Cache-Control header. + * @param proxyRevalidate The proxy-revalidate value from the Cache-Control header. + * @param cachePublic The public value from the Cache-Control header. + * @param staleWhileRevalidate The stale-while-revalidate value from the Cache-Control header. + * @param staleIfError The stale-if-error value from the Cache-Control header. + * @param noCacheFields The set of field names specified in the "no-cache" directive of the Cache-Control header. + */ + ResponseCacheControl(final long maxAge, final long sharedMaxAge, final boolean mustRevalidate, final boolean noCache, + final boolean noStore, final boolean cachePrivate, final boolean proxyRevalidate, + final boolean cachePublic, final long staleWhileRevalidate, final long staleIfError, + final Set noCacheFields) { + this.maxAge = maxAge; + this.sharedMaxAge = sharedMaxAge; + this.noCache = noCache; + this.noStore = noStore; + this.cachePrivate = cachePrivate; + this.mustRevalidate = mustRevalidate; + this.proxyRevalidate = proxyRevalidate; + this.cachePublic = cachePublic; + this.staleWhileRevalidate = staleWhileRevalidate; + this.staleIfError = staleIfError; + this.noCacheFields = noCacheFields != null ? Collections.unmodifiableSet(noCacheFields) : Collections.emptySet(); + } + + /** + * Returns the max-age value from the Cache-Control header. + * + * @return The max-age value. + */ + @Override + public long getMaxAge() { + return maxAge; + } + + /** + * Returns the shared-max-age value from the Cache-Control header. + * + * @return The shared-max-age value. + */ + public long getSharedMaxAge() { + return sharedMaxAge; + } + + /** + * Returns the no-cache flag from the Cache-Control header. + * + * @return The no-cache flag. + */ + @Override + public boolean isNoCache() { + return noCache; + } + + /** + * Returns the no-store flag from the Cache-Control header. + * + * @return The no-store flag. + */ + @Override + public boolean isNoStore() { + return noStore; + } + + /** + * Returns the private flag from the Cache-Control header. + * + * @return The private flag. + */ + public boolean isCachePrivate() { + return cachePrivate; + } + + /** + * Returns whether the must-revalidate directive is present in the Cache-Control header. + * + * @return {@code true} if the must-revalidate directive is present, otherwise {@code false} + */ + public boolean isMustRevalidate() { + return mustRevalidate; + } + + + /** + * Returns whether the proxy-revalidate value is set in the Cache-Control header. + * + * @return {@code true} if proxy-revalidate is set, {@code false} otherwise. + */ + public boolean isProxyRevalidate() { + return proxyRevalidate; + } + + /** + * Returns whether the public value is set in the Cache-Control header. + * + * @return {@code true} if public is set, {@code false} otherwise. + */ + public boolean isPublic() { + return cachePublic; + } + + /** + * Returns the stale-while-revalidate value from the Cache-Control header. + * + * @return The stale-while-revalidate value. + */ + public long getStaleWhileRevalidate() { + return staleWhileRevalidate; + } + + /** + * Returns the stale-if-error value from the Cache-Control header. + * + * @return The stale-if-error value. + */ + @Override + public long getStaleIfError() { + return staleIfError; + } + + /** + * Returns an unmodifiable set of field names specified in the "no-cache" directive of the Cache-Control header. + * + * @return The set of field names specified in the "no-cache" directive. + */ + public Set getNoCacheFields() { + return noCacheFields; + } + + @Override + public String toString() { + return "CacheControl{" + + "maxAge=" + maxAge + + ", sharedMaxAge=" + sharedMaxAge + + ", noCache=" + noCache + + ", noStore=" + noStore + + ", cachePrivate=" + cachePrivate + + ", mustRevalidate=" + mustRevalidate + + ", proxyRevalidate=" + proxyRevalidate + + ", cachePublic=" + cachePublic + + ", staleWhileRevalidate=" + staleWhileRevalidate + + ", staleIfError=" + staleIfError + + ", noCacheFields=" + noCacheFields + + '}'; + } + + static Builder create() { + return new Builder(); + } + + static class Builder { + + private long maxAge = -1; + private long sharedMaxAge = -1; + private boolean noCache; + private boolean noStore; + private boolean cachePrivate; + private boolean mustRevalidate; + private boolean proxyRevalidate; + private boolean cachePublic; + private long staleWhileRevalidate = -1; + private long staleIfError = -1; + private Set noCacheFields; + + Builder() { + } + + public long getMaxAge() { + return maxAge; + } + + public Builder setMaxAge(final long maxAge) { + this.maxAge = maxAge; + return this; + } + + public long getSharedMaxAge() { + return sharedMaxAge; + } + + public Builder setSharedMaxAge(final long sharedMaxAge) { + this.sharedMaxAge = sharedMaxAge; + return this; + } + + public boolean isNoCache() { + return noCache; + } + + public Builder setNoCache(final boolean noCache) { + this.noCache = noCache; + return this; + } + + public boolean isNoStore() { + return noStore; + } + + public Builder setNoStore(final boolean noStore) { + this.noStore = noStore; + return this; + } + + public boolean isCachePrivate() { + return cachePrivate; + } + + public Builder setCachePrivate(final boolean cachePrivate) { + this.cachePrivate = cachePrivate; + return this; + } + + public boolean isMustRevalidate() { + return mustRevalidate; + } + + public Builder setMustRevalidate(final boolean mustRevalidate) { + this.mustRevalidate = mustRevalidate; + return this; + } + + public boolean isProxyRevalidate() { + return proxyRevalidate; + } + + public Builder setProxyRevalidate(final boolean proxyRevalidate) { + this.proxyRevalidate = proxyRevalidate; + return this; + } + + public boolean isCachePublic() { + return cachePublic; + } + + public Builder setCachePublic(final boolean cachePublic) { + this.cachePublic = cachePublic; + return this; + } + + public long getStaleWhileRevalidate() { + return staleWhileRevalidate; + } + + public Builder setStaleWhileRevalidate(final long staleWhileRevalidate) { + this.staleWhileRevalidate = staleWhileRevalidate; + return this; + } + + public long getStaleIfError() { + return staleIfError; + } + + public Builder setStaleIfError(final long staleIfError) { + this.staleIfError = staleIfError; + return this; + } + + public Set getNoCacheFields() { + return noCacheFields; + } + + public Builder setNoCacheFields(final Set noCacheFields) { + this.noCacheFields = noCacheFields; + return this; + } + + public ResponseCacheControl build() { + return new ResponseCacheControl(maxAge, sharedMaxAge, mustRevalidate, noCache, noStore, cachePrivate, proxyRevalidate, + cachePublic, staleWhileRevalidate, staleIfError, noCacheFields); + } + + } + +} \ No newline at end of file diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/ResponseCachingPolicy.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/ResponseCachingPolicy.java index c1180c39f..016b75001 100644 --- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/ResponseCachingPolicy.java +++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/ResponseCachingPolicy.java @@ -166,7 +166,7 @@ class ResponseCachingPolicy { * @param response The origin response * @return {@code true} if response is cacheable */ - public boolean isResponseCacheable(final String httpMethod, final HttpResponse response, final CacheControl cacheControl) { + public boolean isResponseCacheable(final String httpMethod, final HttpResponse response, final ResponseCacheControl cacheControl) { boolean cacheable = false; if (!HeaderConstants.GET_METHOD.equals(httpMethod) && !HeaderConstants.HEAD_METHOD.equals(httpMethod) @@ -285,7 +285,7 @@ class ResponseCachingPolicy { *

* When cacheControl is null, returns false, implying the response is cacheable. */ - protected boolean isExplicitlyNonCacheable(final CacheControl cacheControl) { + protected boolean isExplicitlyNonCacheable(final ResponseCacheControl cacheControl) { if (cacheControl == null) { return false; } else { @@ -313,7 +313,7 @@ class ResponseCachingPolicy { return false; } - protected boolean isExplicitlyCacheable(final HttpResponse response, final CacheControl cacheControl ) { + protected boolean isExplicitlyCacheable(final HttpResponse response, final ResponseCacheControl cacheControl ) { if (response.getFirstHeader(HeaderConstants.EXPIRES) != null) { return true; } @@ -363,7 +363,7 @@ class ResponseCachingPolicy { * @param response the {@link HttpResponse} from the origin * @return {@code true} if response is cacheable */ - public boolean isResponseCacheable(final HttpRequest request, final HttpResponse response, final CacheControl cacheControl) { + public boolean isResponseCacheable(final HttpRequest request, final HttpResponse response, final ResponseCacheControl cacheControl) { final ProtocolVersion version = request.getVersion() != null ? request.getVersion() : HttpVersion.DEFAULT; if (version.compareToVersion(HttpVersion.HTTP_1_1) > 0) { if (LOG.isDebugEnabled()) { @@ -403,7 +403,7 @@ class ResponseCachingPolicy { return isResponseCacheable(method, response, cacheControl); } - private boolean expiresHeaderLessOrEqualToDateHeaderAndNoCacheControl(final HttpResponse response, final CacheControl cacheControl) { + private boolean expiresHeaderLessOrEqualToDateHeaderAndNoCacheControl(final HttpResponse response, final ResponseCacheControl cacheControl) { if (cacheControl != null) { return false; } @@ -464,7 +464,7 @@ class ResponseCachingPolicy { * @param response the HTTP response for which to calculate the freshness lifetime * @return the freshness lifetime of the response, in seconds */ - private Duration calculateFreshnessLifetime(final HttpResponse response, final CacheControl cacheControl) { + private Duration calculateFreshnessLifetime(final HttpResponse response, final ResponseCacheControl cacheControl) { if (cacheControl == null) { // If no cache-control header is present, assume no caching directives and return a default value @@ -516,7 +516,7 @@ class ResponseCachingPolicy { final int statusCode = entry.getStatus(); if (statusCode >= HttpStatus.SC_INTERNAL_SERVER_ERROR && statusCode <= HttpStatus.SC_GATEWAY_TIMEOUT) { // Check if the cached response has a stale-while-revalidate directive - final CacheControl cacheControl = parseCacheControlHeader(entry); + final ResponseCacheControl cacheControl = parseCacheControlHeader(entry); if (cacheControl == null) { return false; } else { @@ -534,12 +534,12 @@ class ResponseCachingPolicy { * @param messageHeaders the HTTP message to parse the header from * @return a CacheControl instance with the parsed directives or default values if the header is not present */ - private CacheControl parseCacheControlHeader(final MessageHeaders messageHeaders) { + private ResponseCacheControl parseCacheControlHeader(final MessageHeaders messageHeaders) { final Iterator

it = messageHeaders.headerIterator(HttpHeaders.CACHE_CONTROL); if (it == null || !it.hasNext()) { return null; } else { - return CacheControlHeaderParser.INSTANCE.parse(it); + return CacheControlHeaderParser.INSTANCE.parseResponse(it); } } @@ -558,7 +558,7 @@ class ResponseCachingPolicy { * @return true if revalidation is required based on the {@code no-cache} directive, {@code false} otherwise. */ boolean responseContainsNoCacheDirective(final HttpCacheEntry entry) { - final CacheControl responseCacheControl = parseCacheControlHeader(entry); + final ResponseCacheControl responseCacheControl = parseCacheControlHeader(entry); if (responseCacheControl != null) { final Set noCacheFields = responseCacheControl.getNoCacheFields(); diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/CacheControlParserTest.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/CacheControlParserTest.java index 1280d4ea5..66c892c48 100644 --- a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/CacheControlParserTest.java +++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/CacheControlParserTest.java @@ -31,13 +31,13 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.Arrays; +import java.util.Collections; + import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.message.BasicHeader; import org.junit.jupiter.api.Test; -import java.util.Arrays; -import java.util.Collections; - public class CacheControlParserTest { private final CacheControlHeaderParser parser = CacheControlHeaderParser.INSTANCE; @@ -45,69 +45,69 @@ public class CacheControlParserTest { @Test public void testParseMaxAgeZero() { final Header header = new BasicHeader("Cache-Control", "max-age=0 , this = stuff;"); - final CacheControl cacheControl = parser.parse(Collections.singletonList(header).iterator()); + final ResponseCacheControl cacheControl = parser.parseResponse(Collections.singletonList(header).iterator()); assertEquals(0L, cacheControl.getMaxAge()); } @Test public void testParseSMaxAge() { final Header header = new BasicHeader("Cache-Control", "s-maxage=3600"); - final CacheControl cacheControl = parser.parse(Collections.singletonList(header).iterator()); + final ResponseCacheControl cacheControl = parser.parseResponse(Collections.singletonList(header).iterator()); assertEquals(3600L, cacheControl.getSharedMaxAge()); } @Test public void testParseInvalidCacheValue() { final Header header = new BasicHeader("Cache-Control", "max-age=invalid"); - final CacheControl cacheControl = parser.parse(Collections.singletonList(header).iterator()); - assertEquals(-1L, cacheControl.getMaxAge()); + final ResponseCacheControl cacheControl = parser.parseResponse(Collections.singletonList(header).iterator()); + assertEquals(0L, cacheControl.getMaxAge()); } @Test public void testParseInvalidHeader() { final Header header = new BasicHeader("Cache-Control", "max-age"); - final CacheControl cacheControl = parser.parse(Collections.singletonList(header).iterator()); + final ResponseCacheControl cacheControl = parser.parseResponse(Collections.singletonList(header).iterator()); assertEquals(-1L, cacheControl.getMaxAge()); } @Test public void testParseNullHeader() { final Header header = null; - assertThrows(NullPointerException.class, () -> parser.parse(Collections.singletonList(header).iterator())); + assertThrows(NullPointerException.class, () -> parser.parseResponse(Collections.singletonList(header).iterator())); } @Test public void testParseEmptyHeader() { final Header header = new BasicHeader("Cache-Control", ""); - final CacheControl cacheControl = parser.parse(Collections.singletonList(header).iterator()); + final ResponseCacheControl cacheControl = parser.parseResponse(Collections.singletonList(header).iterator()); assertEquals(-1L, cacheControl.getMaxAge()); } @Test public void testParseCookieEmptyValue() { - final Header header = new BasicHeader("Cache-Control", "max-age=;"); - final CacheControl cacheControl = parser.parse(Collections.singletonList(header).iterator()); + final Header header = new BasicHeader("Cache-Control", "max-age=,"); + final ResponseCacheControl cacheControl = parser.parseResponse(Collections.singletonList(header).iterator()); assertEquals(-1L, cacheControl.getMaxAge()); } @Test public void testParseNoCache() { final Header header = new BasicHeader(" Cache-Control", "no-cache"); - final CacheControl cacheControl = parser.parse(Collections.singletonList(header).iterator()); + final ResponseCacheControl cacheControl = parser.parseResponse(Collections.singletonList(header).iterator()); assertEquals(-1L, cacheControl.getMaxAge()); } @Test public void testParseNoDirective() { final Header header = new BasicHeader(" Cache-Control", ""); - final CacheControl cacheControl = parser.parse(Collections.singletonList(header).iterator()); + final ResponseCacheControl cacheControl = parser.parseResponse(Collections.singletonList(header).iterator()); assertEquals(-1L, cacheControl.getMaxAge()); } @Test public void testGarbage() { final Header header = new BasicHeader("Cache-Control", ",,= blah,"); - final CacheControl cacheControl = parser.parse(Collections.singletonList(header).iterator()); + final ResponseCacheControl cacheControl = parser.parseResponse(Collections.singletonList(header).iterator()); assertEquals(-1L, cacheControl.getMaxAge()); } @@ -115,7 +115,7 @@ public class CacheControlParserTest { @Test public void testParseMultipleDirectives() { final Header header = new BasicHeader("Cache-Control", "max-age=604800, stale-while-revalidate=86400, s-maxage=3600, must-revalidate, private"); - final CacheControl cacheControl = parser.parse(Collections.singletonList(header).iterator()); + final ResponseCacheControl cacheControl = parser.parseResponse(Collections.singletonList(header).iterator()); assertAll("Must all pass", () -> assertEquals(604800L, cacheControl.getMaxAge()), @@ -128,7 +128,7 @@ public class CacheControlParserTest { @Test public void testParseMultipleDirectives2() { final Header header = new BasicHeader("Cache-Control", "max-age=604800, stale-while-revalidate=86400, must-revalidate, private, s-maxage=3600"); - final CacheControl cacheControl = parser.parse(Collections.singletonList(header).iterator()); + final ResponseCacheControl cacheControl = parser.parseResponse(Collections.singletonList(header).iterator()); assertAll("Must all pass", () -> assertEquals(604800L, cacheControl.getMaxAge()), @@ -141,7 +141,7 @@ public class CacheControlParserTest { @Test public void testParsePublic() { final Header header = new BasicHeader("Cache-Control", "public"); - final CacheControl cacheControl = parser.parse(Collections.singletonList(header).iterator()); + final ResponseCacheControl cacheControl = parser.parseResponse(Collections.singletonList(header).iterator()); assertTrue(cacheControl.isPublic()); } @@ -149,7 +149,7 @@ public class CacheControlParserTest { @Test public void testParsePrivate() { final Header header = new BasicHeader("Cache-Control", "private"); - final CacheControl cacheControl = parser.parse(Collections.singletonList(header).iterator()); + final ResponseCacheControl cacheControl = parser.parseResponse(Collections.singletonList(header).iterator()); assertTrue(cacheControl.isCachePrivate()); } @@ -157,7 +157,7 @@ public class CacheControlParserTest { @Test public void testParseNoStore() { final Header header = new BasicHeader("Cache-Control", "no-store"); - final CacheControl cacheControl = parser.parse(Collections.singletonList(header).iterator()); + final ResponseCacheControl cacheControl = parser.parseResponse(Collections.singletonList(header).iterator()); assertTrue(cacheControl.isNoStore()); } @@ -165,7 +165,7 @@ public class CacheControlParserTest { @Test public void testParseStaleWhileRevalidate() { final Header header = new BasicHeader("Cache-Control", "max-age=3600, stale-while-revalidate=120"); - final CacheControl cacheControl = parser.parse(Collections.singletonList(header).iterator()); + final ResponseCacheControl cacheControl = parser.parseResponse(Collections.singletonList(header).iterator()); assertEquals(120, cacheControl.getStaleWhileRevalidate()); } @@ -173,7 +173,7 @@ public class CacheControlParserTest { @Test public void testParseNoCacheFields() { final Header header = new BasicHeader("Cache-Control", "no-cache=\"Set-Cookie, Content-Language\", stale-while-revalidate=120"); - final CacheControl cacheControl = parser.parse(Collections.singletonList(header).iterator()); + final ResponseCacheControl cacheControl = parser.parseResponse(Collections.singletonList(header).iterator()); assertTrue(cacheControl.isNoCache()); assertEquals(2, cacheControl.getNoCacheFields().size()); @@ -185,7 +185,7 @@ public class CacheControlParserTest { @Test public void testParseNoCacheFieldsNoQuote() { final Header header = new BasicHeader("Cache-Control", "no-cache=Set-Cookie, Content-Language, stale-while-revalidate=120"); - final CacheControl cacheControl = parser.parse(Collections.singletonList(header).iterator()); + final ResponseCacheControl cacheControl = parser.parseResponse(Collections.singletonList(header).iterator()); assertTrue(cacheControl.isNoCache()); assertEquals(1, cacheControl.getNoCacheFields().size()); @@ -196,7 +196,7 @@ public class CacheControlParserTest { @Test public void testParseNoCacheFieldsMessy() { final Header header = new BasicHeader("Cache-Control", "no-cache=\" , , ,,, Set-Cookie , , Content-Language , \", stale-while-revalidate=120"); - final CacheControl cacheControl = parser.parse(Collections.singletonList(header).iterator()); + final ResponseCacheControl cacheControl = parser.parseResponse(Collections.singletonList(header).iterator()); assertTrue(cacheControl.isNoCache()); assertEquals(2, cacheControl.getNoCacheFields().size()); @@ -217,8 +217,8 @@ public class CacheControlParserTest { final Header header6 = new BasicHeader("Cache-Control", "must-revalidate"); // Parse headers - final CacheControl cacheControl1 = parser.parse(Arrays.asList(header1, header2).iterator()); - final CacheControl cacheControl2 = parser.parse(Arrays.asList(header3, header4, header5, header6).iterator()); + final ResponseCacheControl cacheControl1 = parser.parseResponse(Arrays.asList(header1, header2).iterator()); + final ResponseCacheControl cacheControl2 = parser.parseResponse(Arrays.asList(header3, header4, header5, header6).iterator()); // Validate Cache-Control directives assertEquals(cacheControl1.getMaxAge(), cacheControl2.getMaxAge()); @@ -226,4 +226,21 @@ public class CacheControlParserTest { assertEquals(cacheControl1.isCachePrivate(), cacheControl2.isCachePrivate()); assertEquals(cacheControl1.isMustRevalidate(), cacheControl2.isMustRevalidate()); } + + @Test + public void testParseRequestMultipleDirectives() { + final Header header = new BasicHeader("Cache-Control", "blah, max-age=1111, max-stale=2222, " + + "min-fresh=3333, no-cache, no-store, no-cache, no-stuff, only-if-cached, only-if-cached-or-maybe-not"); + final RequestCacheControl cacheControl = parser.parseRequest(Collections.singletonList(header).iterator()); + + assertAll("Must all pass", + () -> assertEquals(1111L, cacheControl.getMaxAge()), + () -> assertEquals(2222L, cacheControl.getMaxStale()), + () -> assertEquals(3333L, cacheControl.getMinFresh()), + () -> assertTrue(cacheControl.isNoCache()), + () -> assertTrue(cacheControl.isNoCache()), + () -> assertTrue(cacheControl.isOnlyIfCached()) + ); + } + } diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestResponseCachingPolicy.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestResponseCachingPolicy.java index b9f2ff2a4..9a7b1585a 100644 --- a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestResponseCachingPolicy.java +++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestResponseCachingPolicy.java @@ -227,7 +227,7 @@ public class TestResponseCachingPolicy { final int status = getRandomStatus(); response.setCode(status); response.setHeader("Cache-Control", "max-age=boom"); - Assertions.assertTrue(policy.isResponseCacheable("GET", response)); + Assertions.assertFalse(policy.isResponseCacheable("GET", response)); }