1 package com.squareup.okhttp; 2 3 import com.squareup.okhttp.internal.http.HeaderParser; 4 import java.util.concurrent.TimeUnit; 5 6 /** 7 * A Cache-Control header with cache directives from a server or client. These 8 * directives set policy on what responses can be stored, and which requests can 9 * be satisfied by those stored responses. 10 * 11 * <p>See <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9">RFC 12 * 2616, 14.9</a>. 13 */ 14 public final class CacheControl { 15 /** 16 * Cache control request directives that require network validation of 17 * responses. Note that such requests may be assisted by the cache via 18 * conditional GET requests. 19 */ 20 public static final CacheControl FORCE_NETWORK = new Builder().noCache().build(); 21 22 /** 23 * Cache control request directives that uses the cache only, even if the 24 * cached response is stale. If the response isn't available in the cache or 25 * requires server validation, the call will fail with a {@code 504 26 * Unsatisfiable Request}. 27 */ 28 public static final CacheControl FORCE_CACHE = new Builder() 29 .onlyIfCached() 30 .maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS) 31 .build(); 32 33 private final boolean noCache; 34 private final boolean noStore; 35 private final int maxAgeSeconds; 36 private final int sMaxAgeSeconds; 37 private final boolean isPrivate; 38 private final boolean isPublic; 39 private final boolean mustRevalidate; 40 private final int maxStaleSeconds; 41 private final int minFreshSeconds; 42 private final boolean onlyIfCached; 43 private final boolean noTransform; 44 45 String headerValue; // Lazily computed, if absent. 46 47 private CacheControl(boolean noCache, boolean noStore, int maxAgeSeconds, int sMaxAgeSeconds, 48 boolean isPrivate, boolean isPublic, boolean mustRevalidate, int maxStaleSeconds, 49 int minFreshSeconds, boolean onlyIfCached, boolean noTransform, String headerValue) { 50 this.noCache = noCache; 51 this.noStore = noStore; 52 this.maxAgeSeconds = maxAgeSeconds; 53 this.sMaxAgeSeconds = sMaxAgeSeconds; 54 this.isPrivate = isPrivate; 55 this.isPublic = isPublic; 56 this.mustRevalidate = mustRevalidate; 57 this.maxStaleSeconds = maxStaleSeconds; 58 this.minFreshSeconds = minFreshSeconds; 59 this.onlyIfCached = onlyIfCached; 60 this.noTransform = noTransform; 61 this.headerValue = headerValue; 62 } 63 64 private CacheControl(Builder builder) { 65 this.noCache = builder.noCache; 66 this.noStore = builder.noStore; 67 this.maxAgeSeconds = builder.maxAgeSeconds; 68 this.sMaxAgeSeconds = -1; 69 this.isPrivate = false; 70 this.isPublic = false; 71 this.mustRevalidate = false; 72 this.maxStaleSeconds = builder.maxStaleSeconds; 73 this.minFreshSeconds = builder.minFreshSeconds; 74 this.onlyIfCached = builder.onlyIfCached; 75 this.noTransform = builder.noTransform; 76 } 77 78 /** 79 * In a response, this field's name "no-cache" is misleading. It doesn't 80 * prevent us from caching the response; it only means we have to validate the 81 * response with the origin server before returning it. We can do this with a 82 * conditional GET. 83 * 84 * <p>In a request, it means do not use a cache to satisfy the request. 85 */ 86 public boolean noCache() { 87 return noCache; 88 } 89 90 /** If true, this response should not be cached. */ 91 public boolean noStore() { 92 return noStore; 93 } 94 95 /** 96 * The duration past the response's served date that it can be served without 97 * validation. 98 */ 99 public int maxAgeSeconds() { 100 return maxAgeSeconds; 101 } 102 103 /** 104 * The "s-maxage" directive is the max age for shared caches. Not to be 105 * confused with "max-age" for non-shared caches, As in Firefox and Chrome, 106 * this directive is not honored by this cache. 107 */ 108 public int sMaxAgeSeconds() { 109 return sMaxAgeSeconds; 110 } 111 112 public boolean isPrivate() { 113 return isPrivate; 114 } 115 116 public boolean isPublic() { 117 return isPublic; 118 } 119 120 public boolean mustRevalidate() { 121 return mustRevalidate; 122 } 123 124 public int maxStaleSeconds() { 125 return maxStaleSeconds; 126 } 127 128 public int minFreshSeconds() { 129 return minFreshSeconds; 130 } 131 132 /** 133 * This field's name "only-if-cached" is misleading. It actually means "do 134 * not use the network". It is set by a client who only wants to make a 135 * request if it can be fully satisfied by the cache. Cached responses that 136 * would require validation (ie. conditional gets) are not permitted if this 137 * header is set. 138 */ 139 public boolean onlyIfCached() { 140 return onlyIfCached; 141 } 142 143 public boolean noTransform() { 144 return noTransform; 145 } 146 147 /** 148 * Returns the cache directives of {@code headers}. This honors both 149 * Cache-Control and Pragma headers if they are present. 150 */ 151 public static CacheControl parse(Headers headers) { 152 boolean noCache = false; 153 boolean noStore = false; 154 int maxAgeSeconds = -1; 155 int sMaxAgeSeconds = -1; 156 boolean isPrivate = false; 157 boolean isPublic = false; 158 boolean mustRevalidate = false; 159 int maxStaleSeconds = -1; 160 int minFreshSeconds = -1; 161 boolean onlyIfCached = false; 162 boolean noTransform = false; 163 164 boolean canUseHeaderValue = true; 165 String headerValue = null; 166 167 for (int i = 0, size = headers.size(); i < size; i++) { 168 String name = headers.name(i); 169 String value = headers.value(i); 170 171 if (name.equalsIgnoreCase("Cache-Control")) { 172 if (headerValue != null) { 173 // Multiple cache-control headers means we can't use the raw value. 174 canUseHeaderValue = false; 175 } else { 176 headerValue = value; 177 } 178 } else if (name.equalsIgnoreCase("Pragma")) { 179 // Might specify additional cache-control params. We invalidate just in case. 180 canUseHeaderValue = false; 181 } else { 182 continue; 183 } 184 185 int pos = 0; 186 while (pos < value.length()) { 187 int tokenStart = pos; 188 pos = HeaderParser.skipUntil(value, pos, "=,;"); 189 String directive = value.substring(tokenStart, pos).trim(); 190 String parameter; 191 192 if (pos == value.length() || value.charAt(pos) == ',' || value.charAt(pos) == ';') { 193 pos++; // consume ',' or ';' (if necessary) 194 parameter = null; 195 } else { 196 pos++; // consume '=' 197 pos = HeaderParser.skipWhitespace(value, pos); 198 199 // quoted string 200 if (pos < value.length() && value.charAt(pos) == '\"') { 201 pos++; // consume '"' open quote 202 int parameterStart = pos; 203 pos = HeaderParser.skipUntil(value, pos, "\""); 204 parameter = value.substring(parameterStart, pos); 205 pos++; // consume '"' close quote (if necessary) 206 207 // unquoted string 208 } else { 209 int parameterStart = pos; 210 pos = HeaderParser.skipUntil(value, pos, ",;"); 211 parameter = value.substring(parameterStart, pos).trim(); 212 } 213 } 214 215 if ("no-cache".equalsIgnoreCase(directive)) { 216 noCache = true; 217 } else if ("no-store".equalsIgnoreCase(directive)) { 218 noStore = true; 219 } else if ("max-age".equalsIgnoreCase(directive)) { 220 maxAgeSeconds = HeaderParser.parseSeconds(parameter, -1); 221 } else if ("s-maxage".equalsIgnoreCase(directive)) { 222 sMaxAgeSeconds = HeaderParser.parseSeconds(parameter, -1); 223 } else if ("private".equalsIgnoreCase(directive)) { 224 isPrivate = true; 225 } else if ("public".equalsIgnoreCase(directive)) { 226 isPublic = true; 227 } else if ("must-revalidate".equalsIgnoreCase(directive)) { 228 mustRevalidate = true; 229 } else if ("max-stale".equalsIgnoreCase(directive)) { 230 maxStaleSeconds = HeaderParser.parseSeconds(parameter, Integer.MAX_VALUE); 231 } else if ("min-fresh".equalsIgnoreCase(directive)) { 232 minFreshSeconds = HeaderParser.parseSeconds(parameter, -1); 233 } else if ("only-if-cached".equalsIgnoreCase(directive)) { 234 onlyIfCached = true; 235 } else if ("no-transform".equalsIgnoreCase(directive)) { 236 noTransform = true; 237 } 238 } 239 } 240 241 if (!canUseHeaderValue) { 242 headerValue = null; 243 } 244 return new CacheControl(noCache, noStore, maxAgeSeconds, sMaxAgeSeconds, isPrivate, isPublic, 245 mustRevalidate, maxStaleSeconds, minFreshSeconds, onlyIfCached, noTransform, headerValue); 246 } 247 248 @Override public String toString() { 249 String result = headerValue; 250 return result != null ? result : (headerValue = headerValue()); 251 } 252 253 private String headerValue() { 254 StringBuilder result = new StringBuilder(); 255 if (noCache) result.append("no-cache, "); 256 if (noStore) result.append("no-store, "); 257 if (maxAgeSeconds != -1) result.append("max-age=").append(maxAgeSeconds).append(", "); 258 if (sMaxAgeSeconds != -1) result.append("s-maxage=").append(sMaxAgeSeconds).append(", "); 259 if (isPrivate) result.append("private, "); 260 if (isPublic) result.append("public, "); 261 if (mustRevalidate) result.append("must-revalidate, "); 262 if (maxStaleSeconds != -1) result.append("max-stale=").append(maxStaleSeconds).append(", "); 263 if (minFreshSeconds != -1) result.append("min-fresh=").append(minFreshSeconds).append(", "); 264 if (onlyIfCached) result.append("only-if-cached, "); 265 if (noTransform) result.append("no-transform, "); 266 if (result.length() == 0) return ""; 267 result.delete(result.length() - 2, result.length()); 268 return result.toString(); 269 } 270 271 /** Builds a {@code Cache-Control} request header. */ 272 public static final class Builder { 273 boolean noCache; 274 boolean noStore; 275 int maxAgeSeconds = -1; 276 int maxStaleSeconds = -1; 277 int minFreshSeconds = -1; 278 boolean onlyIfCached; 279 boolean noTransform; 280 281 /** Don't accept an unvalidated cached response. */ 282 public Builder noCache() { 283 this.noCache = true; 284 return this; 285 } 286 287 /** Don't store the server's response in any cache. */ 288 public Builder noStore() { 289 this.noStore = true; 290 return this; 291 } 292 293 /** 294 * Sets the maximum age of a cached response. If the cache response's age 295 * exceeds {@code maxAge}, it will not be used and a network request will 296 * be made. 297 * 298 * @param maxAge a non-negative integer. This is stored and transmitted with 299 * {@link TimeUnit#SECONDS} precision; finer precision will be lost. 300 */ 301 public Builder maxAge(int maxAge, TimeUnit timeUnit) { 302 if (maxAge < 0) throw new IllegalArgumentException("maxAge < 0: " + maxAge); 303 long maxAgeSecondsLong = timeUnit.toSeconds(maxAge); 304 this.maxAgeSeconds = maxAgeSecondsLong > Integer.MAX_VALUE 305 ? Integer.MAX_VALUE 306 : (int) maxAgeSecondsLong; 307 return this; 308 } 309 310 /** 311 * Accept cached responses that have exceeded their freshness lifetime by 312 * up to {@code maxStale}. If unspecified, stale cache responses will not be 313 * used. 314 * 315 * @param maxStale a non-negative integer. This is stored and transmitted 316 * with {@link TimeUnit#SECONDS} precision; finer precision will be 317 * lost. 318 */ 319 public Builder maxStale(int maxStale, TimeUnit timeUnit) { 320 if (maxStale < 0) throw new IllegalArgumentException("maxStale < 0: " + maxStale); 321 long maxStaleSecondsLong = timeUnit.toSeconds(maxStale); 322 this.maxStaleSeconds = maxStaleSecondsLong > Integer.MAX_VALUE 323 ? Integer.MAX_VALUE 324 : (int) maxStaleSecondsLong; 325 return this; 326 } 327 328 /** 329 * Sets the minimum number of seconds that a response will continue to be 330 * fresh for. If the response will be stale when {@code minFresh} have 331 * elapsed, the cached response will not be used and a network request will 332 * be made. 333 * 334 * @param minFresh a non-negative integer. This is stored and transmitted 335 * with {@link TimeUnit#SECONDS} precision; finer precision will be 336 * lost. 337 */ 338 public Builder minFresh(int minFresh, TimeUnit timeUnit) { 339 if (minFresh < 0) throw new IllegalArgumentException("minFresh < 0: " + minFresh); 340 long minFreshSecondsLong = timeUnit.toSeconds(minFresh); 341 this.minFreshSeconds = minFreshSecondsLong > Integer.MAX_VALUE 342 ? Integer.MAX_VALUE 343 : (int) minFreshSecondsLong; 344 return this; 345 } 346 347 /** 348 * Only accept the response if it is in the cache. If the response isn't 349 * cached, a {@code 504 Unsatisfiable Request} response will be returned. 350 */ 351 public Builder onlyIfCached() { 352 this.onlyIfCached = true; 353 return this; 354 } 355 356 /** Don't accept a transformed response. */ 357 public Builder noTransform() { 358 this.noTransform = true; 359 return this; 360 } 361 362 public CacheControl build() { 363 return new CacheControl(this); 364 } 365 } 366 } 367