1
0
mirror of https://github.com/square/okhttp.git synced 2026-01-24 04:02:07 +03:00

New CacheControl class.

This moves cache control headers from the main request
and response classes to their own class.
This commit is contained in:
jwilson
2014-01-11 15:33:25 -05:00
parent 8e4c591f57
commit 684aa14bca
5 changed files with 222 additions and 212 deletions

View File

@@ -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.
*
* <p>See <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9">RFC
* 2616, 14.9</a>.
*/
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.
*
* <p>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);
}
}

View File

@@ -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;

View File

@@ -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<String> 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)) {

View File

@@ -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) {

View File

@@ -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