diff --git a/okhttp/src/main/java/com/squareup/okhttp/CacheControl.java b/okhttp/src/main/java/com/squareup/okhttp/CacheControl.java new file mode 100644 index 000000000..dc944e4f6 --- /dev/null +++ b/okhttp/src/main/java/com/squareup/okhttp/CacheControl.java @@ -0,0 +1,176 @@ +package com.squareup.okhttp; + +import com.squareup.okhttp.internal.http.HeaderParser; + +/** + * A Cache-Control header with cache directives from a server or client. These + * directives set policy on what responses can be stored, and which requests can + * be satisfied by those stored responses. + * + *

See RFC + * 2616, 14.9. + */ +public final class CacheControl { + private final boolean noCache; + private final boolean noStore; + private final int maxAgeSeconds; + private final int sMaxAgeSeconds; + private final boolean isPublic; + private final boolean mustRevalidate; + private final int maxStaleSeconds; + private final int minFreshSeconds; + private final boolean onlyIfCached; + + private CacheControl(boolean noCache, boolean noStore, int maxAgeSeconds, int sMaxAgeSeconds, + boolean isPublic, boolean mustRevalidate, int maxStaleSeconds, int minFreshSeconds, + boolean onlyIfCached) { + this.noCache = noCache; + this.noStore = noStore; + this.maxAgeSeconds = maxAgeSeconds; + this.sMaxAgeSeconds = sMaxAgeSeconds; + this.isPublic = isPublic; + this.mustRevalidate = mustRevalidate; + this.maxStaleSeconds = maxStaleSeconds; + this.minFreshSeconds = minFreshSeconds; + this.onlyIfCached = onlyIfCached; + } + + /** + * In a response, this field's name "no-cache" is misleading. It doesn't + * prevent us from caching the response; it only means we have to validate the + * response with the origin server before returning it. We can do this with a + * conditional GET. + * + *

In a request, it means do not use a cache to satisfy the request. + */ + public boolean noCache() { + return noCache; + } + + /** If true, this response should not be cached. */ + public boolean noStore() { + return noStore; + } + + /** + * The duration past the response's served date that it can be served without + * validation. + */ + public int maxAgeSeconds() { + return maxAgeSeconds; + } + + /** + * The "s-maxage" directive is the max age for shared caches. Not to be + * confused with "max-age" for non-shared caches, As in Firefox and Chrome, + * this directive is not honored by this cache. + */ + public int sMaxAgeSeconds() { + return sMaxAgeSeconds; + } + + public boolean isPublic() { + return isPublic; + } + + public boolean mustRevalidate() { + return mustRevalidate; + } + + public int maxStaleSeconds() { + return maxStaleSeconds; + } + + public int minFreshSeconds() { + return minFreshSeconds; + } + + /** + * This field's name "only-if-cached" is misleading. It actually means "do + * not use the network". It is set by a client who only wants to make a + * request if it can be fully satisfied by the cache. Cached responses that + * would require validation (ie. conditional gets) are not permitted if this + * header is set. + */ + public boolean onlyIfCached() { + return onlyIfCached; + } + + /** + * Returns the cache directives of {@code headers}. This honors both + * Cache-Control and Pragma headers if they are present. + */ + public static CacheControl parse(Headers headers) { + boolean noCache = false; + boolean noStore = false; + int maxAgeSeconds = -1; + int sMaxAgeSeconds = -1; + boolean isPublic = false; + boolean mustRevalidate = false; + int maxStaleSeconds = -1; + int minFreshSeconds = -1; + boolean onlyIfCached = false; + + for (int i = 0; i < headers.size(); i++) { + if (!headers.name(i).equalsIgnoreCase("Cache-Control") + && !headers.name(i).equalsIgnoreCase("Pragma")) { + continue; + } + + String string = headers.value(i); + int pos = 0; + while (pos < string.length()) { + int tokenStart = pos; + pos = HeaderParser.skipUntil(string, pos, "=,;"); + String directive = string.substring(tokenStart, pos).trim(); + String parameter; + + if (pos == string.length() || string.charAt(pos) == ',' || string.charAt(pos) == ';') { + pos++; // consume ',' or ';' (if necessary) + parameter = null; + } else { + pos++; // consume '=' + pos = HeaderParser.skipWhitespace(string, pos); + + // quoted string + if (pos < string.length() && string.charAt(pos) == '\"') { + pos++; // consume '"' open quote + int parameterStart = pos; + pos = HeaderParser.skipUntil(string, pos, "\""); + parameter = string.substring(parameterStart, pos); + pos++; // consume '"' close quote (if necessary) + + // unquoted string + } else { + int parameterStart = pos; + pos = HeaderParser.skipUntil(string, pos, ",;"); + parameter = string.substring(parameterStart, pos).trim(); + } + } + + if ("no-cache".equalsIgnoreCase(directive)) { + noCache = true; + } else if ("no-store".equalsIgnoreCase(directive)) { + noStore = true; + } else if ("max-age".equalsIgnoreCase(directive)) { + maxAgeSeconds = HeaderParser.parseSeconds(parameter); + } else if ("s-maxage".equalsIgnoreCase(directive)) { + sMaxAgeSeconds = HeaderParser.parseSeconds(parameter); + } else if ("public".equalsIgnoreCase(directive)) { + isPublic = true; + } else if ("must-revalidate".equalsIgnoreCase(directive)) { + mustRevalidate = true; + } else if ("max-stale".equalsIgnoreCase(directive)) { + maxStaleSeconds = HeaderParser.parseSeconds(parameter); + } else if ("min-fresh".equalsIgnoreCase(directive)) { + minFreshSeconds = HeaderParser.parseSeconds(parameter); + } else if ("only-if-cached".equalsIgnoreCase(directive)) { + onlyIfCached = true; + } + } + } + + return new CacheControl(noCache, noStore, maxAgeSeconds, sMaxAgeSeconds, isPublic, + mustRevalidate, maxStaleSeconds, minFreshSeconds, onlyIfCached); + } +} diff --git a/okhttp/src/main/java/com/squareup/okhttp/Request.java b/okhttp/src/main/java/com/squareup/okhttp/Request.java index 83c6fbb2d..af90db152 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/Request.java +++ b/okhttp/src/main/java/com/squareup/okhttp/Request.java @@ -17,7 +17,6 @@ package com.squareup.okhttp; import com.squareup.okhttp.internal.Platform; import com.squareup.okhttp.internal.Util; -import com.squareup.okhttp.internal.http.HeaderParser; import com.squareup.okhttp.internal.http.HttpDate; import java.io.File; import java.io.FileInputStream; @@ -45,6 +44,7 @@ public final class Request { private volatile ParsedHeaders parsedHeaders; // Lazily initialized. private volatile URI uri; // Lazily initialized. + private volatile CacheControl cacheControl; // Lazily initialized. private Request(Builder builder) { this.url = builder.url; @@ -103,26 +103,6 @@ public final class Request { return headers; } - public boolean getNoCache() { - return parsedHeaders().noCache; - } - - public int getMaxAgeSeconds() { - return parsedHeaders().maxAgeSeconds; - } - - public int getMaxStaleSeconds() { - return parsedHeaders().maxStaleSeconds; - } - - public int getMinFreshSeconds() { - return parsedHeaders().minFreshSeconds; - } - - public boolean getOnlyIfCached() { - return parsedHeaders().onlyIfCached; - } - public String getUserAgent() { return parsedHeaders().userAgent; } @@ -136,57 +116,29 @@ public final class Request { return result != null ? result : (parsedHeaders = new ParsedHeaders(headers)); } + /** + * Returns the cache control directives for this response. This is never null, + * even if this response contains no {@code Cache-Control} header. + */ + public CacheControl cacheControl() { + CacheControl result = cacheControl; + return result != null ? result : (cacheControl = CacheControl.parse(headers)); + } + public boolean isHttps() { return url().getProtocol().equals("https"); } /** Parsed request headers, computed on-demand and cached. */ private static class ParsedHeaders { - /** Don't use a cache to satisfy this request. */ - private boolean noCache; - private int maxAgeSeconds = -1; - private int maxStaleSeconds = -1; - private int minFreshSeconds = -1; - - /** - * This field's name "only-if-cached" is misleading. It actually means "do - * not use the network". It is set by a client who only wants to make a - * request if it can be fully satisfied by the cache. Cached responses that - * would require validation (ie. conditional gets) are not permitted if this - * header is set. - */ - private boolean onlyIfCached; - private String userAgent; private String proxyAuthorization; public ParsedHeaders(Headers headers) { - HeaderParser.CacheControlHandler handler = new HeaderParser.CacheControlHandler() { - @Override public void handle(String directive, String parameter) { - if ("no-cache".equalsIgnoreCase(directive)) { - noCache = true; - } else if ("max-age".equalsIgnoreCase(directive)) { - maxAgeSeconds = HeaderParser.parseSeconds(parameter); - } else if ("max-stale".equalsIgnoreCase(directive)) { - maxStaleSeconds = HeaderParser.parseSeconds(parameter); - } else if ("min-fresh".equalsIgnoreCase(directive)) { - minFreshSeconds = HeaderParser.parseSeconds(parameter); - } else if ("only-if-cached".equalsIgnoreCase(directive)) { - onlyIfCached = true; - } - } - }; - for (int i = 0; i < headers.size(); i++) { String fieldName = headers.name(i); String value = headers.value(i); - if ("Cache-Control".equalsIgnoreCase(fieldName)) { - HeaderParser.parseCacheControl(value, handler); - } else if ("Pragma".equalsIgnoreCase(fieldName)) { - if ("no-cache".equalsIgnoreCase(value)) { - noCache = true; - } - } else if ("User-Agent".equalsIgnoreCase(fieldName)) { + if ("User-Agent".equalsIgnoreCase(fieldName)) { userAgent = value; } else if ("Proxy-Authorization".equalsIgnoreCase(fieldName)) { proxyAuthorization = value; diff --git a/okhttp/src/main/java/com/squareup/okhttp/Response.java b/okhttp/src/main/java/com/squareup/okhttp/Response.java index cb3ecb87d..0110f3e19 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/Response.java +++ b/okhttp/src/main/java/com/squareup/okhttp/Response.java @@ -51,6 +51,7 @@ public final class Response { private final Response redirectedBy; private volatile ParsedHeaders parsedHeaders; // Lazily initialized. + private volatile CacheControl cacheControl; // Lazily initialized. private Response(Builder builder) { this.request = builder.request; @@ -148,30 +149,6 @@ public final class Response { return parsedHeaders().expires; } - public boolean isNoCache() { - return parsedHeaders().noCache; - } - - public boolean isNoStore() { - return parsedHeaders().noStore; - } - - public int getMaxAgeSeconds() { - return parsedHeaders().maxAgeSeconds; - } - - public int getSMaxAgeSeconds() { - return parsedHeaders().sMaxAgeSeconds; - } - - public boolean isPublic() { - return parsedHeaders().isPublic; - } - - public boolean isMustRevalidate() { - return parsedHeaders().mustRevalidate; - } - public String getEtag() { return parsedHeaders().etag; } @@ -319,6 +296,15 @@ public final class Response { return result != null ? result : (parsedHeaders = new ParsedHeaders(headers)); } + /** + * Returns the cache control directives for this response. This is never null, + * even if this response contains no {@code Cache-Control} header. + */ + public CacheControl cacheControl() { + CacheControl result = cacheControl; + return result != null ? result : (cacheControl = CacheControl.parse(headers)); + } + /** Parsed response headers, computed on-demand and cached. */ private static class ParsedHeaders { /** The server's time when this response was served, if known. */ @@ -345,73 +331,17 @@ public final class Response { */ long receivedResponseMillis; - /** - * In the response, this field's name "no-cache" is misleading. It doesn't - * prevent us from caching the response; it only means we have to validate - * the response with the origin server before returning it. We can do this - * with a conditional get. - */ - boolean noCache; - - /** If true, this response should not be cached. */ - boolean noStore; - - /** - * The duration past the response's served date that it can be served - * without validation. - */ - int maxAgeSeconds = -1; - - /** - * The "s-maxage" directive is the max age for shared caches. Not to be - * confused with "max-age" for non-shared caches, As in Firefox and Chrome, - * this directive is not honored by this cache. - */ - int sMaxAgeSeconds = -1; - - /** - * This request header field's name "only-if-cached" is misleading. It - * actually means "do not use the network". It is set by a client who only - * wants to make a request if it can be fully satisfied by the cache. - * Cached responses that would require validation (ie. conditional gets) are - * not permitted if this header is set. - */ - boolean isPublic; - boolean mustRevalidate; String etag; int ageSeconds = -1; /** Case-insensitive set of field names. */ private Set varyFields = Collections.emptySet(); - private long contentLength = -1; - private String contentType; - private ParsedHeaders(Headers headers) { - HeaderParser.CacheControlHandler handler = new HeaderParser.CacheControlHandler() { - @Override public void handle(String directive, String parameter) { - if ("no-cache".equalsIgnoreCase(directive)) { - noCache = true; - } else if ("no-store".equalsIgnoreCase(directive)) { - noStore = true; - } else if ("max-age".equalsIgnoreCase(directive)) { - maxAgeSeconds = HeaderParser.parseSeconds(parameter); - } else if ("s-maxage".equalsIgnoreCase(directive)) { - sMaxAgeSeconds = HeaderParser.parseSeconds(parameter); - } else if ("public".equalsIgnoreCase(directive)) { - isPublic = true; - } else if ("must-revalidate".equalsIgnoreCase(directive)) { - mustRevalidate = true; - } - } - }; - for (int i = 0; i < headers.size(); i++) { String fieldName = headers.name(i); String value = headers.value(i); - if ("Cache-Control".equalsIgnoreCase(fieldName)) { - HeaderParser.parseCacheControl(value, handler); - } else if ("Date".equalsIgnoreCase(fieldName)) { + if ("Date".equalsIgnoreCase(fieldName)) { servedDate = HttpDate.parse(value); } else if ("Expires".equalsIgnoreCase(fieldName)) { expires = HttpDate.parse(value); @@ -419,10 +349,6 @@ public final class Response { lastModified = HttpDate.parse(value); } else if ("ETag".equalsIgnoreCase(fieldName)) { etag = value; - } else if ("Pragma".equalsIgnoreCase(fieldName)) { - if ("no-cache".equalsIgnoreCase(value)) { - noCache = true; - } } else if ("Age".equalsIgnoreCase(fieldName)) { ageSeconds = HeaderParser.parseSeconds(value); } else if ("Vary".equalsIgnoreCase(fieldName)) { @@ -433,13 +359,6 @@ public final class Response { for (String varyField : value.split(",")) { varyFields.add(varyField.trim()); } - } else if ("Content-Length".equalsIgnoreCase(fieldName)) { - try { - contentLength = Long.parseLong(value); - } catch (NumberFormatException ignored) { - } - } else if ("Content-Type".equalsIgnoreCase(fieldName)) { - contentType = value; } else if (OkHeaders.SENT_MILLIS.equalsIgnoreCase(fieldName)) { sentRequestMillis = Long.parseLong(value); } else if (OkHeaders.RECEIVED_MILLIS.equalsIgnoreCase(fieldName)) { diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/CacheStrategy.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/CacheStrategy.java index 459f3ad7a..b8c1d5155 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/CacheStrategy.java +++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/CacheStrategy.java @@ -1,5 +1,6 @@ package com.squareup.okhttp.internal.http; +import com.squareup.okhttp.CacheControl; import com.squareup.okhttp.MediaType; import com.squareup.okhttp.Request; import com.squareup.okhttp.Response; @@ -7,9 +8,9 @@ import com.squareup.okhttp.ResponseSource; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; -import java.util.concurrent.TimeUnit; import static com.squareup.okhttp.internal.Util.EMPTY_INPUT_STREAM; +import static java.util.concurrent.TimeUnit.SECONDS; /** * Given a request and cached response, this figures out whether to use the @@ -64,7 +65,7 @@ public final class CacheStrategy { ? Math.max(0, response.getReceivedResponseMillis() - response.getServedDate().getTime()) : 0; long receivedAge = response.getAgeSeconds() != -1 - ? Math.max(apparentReceivedAge, TimeUnit.SECONDS.toMillis(response.getAgeSeconds())) + ? Math.max(apparentReceivedAge, SECONDS.toMillis(response.getAgeSeconds())) : apparentReceivedAge; long responseDuration = response.getReceivedResponseMillis() - response.getSentRequestMillis(); long residentDuration = nowMillis - response.getReceivedResponseMillis(); @@ -76,8 +77,9 @@ public final class CacheStrategy { * starting from the served date. */ private static long computeFreshnessLifetime(Response response) { - if (response.getMaxAgeSeconds() != -1) { - return TimeUnit.SECONDS.toMillis(response.getMaxAgeSeconds()); + CacheControl responseCaching = response.cacheControl(); + if (responseCaching.maxAgeSeconds() != -1) { + return SECONDS.toMillis(responseCaching.maxAgeSeconds()); } else if (response.getExpires() != null) { long servedMillis = response.getServedDate() != null ? response.getServedDate().getTime() @@ -104,7 +106,8 @@ public final class CacheStrategy { * to attach a warning. */ private static boolean isFreshnessLifetimeHeuristic(Response response) { - return response.getMaxAgeSeconds() == -1 && response.getExpires() == null; + return response.cacheControl().maxAgeSeconds() == -1 + && response.getExpires() == null; } /** @@ -125,14 +128,15 @@ public final class CacheStrategy { // Responses to authorized requests aren't cacheable unless they include // a 'public', 'must-revalidate' or 's-maxage' directive. + CacheControl responseCaching = response.cacheControl(); if (request.header("Authorization") != null - && !response.isPublic() - && !response.isMustRevalidate() - && response.getSMaxAgeSeconds() == -1) { + && !responseCaching.isPublic() + && !responseCaching.mustRevalidate() + && responseCaching.sMaxAgeSeconds() == -1) { return false; } - if (response.isNoStore()) { + if (responseCaching.noStore()) { return false; } @@ -146,7 +150,7 @@ public final class CacheStrategy { public static CacheStrategy get(long nowMillis, Response response, Request request) { CacheStrategy candidate = getCandidate(nowMillis, response, request); - if (candidate.source != ResponseSource.CACHE && request.getOnlyIfCached()) { + if (candidate.source != ResponseSource.CACHE && request.cacheControl().onlyIfCached()) { // We're forbidden from using the network, but the cache is insufficient. Response noneResponse = new Response.Builder() .request(candidate.request) @@ -179,28 +183,30 @@ public final class CacheStrategy { return new CacheStrategy(request, response, ResponseSource.NETWORK); } - if (request.getNoCache() || hasConditions(request)) { + CacheControl requestCaching = request.cacheControl(); + if (requestCaching.noCache() || hasConditions(request)) { return new CacheStrategy(request, response, ResponseSource.NETWORK); } long ageMillis = computeAge(response, nowMillis); long freshMillis = computeFreshnessLifetime(response); - if (request.getMaxAgeSeconds() != -1) { - freshMillis = Math.min(freshMillis, TimeUnit.SECONDS.toMillis(request.getMaxAgeSeconds())); + if (requestCaching.maxAgeSeconds() != -1) { + freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds())); } long minFreshMillis = 0; - if (request.getMinFreshSeconds() != -1) { - minFreshMillis = TimeUnit.SECONDS.toMillis(request.getMinFreshSeconds()); + if (requestCaching.minFreshSeconds() != -1) { + minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds()); } long maxStaleMillis = 0; - if (!response.isMustRevalidate() && request.getMaxStaleSeconds() != -1) { - maxStaleMillis = TimeUnit.SECONDS.toMillis(request.getMaxStaleSeconds()); + CacheControl responseCaching = response.cacheControl(); + if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) { + maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds()); } - if (!response.isNoCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) { + if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) { Response.Builder builder = response.newBuilder() .setResponseSource(ResponseSource.CACHE); // Overwrite any stored response source. if (ageMillis + minFreshMillis >= freshMillis) { diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HeaderParser.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HeaderParser.java index c8a7119ae..e9af13026 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HeaderParser.java +++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HeaderParser.java @@ -17,49 +17,6 @@ package com.squareup.okhttp.internal.http; public final class HeaderParser { - - public interface CacheControlHandler { - void handle(String directive, String parameter); - } - - /** Parse a comma-separated list of cache control header values. */ - public static void parseCacheControl(String value, CacheControlHandler handler) { - int pos = 0; - while (pos < value.length()) { - int tokenStart = pos; - pos = skipUntil(value, pos, "=,;"); - String directive = value.substring(tokenStart, pos).trim(); - - if (pos == value.length() || value.charAt(pos) == ',' || value.charAt(pos) == ';') { - pos++; // consume ',' or ';' (if necessary) - handler.handle(directive, null); - continue; - } - - pos++; // consume '=' - pos = skipWhitespace(value, pos); - - String parameter; - - // quoted string - if (pos < value.length() && value.charAt(pos) == '\"') { - pos++; // consume '"' open quote - int parameterStart = pos; - pos = skipUntil(value, pos, "\""); - parameter = value.substring(parameterStart, pos); - pos++; // consume '"' close quote (if necessary) - - // unquoted string - } else { - int parameterStart = pos; - pos = skipUntil(value, pos, ",;"); - parameter = value.substring(parameterStart, pos).trim(); - } - - handler.handle(directive, parameter); - } - } - /** * Returns the next index in {@code input} at or after {@code pos} that * contains a character from {@code characters}. Returns the input length if