1 /*
2  * Copyright (C) 2019 Square, Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */

16 package okhttp3;
17
18 import java.util.concurrent.TimeUnit;
19 import javax.annotation.Nullable;
20 import okhttp3.internal.http.HttpHeaders;
21
22 /**
23  * A Cache-Control header with cache directives from a server or client. These directives set policy
24  * on what responses can be stored, and which requests can be satisfied by those stored responses.
25  *
26  * <p>See <a href="https://tools.ietf.org/html/rfc7234#section-5.2">RFC 7234, 5.2</a>.
27  */

28 public final class CacheControl {
29   /**
30    * Cache control request directives that require network validation of responses. Note that such
31    * requests may be assisted by the cache via conditional GET requests.
32    */

33   public static final CacheControl FORCE_NETWORK = new Builder().noCache().build();
34
35   /**
36    * Cache control request directives that uses the cache only, even if the cached response is
37    * stale. If the response isn't available in the cache or requires server validation, the call
38    * will fail with a {@code 504 Unsatisfiable Request}.
39    */

40   public static final CacheControl FORCE_CACHE = new Builder()
41       .onlyIfCached()
42       .maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS)
43       .build();
44
45   private final boolean noCache;
46   private final boolean noStore;
47   private final int maxAgeSeconds;
48   private final int sMaxAgeSeconds;
49   private final boolean isPrivate;
50   private final boolean isPublic;
51   private final boolean mustRevalidate;
52   private final int maxStaleSeconds;
53   private final int minFreshSeconds;
54   private final boolean onlyIfCached;
55   private final boolean noTransform;
56   private final boolean immutable;
57
58   @Nullable String headerValue; // Lazily computed, null if absent.
59
60   private CacheControl(boolean noCache, boolean noStore, int maxAgeSeconds, int sMaxAgeSeconds,
61       boolean isPrivate, boolean isPublic, boolean mustRevalidate, int maxStaleSeconds,
62       int minFreshSeconds, boolean onlyIfCached, boolean noTransform, boolean immutable,
63       @Nullable String headerValue) {
64     this.noCache = noCache;
65     this.noStore = noStore;
66     this.maxAgeSeconds = maxAgeSeconds;
67     this.sMaxAgeSeconds = sMaxAgeSeconds;
68     this.isPrivate = isPrivate;
69     this.isPublic = isPublic;
70     this.mustRevalidate = mustRevalidate;
71     this.maxStaleSeconds = maxStaleSeconds;
72     this.minFreshSeconds = minFreshSeconds;
73     this.onlyIfCached = onlyIfCached;
74     this.noTransform = noTransform;
75     this.immutable = immutable;
76     this.headerValue = headerValue;
77   }
78
79   CacheControl(Builder builder) {
80     this.noCache = builder.noCache;
81     this.noStore = builder.noStore;
82     this.maxAgeSeconds = builder.maxAgeSeconds;
83     this.sMaxAgeSeconds = -1;
84     this.isPrivate = false;
85     this.isPublic = false;
86     this.mustRevalidate = false;
87     this.maxStaleSeconds = builder.maxStaleSeconds;
88     this.minFreshSeconds = builder.minFreshSeconds;
89     this.onlyIfCached = builder.onlyIfCached;
90     this.noTransform = builder.noTransform;
91     this.immutable = builder.immutable;
92   }
93
94   /**
95    * In a response, this field's name "no-cache" is misleading. It doesn't prevent us from caching
96    * the response; it only means we have to validate the response with the origin server before
97    * returning it. We can do this with a conditional GET.
98    *
99    * <p>In a request, it means do not use a cache to satisfy the request.
100    */

101   public boolean noCache() {
102     return noCache;
103   }
104
105   /** If truethis response should not be cached. */
106   public boolean noStore() {
107     return noStore;
108   }
109
110   /**
111    * The duration past the response's served date that it can be served without validation.
112    */

113   public int maxAgeSeconds() {
114     return maxAgeSeconds;
115   }
116
117   /**
118    * The "s-maxage" directive is the max age for shared caches. Not to be confused with "max-age"
119    * for non-shared caches, As in Firefox and Chrome, this directive is not honored by this cache.
120    */

121   public int sMaxAgeSeconds() {
122     return sMaxAgeSeconds;
123   }
124
125   public boolean isPrivate() {
126     return isPrivate;
127   }
128
129   public boolean isPublic() {
130     return isPublic;
131   }
132
133   public boolean mustRevalidate() {
134     return mustRevalidate;
135   }
136
137   public int maxStaleSeconds() {
138     return maxStaleSeconds;
139   }
140
141   public int minFreshSeconds() {
142     return minFreshSeconds;
143   }
144
145   /**
146    * This field's name "only-if-cached" is misleading. It actually means "do not use the network".
147    * It is set by a client who only wants to make a request if it can be fully satisfied by the
148    * cache. Cached responses that would require validation (ie. conditional gets) are not permitted
149    * if this header is set.
150    */

151   public boolean onlyIfCached() {
152     return onlyIfCached;
153   }
154
155   public boolean noTransform() {
156     return noTransform;
157   }
158
159   public boolean immutable() {
160     return immutable;
161   }
162
163   /**
164    * Returns the cache directives of {@code headers}. This honors both Cache-Control and Pragma
165    * headers if they are present.
166    */

167   public static CacheControl parse(Headers headers) {
168     boolean noCache = false;
169     boolean noStore = false;
170     int maxAgeSeconds = -1;
171     int sMaxAgeSeconds = -1;
172     boolean isPrivate = false;
173     boolean isPublic = false;
174     boolean mustRevalidate = false;
175     int maxStaleSeconds = -1;
176     int minFreshSeconds = -1;
177     boolean onlyIfCached = false;
178     boolean noTransform = false;
179     boolean immutable = false;
180
181     boolean canUseHeaderValue = true;
182     String headerValue = null;
183
184     for (int i = 0, size = headers.size(); i < size; i++) {
185       String name = headers.name(i);
186       String value = headers.value(i);
187
188       if (name.equalsIgnoreCase("Cache-Control")) {
189         if (headerValue != null) {
190           // Multiple cache-control headers means we can't use the raw value.
191           canUseHeaderValue = false;
192         } else {
193           headerValue = value;
194         }
195       } else if (name.equalsIgnoreCase("Pragma")) {
196         // Might specify additional cache-control params. We invalidate just in case.
197         canUseHeaderValue = false;
198       } else {
199         continue;
200       }
201
202       int pos = 0;
203       while (pos < value.length()) {
204         int tokenStart = pos;
205         pos = HttpHeaders.skipUntil(value, pos, "=,;");
206         String directive = value.substring(tokenStart, pos).trim();
207         String parameter;
208
209         if (pos == value.length() || value.charAt(pos) == ',' || value.charAt(pos) == ';') {
210           pos++; // consume ',' or ';' (if necessary)
211           parameter = null;
212         } else {
213           pos++; // consume '='
214           pos = HttpHeaders.skipWhitespace(value, pos);
215
216           // quoted string
217           if (pos < value.length() && value.charAt(pos) == '\"') {
218             pos++; // consume '"' open quote
219             int parameterStart = pos;
220             pos = HttpHeaders.skipUntil(value, pos, "\"");
221             parameter = value.substring(parameterStart, pos);
222             pos++; // consume '"' close quote (if necessary)
223
224             // unquoted string
225           } else {
226             int parameterStart = pos;
227             pos = HttpHeaders.skipUntil(value, pos, ",;");
228             parameter = value.substring(parameterStart, pos).trim();
229           }
230         }
231
232         if ("no-cache".equalsIgnoreCase(directive)) {
233           noCache = true;
234         } else if ("no-store".equalsIgnoreCase(directive)) {
235           noStore = true;
236         } else if ("max-age".equalsIgnoreCase(directive)) {
237           maxAgeSeconds = HttpHeaders.parseSeconds(parameter, -1);
238         } else if ("s-maxage".equalsIgnoreCase(directive)) {
239           sMaxAgeSeconds = HttpHeaders.parseSeconds(parameter, -1);
240         } else if ("private".equalsIgnoreCase(directive)) {
241           isPrivate = true;
242         } else if ("public".equalsIgnoreCase(directive)) {
243           isPublic = true;
244         } else if ("must-revalidate".equalsIgnoreCase(directive)) {
245           mustRevalidate = true;
246         } else if ("max-stale".equalsIgnoreCase(directive)) {
247           maxStaleSeconds = HttpHeaders.parseSeconds(parameter, Integer.MAX_VALUE);
248         } else if ("min-fresh".equalsIgnoreCase(directive)) {
249           minFreshSeconds = HttpHeaders.parseSeconds(parameter, -1);
250         } else if ("only-if-cached".equalsIgnoreCase(directive)) {
251           onlyIfCached = true;
252         } else if ("no-transform".equalsIgnoreCase(directive)) {
253           noTransform = true;
254         } else if ("immutable".equalsIgnoreCase(directive)) {
255           immutable = true;
256         }
257       }
258     }
259
260     if (!canUseHeaderValue) {
261       headerValue = null;
262     }
263     return new CacheControl(noCache, noStore, maxAgeSeconds, sMaxAgeSeconds, isPrivate, isPublic,
264         mustRevalidate, maxStaleSeconds, minFreshSeconds, onlyIfCached, noTransform, immutable,
265         headerValue);
266   }
267
268   @Override public String toString() {
269     String result = headerValue;
270     return result != null ? result : (headerValue = headerValue());
271   }
272
273   private String headerValue() {
274     StringBuilder result = new StringBuilder();
275     if (noCache) result.append("no-cache, ");
276     if (noStore) result.append("no-store, ");
277     if (maxAgeSeconds != -1) result.append("max-age=").append(maxAgeSeconds).append(", ");
278     if (sMaxAgeSeconds != -1) result.append("s-maxage=").append(sMaxAgeSeconds).append(", ");
279     if (isPrivate) result.append("private, ");
280     if (isPublic) result.append("public, ");
281     if (mustRevalidate) result.append("must-revalidate, ");
282     if (maxStaleSeconds != -1) result.append("max-stale=").append(maxStaleSeconds).append(", ");
283     if (minFreshSeconds != -1) result.append("min-fresh=").append(minFreshSeconds).append(", ");
284     if (onlyIfCached) result.append("only-if-cached, ");
285     if (noTransform) result.append("no-transform, ");
286     if (immutable) result.append("immutable, ");
287     if (result.length() == 0) return "";
288     result.delete(result.length() - 2, result.length());
289     return result.toString();
290   }
291
292   /** Builds a {@code Cache-Control} request header. */
293   public static final class Builder {
294     boolean noCache;
295     boolean noStore;
296     int maxAgeSeconds = -1;
297     int maxStaleSeconds = -1;
298     int minFreshSeconds = -1;
299     boolean onlyIfCached;
300     boolean noTransform;
301     boolean immutable;
302
303     /** Don't accept an unvalidated cached response. */
304     public Builder noCache() {
305       this.noCache = true;
306       return this;
307     }
308
309     /** Don't store the server's response in any cache. */
310     public Builder noStore() {
311       this.noStore = true;
312       return this;
313     }
314
315     /**
316      * Sets the maximum age of a cached response. If the cache response's age exceeds {@code
317      * maxAge}, it will not be used and a network request will be made.
318      *
319      * @param maxAge a non-negative integer. This is stored and transmitted with {@link
320      * TimeUnit#SECONDS} precision; finer precision will be lost.
321      */

322     public Builder maxAge(int maxAge, TimeUnit timeUnit) {
323       if (maxAge < 0) throw new IllegalArgumentException("maxAge < 0: " + maxAge);
324       long maxAgeSecondsLong = timeUnit.toSeconds(maxAge);
325       this.maxAgeSeconds = maxAgeSecondsLong > Integer.MAX_VALUE
326           ? Integer.MAX_VALUE
327           : (int) maxAgeSecondsLong;
328       return this;
329     }
330
331     /**
332      * Accept cached responses that have exceeded their freshness lifetime by up to {@code
333      * maxStale}. If unspecified, stale cache responses will not be used.
334      *
335      * @param maxStale a non-negative integer. This is stored and transmitted with {@link
336      * TimeUnit#SECONDS} precision; finer precision will be lost.
337      */

338     public Builder maxStale(int maxStale, TimeUnit timeUnit) {
339       if (maxStale < 0) throw new IllegalArgumentException("maxStale < 0: " + maxStale);
340       long maxStaleSecondsLong = timeUnit.toSeconds(maxStale);
341       this.maxStaleSeconds = maxStaleSecondsLong > Integer.MAX_VALUE
342           ? Integer.MAX_VALUE
343           : (int) maxStaleSecondsLong;
344       return this;
345     }
346
347     /**
348      * Sets the minimum number of seconds that a response will continue to be fresh for. If the
349      * response will be stale when {@code minFresh} have elapsed, the cached response will not be
350      * used and a network request will be made.
351      *
352      * @param minFresh a non-negative integer. This is stored and transmitted with {@link
353      * TimeUnit#SECONDS} precision; finer precision will be lost.
354      */

355     public Builder minFresh(int minFresh, TimeUnit timeUnit) {
356       if (minFresh < 0) throw new IllegalArgumentException("minFresh < 0: " + minFresh);
357       long minFreshSecondsLong = timeUnit.toSeconds(minFresh);
358       this.minFreshSeconds = minFreshSecondsLong > Integer.MAX_VALUE
359           ? Integer.MAX_VALUE
360           : (int) minFreshSecondsLong;
361       return this;
362     }
363
364     /**
365      * Only accept the response if it is in the cache. If the response isn't cached, a {@code 504
366      * Unsatisfiable Request} response will be returned.
367      */

368     public Builder onlyIfCached() {
369       this.onlyIfCached = true;
370       return this;
371     }
372
373     /** Don't accept a transformed response. */
374     public Builder noTransform() {
375       this.noTransform = true;
376       return this;
377     }
378
379     public Builder immutable() {
380       this.immutable = true;
381       return this;
382     }
383
384     public CacheControl build() {
385       return new CacheControl(this);
386     }
387   }
388 }
389