| 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 |
| * 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 { |
| /** |
| * 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; |
| private final int sMaxAgeSeconds; |
| private final boolean isPrivate; |
| private final boolean isPublic; |
| private final boolean mustRevalidate; |
| private final int maxStaleSeconds; |
| private final int minFreshSeconds; |
| private final boolean onlyIfCached; |
| private final boolean noTransform; |
| |
| String headerValue; // Lazily computed, if absent. |
| |
| private CacheControl(boolean noCache, boolean noStore, int maxAgeSeconds, int sMaxAgeSeconds, |
| boolean isPrivate, boolean isPublic, boolean mustRevalidate, int maxStaleSeconds, |
| int minFreshSeconds, boolean onlyIfCached, boolean noTransform, String headerValue) { |
| this.noCache = noCache; |
| this.noStore = noStore; |
| this.maxAgeSeconds = maxAgeSeconds; |
| this.sMaxAgeSeconds = sMaxAgeSeconds; |
| this.isPrivate = isPrivate; |
| this.isPublic = isPublic; |
| this.mustRevalidate = mustRevalidate; |
| this.maxStaleSeconds = maxStaleSeconds; |
| this.minFreshSeconds = minFreshSeconds; |
| this.onlyIfCached = onlyIfCached; |
| this.noTransform = noTransform; |
| this.headerValue = headerValue; |
| } |
| |
| private CacheControl(Builder builder) { |
| this.noCache = builder.noCache; |
| this.noStore = builder.noStore; |
| this.maxAgeSeconds = builder.maxAgeSeconds; |
| this.sMaxAgeSeconds = -1; |
| this.isPrivate = false; |
| this.isPublic = false; |
| this.mustRevalidate = false; |
| this.maxStaleSeconds = builder.maxStaleSeconds; |
| this.minFreshSeconds = builder.minFreshSeconds; |
| this.onlyIfCached = builder.onlyIfCached; |
| this.noTransform = builder.noTransform; |
| } |
| |
| /** |
| * 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 isPrivate() { |
| return isPrivate; |
| } |
| |
| 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; |
| } |
| |
| public boolean noTransform() { |
| return noTransform; |
| } |
| |
| /** |
| * 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 isPrivate = false; |
| boolean isPublic = false; |
| boolean mustRevalidate = false; |
| int maxStaleSeconds = -1; |
| int minFreshSeconds = -1; |
| boolean onlyIfCached = false; |
| boolean noTransform = false; |
| |
| boolean canUseHeaderValue = true; |
| String headerValue = null; |
| |
| for (int i = 0, size = headers.size(); i < size; i++) { |
| String name = headers.name(i); |
| String value = headers.value(i); |
| |
| if (name.equalsIgnoreCase("Cache-Control")) { |
| if (headerValue != null) { |
| // Multiple cache-control headers means we can't use the raw value. |
| canUseHeaderValue = false; |
| } else { |
| headerValue = value; |
| } |
| } else if (name.equalsIgnoreCase("Pragma")) { |
| // Might specify additional cache-control params. We invalidate just in case. |
| canUseHeaderValue = false; |
| } else { |
| continue; |
| } |
| |
| int pos = 0; |
| while (pos < value.length()) { |
| int tokenStart = pos; |
| pos = HeaderParser.skipUntil(value, pos, "=,;"); |
| String directive = value.substring(tokenStart, pos).trim(); |
| String parameter; |
| |
| if (pos == value.length() || value.charAt(pos) == ',' || value.charAt(pos) == ';') { |
| pos++; // consume ',' or ';' (if necessary) |
| parameter = null; |
| } else { |
| pos++; // consume '=' |
| pos = HeaderParser.skipWhitespace(value, pos); |
| |
| // quoted string |
| if (pos < value.length() && value.charAt(pos) == '\"') { |
| pos++; // consume '"' open quote |
| int parameterStart = pos; |
| pos = HeaderParser.skipUntil(value, pos, "\""); |
| parameter = value.substring(parameterStart, pos); |
| pos++; // consume '"' close quote (if necessary) |
| |
| // unquoted string |
| } else { |
| int parameterStart = pos; |
| pos = HeaderParser.skipUntil(value, pos, ",;"); |
| parameter = value.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, -1); |
| } else if ("s-maxage".equalsIgnoreCase(directive)) { |
| sMaxAgeSeconds = HeaderParser.parseSeconds(parameter, -1); |
| } else if ("private".equalsIgnoreCase(directive)) { |
| isPrivate = true; |
| } 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, Integer.MAX_VALUE); |
| } else if ("min-fresh".equalsIgnoreCase(directive)) { |
| minFreshSeconds = HeaderParser.parseSeconds(parameter, -1); |
| } else if ("only-if-cached".equalsIgnoreCase(directive)) { |
| onlyIfCached = true; |
| } else if ("no-transform".equalsIgnoreCase(directive)) { |
| noTransform = true; |
| } |
| } |
| } |
| |
| if (!canUseHeaderValue) { |
| headerValue = null; |
| } |
| return new CacheControl(noCache, noStore, maxAgeSeconds, sMaxAgeSeconds, isPrivate, isPublic, |
| mustRevalidate, maxStaleSeconds, minFreshSeconds, onlyIfCached, noTransform, headerValue); |
| } |
| |
| @Override public String toString() { |
| String result = headerValue; |
| return result != null ? result : (headerValue = headerValue()); |
| } |
| |
| private String headerValue() { |
| 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 (isPrivate) result.append("private, "); |
| 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); |
| } |
| } |
| } |