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 + * . + * + */ + +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 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. + *

+ * 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 + * . + * + */ +package org.apache.hc.client5.http.impl.cache; + +import java.util.BitSet; + +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.FormattedHeader; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.util.Args; +import org.apache.hc.core5.util.CharArrayBuffer; +import org.apache.hc.core5.util.Tokenizer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/** + * A parser for the HTTP Cache-Control header that can be used to extract information about caching directives. + *

+ * 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 + * . + * + */ +package org.apache.hc.client5.http.impl.cache; + +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.message.BasicHeader; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class CacheControlParserTest { + + private final CacheControlHeaderParser parser = CacheControlHeaderParser.INSTANCE; + + @Test + public void testParseMaxAgeZero() { + final Header header = new BasicHeader("Cache-Control", "max-age=0 , this = stuff;"); + final CacheControl cacheControl = parser.parse(header); + assertEquals(0L, cacheControl.getMaxAge()); + } + + @Test + public void testParseSMaxAge() { + final Header header = new BasicHeader("Cache-Control", "s-maxage=3600"); + final CacheControl cacheControl = parser.parse(header); + assertEquals(3600L, cacheControl.getSharedMaxAge()); + } + + @Test + public void testParseInvalidCacheValue() { + final Header header = new BasicHeader("Cache-Control", "max-age=invalid"); + final CacheControl cacheControl = parser.parse(header); + assertEquals(-1L, cacheControl.getMaxAge()); + } + + @Test + public void testParseInvalidHeader() { + final Header header = new BasicHeader("Cache-Control", "max-age"); + final CacheControl cacheControl = parser.parse(header); + assertEquals(-1L, cacheControl.getMaxAge()); + } + + @Test + public void testParseNullHeader() { + final Header header = null; + assertThrows(NullPointerException.class, () -> parser.parse(header)); + } + + @Test + public void testParseEmptyHeader() { + final Header header = new BasicHeader("Cache-Control", ""); + final CacheControl cacheControl = parser.parse(header); + assertEquals(-1L, cacheControl.getMaxAge()); + } + + @Test + public void testParseCookieEmptyValue() { + final Header header = new BasicHeader("Cache-Control", "max-age=;"); + final CacheControl cacheControl = parser.parse(header); + assertEquals(-1L, cacheControl.getMaxAge()); + } + + @Test + public void testParseNoCache() { + final Header header = new BasicHeader(" Cache-Control", "no-cache"); + final CacheControl cacheControl = parser.parse(header); + assertEquals(-1L, cacheControl.getMaxAge()); + } + + @Test + public void testParseNoDirective() { + final Header header = new BasicHeader(" Cache-Control", ""); + final CacheControl cacheControl = parser.parse(header); + assertEquals(-1L, cacheControl.getMaxAge()); + } + + @Test + public void testGarbage() { + final Header header = new BasicHeader("Cache-Control", ",,= blah,"); + final CacheControl cacheControl = parser.parse(header); + assertEquals(-1L, cacheControl.getMaxAge()); + } + +} 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 0a3db1636..75a7ca79e 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 @@ -642,7 +642,7 @@ public class TestCachingExecChain { execute(req2); Assertions.assertEquals(CacheResponseStatus.CACHE_MODULE_RESPONSE, - context.getCacheResponseStatus()); + context.getCacheResponseStatus()); } @Test @@ -1165,12 +1165,12 @@ public class TestCachingExecChain { final ClassicHttpResponse resp1 = HttpTestUtils.make304Response(); resp1.setHeader("Date", DateUtils.formatStandardDate(now)); - resp1.setHeader("Cache-Control", "max-age=0"); + resp1.setHeader("Cache-Control", "max-age=1"); resp1.setHeader("Etag", "etag"); final ClassicHttpResponse resp2 = HttpTestUtils.make304Response(); resp2.setHeader("Date", DateUtils.formatStandardDate(now)); - resp2.setHeader("Cache-Control", "max-age=0"); + resp2.setHeader("Cache-Control", "max-age=1"); resp1.setHeader("Etag", "etag"); Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1); @@ -1199,13 +1199,13 @@ public class TestCachingExecChain { final ClassicHttpResponse resp1 = HttpTestUtils.make304Response(); resp1.setHeader("Date", DateUtils.formatStandardDate(now)); - resp1.setHeader("Cache-Control", "max-age=0"); + resp1.setHeader("Cache-Control", "max-age=1"); resp1.setHeader("Etag", "etag"); resp1.setHeader("Vary", "Accept-Encoding"); final ClassicHttpResponse resp2 = HttpTestUtils.make304Response(); resp2.setHeader("Date", DateUtils.formatStandardDate(now)); - resp2.setHeader("Cache-Control", "max-age=0"); + resp2.setHeader("Cache-Control", "max-age=1"); resp1.setHeader("Etag", "etag"); resp1.setHeader("Vary", "Accept-Encoding"); @@ -1242,7 +1242,7 @@ public class TestCachingExecChain { resp1.setHeader("Vary", "Accept-Encoding"); final ClassicHttpResponse resp2 = new BasicClassicHttpResponse(HttpStatus.SC_OK, - "Ok"); + "Ok"); resp2.setHeader("Date", DateUtils.formatStandardDate(now)); resp2.setHeader("Cache-Control", "public, max-age=60"); resp2.setHeader("Expires", DateUtils.formatStandardDate(inOneMinute)); 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 abf7324b3..d3c2e6112 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 @@ -27,11 +27,13 @@ package org.apache.hc.client5.http.impl.cache; import java.time.Instant; +import java.util.Date; import java.util.Random; import org.apache.hc.client5.http.auth.StandardAuthScheme; import org.apache.hc.client5.http.classic.methods.HttpOptions; import org.apache.hc.client5.http.utils.DateUtils; +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.HttpStatus; @@ -216,15 +218,49 @@ public class TestResponseCachingPolicy { final int status = getRandomStatus(); response.setCode(status); response.setHeader("Cache-Control", "max-age=0"); + Assertions.assertFalse(policy.isResponseCacheable("GET", response)); + } + + @Test + public void testMalformedHeaderValue() { + final int status = getRandomStatus(); + response.setCode(status); + response.setHeader("Cache-Control", "max-age=boom"); Assertions.assertTrue(policy.isResponseCacheable("GET", response)); } + + @Test + public void testNullHeaderValue() { + final int status = getRandomStatus(); + response.setCode(status); + response.setHeader("Cache-Control", null); + Assertions.assertTrue(policy.isResponseCacheable("GET", response)); + } + + @Test + public void testMultipleHeaderValue() { + final int status = getRandomStatus(); + response.setCode(status); + response.setHeader("Cache-Control", "s-maxage=12,must-revalidate=true,max-age=23"); + Assertions.assertTrue(policy.isResponseCacheable("GET", response)); + } + + @Test + public void testMissingCacheControlHeader() { + final int status = getRandomStatus(); + response.setCode(status); + response.removeHeaders(HttpHeaders.CACHE_CONTROL); + final boolean isCacheable = policy.isResponseCacheable("GET", response); + Assertions.assertTrue(isCacheable); + } + @Test public void testNon206WithSMaxAgeIsCacheable() { final int status = getRandomStatus(); response.setCode(status); response.setHeader("Cache-Control", "s-maxage=0"); - Assertions.assertTrue(policy.isResponseCacheable("GET", response)); + Assertions.assertFalse(policy.isResponseCacheable("GET", response)); } @Test @@ -870,9 +906,99 @@ public class TestResponseCachingPolicy { response.setCode(HttpStatus.SC_NOT_FOUND); response.setHeader("Date", DateUtils.formatStandardDate(now)); response.setHeader("Cache-Control","max-age=300"); - Assertions.assertTrue(policy.isResponseCacheable(request, response)); + assertTrue(policy.isResponseCacheable(request, response)); } + @Test + void testIsResponseCacheableNullCacheControl() { + + // Set up test data + final long now = System.currentTimeMillis(); + final long tenSecondsFromNow = now + 10000; + + response = new BasicHttpResponse(HttpStatus.SC_OK, ""); + response.setHeader(HttpHeaders.DATE, DateUtils.formatDate(new Date(now), DateUtils.PATTERN_RFC1123)); + response.setHeader(HttpHeaders.EXPIRES, DateUtils.formatDate(new Date(tenSecondsFromNow), DateUtils.PATTERN_RFC1123)); + + + // Create ResponseCachingPolicy instance and test the method + policy = new ResponseCachingPolicy(0, true, false, false, false); + request = new BasicHttpRequest("POST", "/foo"); + final boolean isCacheable = policy.isResponseCacheable(request, response); + + // Verify the result + assertTrue(isCacheable); + } + + + @Test + void testIsResponseCacheableNotNullCacheControlSmaxAge60() { + + // Set up test data + final long now = System.currentTimeMillis(); + final long tenSecondsFromNow = now + 10000; + + response = new BasicHttpResponse(HttpStatus.SC_OK, ""); + response.setHeader(HttpHeaders.DATE, DateUtils.formatDate(new Date(now), DateUtils.PATTERN_RFC1123)); + response.setHeader(HttpHeaders.EXPIRES, DateUtils.formatDate(new Date(tenSecondsFromNow), DateUtils.PATTERN_RFC1123)); + + + // Create ResponseCachingPolicy instance and test the method + policy = new ResponseCachingPolicy(0, true, false, false, false); + request = new BasicHttpRequest("POST", "/foo"); + response.addHeader(HttpHeaders.CACHE_CONTROL, "s-maxage=60"); + final boolean isCacheable = policy.isResponseCacheable(request, response); + + // Verify the result + assertTrue(isCacheable); + } + + @Test + void testIsResponseCacheableNotNullCacheControlMaxAge60() { + + // Set up test data + final long now = System.currentTimeMillis(); + final long tenSecondsFromNow = now + 10000; + + response = new BasicHttpResponse(HttpStatus.SC_OK, ""); + response.setHeader(HttpHeaders.DATE, DateUtils.formatDate(new Date(now), DateUtils.PATTERN_RFC1123)); + response.setHeader(HttpHeaders.EXPIRES, DateUtils.formatDate(new Date(tenSecondsFromNow), DateUtils.PATTERN_RFC1123)); + + + // Create ResponseCachingPolicy instance and test the method + policy = new ResponseCachingPolicy(0, true, false, false,false); + request = new BasicHttpRequest("POST", "/foo"); + response.addHeader(HttpHeaders.CACHE_CONTROL, "max-age=60"); + final boolean isCacheable = policy.isResponseCacheable(request, response); + + // Verify the result + assertTrue(isCacheable); + } + + @Test + void testIsResponseCacheableNotExsiresAndDate() { + + // Set up test data + final long now = System.currentTimeMillis(); + final long tenSecondsFromNow = now + 10000; + + response = new BasicHttpResponse(HttpStatus.SC_OK, ""); + response.setHeader(HttpHeaders.DATE, DateUtils.formatDate(new Date(now), DateUtils.PATTERN_RFC1123)); + response.setHeader(HttpHeaders.EXPIRES, DateUtils.formatDate(new Date(tenSecondsFromNow), DateUtils.PATTERN_RFC1123)); + + + // Create ResponseCachingPolicy instance and test the method + policy = new ResponseCachingPolicy(0, true, false, false,false); + request = new BasicHttpRequest("POST", "/foo"); + response.addHeader(HttpHeaders.CACHE_CONTROL, "something"); + final boolean isCacheable = policy.isResponseCacheable(request, response); + + // Verify the result + assertTrue(isCacheable); + } + + + private int getRandomStatus() { final int rnd = new Random().nextInt(acceptableCodes.length);