From da1d7f23ea206d3ba96c228343d08df6f4abc3e0 Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Fri, 10 Oct 2014 23:06:50 -0400 Subject: [PATCH] More convenient APIs for using Cache-Control in requests. --- .../com/squareup/okhttp/CacheControlTest.java | 124 ++++++++++++++ .../java/com/squareup/okhttp/RequestTest.java | 20 ++- .../com/squareup/okhttp/CacheControl.java | 158 +++++++++++++++++- .../java/com/squareup/okhttp/Request.java | 11 ++ 4 files changed, 310 insertions(+), 3 deletions(-) create mode 100644 okhttp-tests/src/test/java/com/squareup/okhttp/CacheControlTest.java diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/CacheControlTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/CacheControlTest.java new file mode 100644 index 000000000..bc9a99286 --- /dev/null +++ b/okhttp-tests/src/test/java/com/squareup/okhttp/CacheControlTest.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * Licensed 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. + */ +package com.squareup.okhttp; + +import java.util.concurrent.TimeUnit; +import org.junit.Test; + +import static junit.framework.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public final class CacheControlTest { + @Test public void emptyBuilderIsEmpty() throws Exception { + CacheControl cacheControl = new CacheControl.Builder().build(); + assertEquals("", cacheControl.toString()); + assertFalse(cacheControl.noCache()); + assertFalse(cacheControl.noStore()); + assertEquals(-1, cacheControl.maxAgeSeconds()); + assertEquals(-1, cacheControl.sMaxAgeSeconds()); + assertFalse(cacheControl.isPublic()); + assertFalse(cacheControl.mustRevalidate()); + assertEquals(-1, cacheControl.maxStaleSeconds()); + assertEquals(-1, cacheControl.minFreshSeconds()); + assertFalse(cacheControl.onlyIfCached()); + assertFalse(cacheControl.mustRevalidate()); + } + + @Test public void completeBuilder() throws Exception { + CacheControl cacheControl = new CacheControl.Builder() + .noCache() + .noStore() + .maxAge(1, TimeUnit.SECONDS) + .maxStale(2, TimeUnit.SECONDS) + .minFresh(3, TimeUnit.SECONDS) + .onlyIfCached() + .noTransform() + .build(); + assertEquals("no-cache, no-store, max-age=1, max-stale=2, min-fresh=3, only-if-cached, " + + "no-transform", cacheControl.toString()); + assertTrue(cacheControl.noCache()); + assertTrue(cacheControl.noStore()); + assertEquals(1, cacheControl.maxAgeSeconds()); + assertEquals(2, cacheControl.maxStaleSeconds()); + assertEquals(3, cacheControl.minFreshSeconds()); + assertTrue(cacheControl.onlyIfCached()); + + // These members are accessible to response headers only. + assertEquals(-1, cacheControl.sMaxAgeSeconds()); + assertFalse(cacheControl.isPublic()); + assertFalse(cacheControl.mustRevalidate()); + } + + @Test public void parseEmpty() throws Exception { + CacheControl cacheControl = CacheControl.parse( + new Headers.Builder().set("Cache-Control", "").build()); + assertEquals("", cacheControl.toString()); + assertFalse(cacheControl.noCache()); + assertFalse(cacheControl.noStore()); + assertEquals(-1, cacheControl.maxAgeSeconds()); + assertEquals(-1, cacheControl.sMaxAgeSeconds()); + assertFalse(cacheControl.isPublic()); + assertFalse(cacheControl.mustRevalidate()); + assertEquals(-1, cacheControl.maxStaleSeconds()); + assertEquals(-1, cacheControl.minFreshSeconds()); + assertFalse(cacheControl.onlyIfCached()); + assertFalse(cacheControl.mustRevalidate()); + } + + @Test public void parse() throws Exception { + String header = "no-cache, no-store, max-age=1, s-maxage=2, public, must-revalidate, " + + "max-stale=3, min-fresh=4, only-if-cached, no-transform"; + CacheControl cacheControl = CacheControl.parse(new Headers.Builder() + .set("Cache-Control", header) + .build()); + assertTrue(cacheControl.noCache()); + assertTrue(cacheControl.noStore()); + assertEquals(1, cacheControl.maxAgeSeconds()); + assertEquals(2, cacheControl.sMaxAgeSeconds()); + assertTrue(cacheControl.isPublic()); + assertTrue(cacheControl.mustRevalidate()); + assertEquals(3, cacheControl.maxStaleSeconds()); + assertEquals(4, cacheControl.minFreshSeconds()); + assertTrue(cacheControl.onlyIfCached()); + assertTrue(cacheControl.noTransform()); + assertEquals(header, cacheControl.toString()); + } + + @Test public void timeDurationTruncatedToMaxValue() throws Exception { + CacheControl cacheControl = new CacheControl.Builder() + .maxAge(365 * 100, TimeUnit.DAYS) // Longer than Integer.MAX_VALUE seconds. + .build(); + assertEquals(Integer.MAX_VALUE, cacheControl.maxAgeSeconds()); + } + + @Test public void secondsMustBeNonNegative() throws Exception { + CacheControl.Builder builder = new CacheControl.Builder(); + try { + builder.maxAge(-1, TimeUnit.SECONDS); + fail(); + } catch (IllegalArgumentException expected) { + } + } + + @Test public void timePrecisionIsTruncatedToSeconds() throws Exception { + CacheControl cacheControl = new CacheControl.Builder() + .maxAge(4999, TimeUnit.MILLISECONDS) + .build(); + assertEquals(4, cacheControl.maxAgeSeconds()); + } +} diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/RequestTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/RequestTest.java index 00f1d0542..d77bb2d2c 100644 --- a/okhttp-tests/src/test/java/com/squareup/okhttp/RequestTest.java +++ b/okhttp-tests/src/test/java/com/squareup/okhttp/RequestTest.java @@ -21,7 +21,8 @@ import java.io.FileWriter; import java.io.IOException; import java.net.URI; import java.net.URL; - +import java.util.Arrays; +import java.util.Collections; import okio.Buffer; import org.junit.Test; @@ -113,6 +114,23 @@ public final class RequestTest { assertEquals(new URL("http://localhost/api"), request.url()); } + @Test public void cacheControl() throws Exception { + Request request = new Request.Builder() + .cacheControl(new CacheControl.Builder().noCache().build()) + .url("https://square.com") + .build(); + assertEquals(Arrays.asList("no-cache"), request.headers("Cache-Control")); + } + + @Test public void emptyCacheControlClearsAllCacheControlHeaders() throws Exception { + Request request = new Request.Builder() + .header("Cache-Control", "foo") + .cacheControl(new CacheControl.Builder().build()) + .url("https://square.com") + .build(); + assertEquals(Collections.emptyList(), request.headers("Cache-Control")); + } + private String bodyToHex(RequestBody body) throws IOException { Buffer buffer = new Buffer(); body.writeTo(buffer); diff --git a/okhttp/src/main/java/com/squareup/okhttp/CacheControl.java b/okhttp/src/main/java/com/squareup/okhttp/CacheControl.java index dc944e4f6..c19d77958 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/CacheControl.java +++ b/okhttp/src/main/java/com/squareup/okhttp/CacheControl.java @@ -1,6 +1,7 @@ package com.squareup.okhttp; import com.squareup.okhttp.internal.http.HeaderParser; +import java.util.concurrent.TimeUnit; /** * A Cache-Control header with cache directives from a server or client. These @@ -11,6 +12,24 @@ import com.squareup.okhttp.internal.http.HeaderParser; * 2616, 14.9. */ public final class CacheControl { + /** + * Cache control request directives that require network validation of + * responses. Note that such requests may be assisted by the cache via + * conditional GET requests. + */ + public static final CacheControl FORCE_NETWORK = new Builder().noCache().build(); + + /** + * Cache control request directives that uses the cache only, even if the + * cached response is stale. If the response isn't available in the cache or + * requires server validation, the call will fail with a {@code 504 + * Unsatisfiable Request}. + */ + public static final CacheControl FORCE_CACHE = new Builder() + .onlyIfCached() + .maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS) + .build(); + private final boolean noCache; private final boolean noStore; private final int maxAgeSeconds; @@ -20,10 +39,11 @@ public final class CacheControl { private final int maxStaleSeconds; private final int minFreshSeconds; private final boolean onlyIfCached; + private final boolean noTransform; private CacheControl(boolean noCache, boolean noStore, int maxAgeSeconds, int sMaxAgeSeconds, boolean isPublic, boolean mustRevalidate, int maxStaleSeconds, int minFreshSeconds, - boolean onlyIfCached) { + boolean onlyIfCached, boolean noTransform) { this.noCache = noCache; this.noStore = noStore; this.maxAgeSeconds = maxAgeSeconds; @@ -33,6 +53,20 @@ public final class CacheControl { this.maxStaleSeconds = maxStaleSeconds; this.minFreshSeconds = minFreshSeconds; this.onlyIfCached = onlyIfCached; + this.noTransform = noTransform; + } + + private CacheControl(Builder builder) { + this.noCache = builder.noCache; + this.noStore = builder.noStore; + this.maxAgeSeconds = builder.maxAgeSeconds; + this.sMaxAgeSeconds = -1; + this.isPublic = false; + this.mustRevalidate = false; + this.maxStaleSeconds = builder.maxStaleSeconds; + this.minFreshSeconds = builder.minFreshSeconds; + this.onlyIfCached = builder.onlyIfCached; + this.noTransform = builder.noTransform; } /** @@ -96,6 +130,10 @@ public final class CacheControl { return onlyIfCached; } + public boolean noTransform() { + return noTransform; + } + /** * Returns the cache directives of {@code headers}. This honors both * Cache-Control and Pragma headers if they are present. @@ -110,6 +148,7 @@ public final class CacheControl { int maxStaleSeconds = -1; int minFreshSeconds = -1; boolean onlyIfCached = false; + boolean noTransform = false; for (int i = 0; i < headers.size(); i++) { if (!headers.name(i).equalsIgnoreCase("Cache-Control") @@ -166,11 +205,126 @@ public final class CacheControl { minFreshSeconds = HeaderParser.parseSeconds(parameter); } else if ("only-if-cached".equalsIgnoreCase(directive)) { onlyIfCached = true; + } else if ("no-transform".equalsIgnoreCase(directive)) { + noTransform = true; } } } return new CacheControl(noCache, noStore, maxAgeSeconds, sMaxAgeSeconds, isPublic, - mustRevalidate, maxStaleSeconds, minFreshSeconds, onlyIfCached); + mustRevalidate, maxStaleSeconds, minFreshSeconds, onlyIfCached, noTransform); + } + + @Override public String toString() { + StringBuilder result = new StringBuilder(); + if (noCache) result.append("no-cache, "); + if (noStore) result.append("no-store, "); + if (maxAgeSeconds != -1) result.append("max-age=").append(maxAgeSeconds).append(", "); + if (sMaxAgeSeconds != -1) result.append("s-maxage=").append(sMaxAgeSeconds).append(", "); + if (isPublic) result.append("public, "); + if (mustRevalidate) result.append("must-revalidate, "); + if (maxStaleSeconds != -1) result.append("max-stale=").append(maxStaleSeconds).append(", "); + if (minFreshSeconds != -1) result.append("min-fresh=").append(minFreshSeconds).append(", "); + if (onlyIfCached) result.append("only-if-cached, "); + if (noTransform) result.append("no-transform, "); + if (result.length() == 0) return ""; + result.delete(result.length() - 2, result.length()); + return result.toString(); + } + + /** Builds a {@code Cache-Control} request header. */ + public static final class Builder { + boolean noCache; + boolean noStore; + int maxAgeSeconds = -1; + int maxStaleSeconds = -1; + int minFreshSeconds = -1; + boolean onlyIfCached; + boolean noTransform; + + /** Don't accept an unvalidated cached response. */ + public Builder noCache() { + this.noCache = true; + return this; + } + + /** Don't store the server's response in any cache. */ + public Builder noStore() { + this.noStore = true; + return this; + } + + /** + * Sets the maximum age of a cached response. If the cache response's age + * exceeds {@code maxAge}, it will not be used and a network request will + * be made. + * + * @param maxAge a non-negative integer. This is stored and transmitted with + * {@link TimeUnit#SECONDS} precision; finer precision will be lost. + */ + public Builder maxAge(int maxAge, TimeUnit timeUnit) { + if (maxAge < 0) throw new IllegalArgumentException("maxAge < 0: " + maxAge); + long maxAgeSecondsLong = timeUnit.toSeconds(maxAge); + this.maxAgeSeconds = maxAgeSecondsLong > Integer.MAX_VALUE + ? Integer.MAX_VALUE + : (int) maxAgeSecondsLong; + return this; + } + + /** + * Accept cached responses that have exceeded their freshness lifetime by + * up to {@code maxStale}. If unspecified, stale cache responses will not be + * used. + * + * @param maxStale a non-negative integer. This is stored and transmitted + * with {@link TimeUnit#SECONDS} precision; finer precision will be + * lost. + */ + public Builder maxStale(int maxStale, TimeUnit timeUnit) { + if (maxStale < 0) throw new IllegalArgumentException("maxStale < 0: " + maxStale); + long maxStaleSecondsLong = timeUnit.toSeconds(maxStale); + this.maxStaleSeconds = maxStaleSecondsLong > Integer.MAX_VALUE + ? Integer.MAX_VALUE + : (int) maxStaleSecondsLong; + return this; + } + + /** + * Sets the minimum number of seconds that a response will continue to be + * fresh for. If the response will be stale when {@code minFresh} have + * elapsed, the cached response will not be used and a network request will + * be made. + * + * @param minFresh a non-negative integer. This is stored and transmitted + * with {@link TimeUnit#SECONDS} precision; finer precision will be + * lost. + */ + public Builder minFresh(int minFresh, TimeUnit timeUnit) { + if (minFresh < 0) throw new IllegalArgumentException("minFresh < 0: " + minFresh); + long minFreshSecondsLong = timeUnit.toSeconds(minFresh); + this.minFreshSeconds = minFreshSecondsLong > Integer.MAX_VALUE + ? Integer.MAX_VALUE + : (int) minFreshSecondsLong; + return this; + } + + /** + * Only accept the response if it is in the cache. If the response isn't + * cached, a {@code 504 Unsatisfiable Request} response will be returned. + */ + public Builder onlyIfCached() { + this.onlyIfCached = true; + return this; + } + + /** Don't accept a transformed response. */ + public Builder noTransform() { + this.noTransform = true; + return this; + } + + public CacheControl build() { + return new CacheControl(this); + } } } diff --git a/okhttp/src/main/java/com/squareup/okhttp/Request.java b/okhttp/src/main/java/com/squareup/okhttp/Request.java index b8f417ebe..cb303c486 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/Request.java +++ b/okhttp/src/main/java/com/squareup/okhttp/Request.java @@ -185,6 +185,17 @@ public final class Request { return this; } + /** + * Sets this request's {@code Cache-Control} header, replacing any cache + * control headers already present. If {@code cacheControl} doesn't define + * any directives, this clears this request's cache-control headers. + */ + public Builder cacheControl(CacheControl cacheControl) { + String value = cacheControl.toString(); + if (value.isEmpty()) return removeHeader("Cache-Control"); + return header("Cache-Control", value); + } + public Builder get() { return method("GET", null); }