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 e2c6cdb77..4cf20ddf6 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
@@ -39,6 +39,7 @@ public class HeaderConstants {
public static final String PUT_METHOD = "PUT";
public static final String DELETE_METHOD = "DELETE";
public static final String TRACE_METHOD = "TRACE";
+ public static final String POST_METHOD = "POST";
public static final String LAST_MODIFIED = "Last-Modified";
public static final String IF_MATCH = "If-Match";
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
new file mode 100644
index 000000000..b026da2f4
--- /dev/null
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheControl.java
@@ -0,0 +1,111 @@
+/*
+ * ====================================================================
+ * 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
+ *
+ * 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; + + + /** + * Creates a new instance of {@code CacheControlHeader} with the specified max-age and shared-max-age values. + * + * @param maxAge The max-age value from the Cache-Control header. + * @param sharedMaxAge The shared-max-age value from the Cache-Control header. + */ + public CacheControl(final long maxAge, final long sharedMaxAge) { + this.maxAge = maxAge; + this.sharedMaxAge = sharedMaxAge; + } + + /** + * 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; + } + + + /** + * Returns a string representation of the {@code CacheControlHeader} object, including the max-age and shared-max-age values. + * + * @return A string representation of the object. + */ + @Override + public String toString() { + return "CacheControl{" + + "maxAge=" + maxAge + + ", sharedMaxAge=" + sharedMaxAge + + '}'; + } +} \ 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 new file mode 100644 index 000000000..c2afaffdd --- /dev/null +++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheControlHeaderParser.java @@ -0,0 +1,173 @@ +/* + * ==================================================================== + * 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 + *
+ * This class is thread-safe and has a singleton instance ({@link #INSTANCE}). + *
+ *+ * The {@link #parse(Header)} method takes an HTTP header and returns a {@link CacheControl} 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 + * present, the value of "s-maxage" takes precedence. + *
+ */ +@Internal +@Contract(threading = ThreadingBehavior.SAFE) +class CacheControlHeaderParser { + + /** + * The singleton instance of this parser. + */ + public static final CacheControlHeaderParser INSTANCE = new CacheControlHeaderParser(); + + /** + * The logger for this class. + */ + private static final Logger LOG = LoggerFactory.getLogger(CacheControlHeaderParser.class); + + + /** + * The character used to indicate a parameter's value in the Cache-Control header. + */ + private final static char EQUAL_CHAR = '='; + + /** + * The set of characters that can delimit a token in the header. + */ + private static final BitSet TOKEN_DELIMS = Tokenizer.INIT_BITSET(EQUAL_CHAR, ','); + + /** + * The set of characters that can delimit a value in the header. + */ + private static final BitSet VALUE_DELIMS = Tokenizer.INIT_BITSET(EQUAL_CHAR, ','); + + /** + * The token parser used to extract values from the header. + */ + private final Tokenizer tokenParser; + + /** + * Constructs a new instance of this parser. + */ + protected CacheControlHeaderParser() { + super(); + 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 header 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 Header header) { + Args.notNull(header, "Header"); + + long maxAge = -1; + long sharedMaxAge = -1; + + final CharArrayBuffer buffer; + final Tokenizer.Cursor cursor; + if (header instanceof FormattedHeader) { + buffer = ((FormattedHeader) header).getBuffer(); + cursor = new Tokenizer.Cursor(((FormattedHeader) header).getValuePos(), buffer.length()); + } else { + final String s = header.getValue(); + if (s == null) { + return new CacheControl(maxAge, sharedMaxAge); + } + buffer = new CharArrayBuffer(s.length()); + buffer.append(s); + cursor = new Tokenizer.Cursor(0, buffer.length()); + } + + while (!cursor.atEnd()) { + final String name = tokenParser.parseToken(buffer, cursor, TOKEN_DELIMS); + if (cursor.atEnd()) { + return new CacheControl(maxAge, sharedMaxAge); + } + final int valueDelim = buffer.charAt(cursor.getPos()); + cursor.updatePos(cursor.getPos() + 1); + if (valueDelim != EQUAL_CHAR) { + continue; + } + final String value = tokenParser.parseValue(buffer, cursor, VALUE_DELIMS); + + if (!cursor.atEnd()) { + cursor.updatePos(cursor.getPos() + 1); + } + + try { + if (name.equalsIgnoreCase("s-maxage")) { + sharedMaxAge = Long.parseLong(value); + } else if (name.equalsIgnoreCase("max-age")) { + maxAge = Long.parseLong(value); + } + } catch (final NumberFormatException e) { + // skip malformed directive + if (LOG.isDebugEnabled()) { + LOG.debug("Header {} was malformed: {}", name, value); + } + } + } + return new CacheControl(maxAge, sharedMaxAge); + } +} + + 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 9e8e15030..da2378f5a 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 @@ -26,7 +26,9 @@ */ package org.apache.hc.client5.http.impl.cache; +import java.time.Duration; import java.time.Instant; +import java.time.format.DateTimeFormatter; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; @@ -50,6 +52,25 @@ import org.slf4j.LoggerFactory; class ResponseCachingPolicy { + /** + * The default freshness duration for a cached object, in seconds. + * + *This constant is used to set the default value for the freshness lifetime of a cached object. + * When a new object is added to the cache, it will be assigned this duration if no other duration + * is specified.
+ * + *By default, this value is set to 300 seconds (5 minutes). Applications can customize this + * value as needed.
+ */ + private static final Duration DEFAULT_FRESHNESS_DURATION = Duration.ofMinutes(5); + + /** + * This {@link DateTimeFormatter} is used to format and parse date-time objects in a specific format commonly + * used in HTTP protocol messages. The format includes the day of the week, day of the month, month, year, and time + * of day, all represented in GMT time. An example of a date-time string in this format is "Tue, 15 Nov 1994 08:12:31 GMT". + */ + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.RFC_1123_DATE_TIME; + private static final String[] AUTH_CACHEABLE_PARAMS = { "s-maxage", HeaderConstants.CACHE_CONTROL_MUST_REVALIDATE, HeaderConstants.PUBLIC }; @@ -108,7 +129,8 @@ class ResponseCachingPolicy { public boolean isResponseCacheable(final String httpMethod, final HttpResponse response) { boolean cacheable = false; - if (!HeaderConstants.GET_METHOD.equals(httpMethod) && !HeaderConstants.HEAD_METHOD.equals(httpMethod)) { + if (!HeaderConstants.GET_METHOD.equals(httpMethod) && !HeaderConstants.HEAD_METHOD.equals(httpMethod) + && !HeaderConstants.POST_METHOD.equals(httpMethod)) { if (LOG.isDebugEnabled()) { LOG.debug("{} method response is not cacheable", httpMethod); } @@ -181,6 +203,15 @@ class ResponseCachingPolicy { return false; } + // calculate freshness lifetime + final Duration freshnessLifetime = calculateFreshnessLifetime(response); + if (freshnessLifetime.isNegative() || freshnessLifetime.isZero()) { + if (LOG.isDebugEnabled()) { + LOG.debug("Freshness lifetime is invalid"); + } + return false; + } + return cacheable || isExplicitlyCacheable(response); } @@ -320,4 +351,74 @@ class ResponseCachingPolicy { return HttpVersion.HTTP_1_0.equals(version); } + /** + * Calculates the freshness lifetime of a response, based on the headers in the response. + *+ * This method follows the algorithm for calculating the freshness lifetime. + * The freshness lifetime represents the time interval in seconds during which the response can be served without + * being considered stale. The freshness lifetime calculation takes into account the s-maxage, max-age, Expires, and + * Date headers as follows: + *
+ * Note that caching is a complex topic and cache control directives may interact with each other in non-trivial ways. + * This method provides a basic implementation of the freshness lifetime calculation algorithm and may not be suitable + * for all use cases. Developers should consult the HTTP caching specifications for more information and consider + * implementing additional caching mechanisms as needed. + *
+ * + * @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) { + // Check if s-maxage is present and use its value if it is + final Header cacheControl = response.getFirstHeader(HttpHeaders.CACHE_CONTROL); + if (cacheControl == null) { + // If no cache-control header is present, assume no caching directives and return a default value + return DEFAULT_FRESHNESS_DURATION; // 5 minutes + } + + final String cacheControlValue = cacheControl.getValue(); + if (cacheControlValue == null) { + // If cache-control header has no value, assume no caching directives and return a default value + return DEFAULT_FRESHNESS_DURATION; // 5 minutes + } + final CacheControl cacheControlHeader = CacheControlHeaderParser.INSTANCE.parse(cacheControl); + if (cacheControlHeader.getSharedMaxAge() != -1) { + return Duration.ofSeconds(cacheControlHeader.getSharedMaxAge()); + } else if (cacheControlHeader.getMaxAge() != -1) { + return Duration.ofSeconds(cacheControlHeader.getMaxAge()); + } + + // Check if Expires is present and use its value minus the value of the Date header + Instant expiresInstant = null; + Instant dateInstant = null; + final Header expire = response.getFirstHeader(HttpHeaders.EXPIRES); + if (expire != null) { + final String expiresHeaderValue = expire.getValue(); + expiresInstant = FORMATTER.parse(expiresHeaderValue, Instant::from); + } + final Header date = response.getFirstHeader(HttpHeaders.DATE); + if (date != null) { + final String dateHeaderValue = date.getValue(); + dateInstant = FORMATTER.parse(dateHeaderValue, Instant::from); + } + if (expiresInstant != null && dateInstant != null) { + return Duration.ofSeconds(expiresInstant.getEpochSecond() - dateInstant.getEpochSecond()); + } + + // If none of the above conditions are met, a heuristic freshness lifetime might be applicable + return DEFAULT_FRESHNESS_DURATION; // 5 minutes + } + } 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 new file mode 100644 index 000000000..1f3b60761 --- /dev/null +++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/CacheControlParserTest.java @@ -0,0 +1,109 @@ +/* + * ==================================================================== + * 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 + *